fuel-web/nailgun/nailgun/api/v1/validators/cluster.py

300 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2013 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from distutils import version
import six
import sqlalchemy as sa
from nailgun.api.v1.validators import base
from nailgun.api.v1.validators.json_schema import cluster as cluster_schema
from nailgun.api.v1.validators.node import ProvisionSelectedNodesValidator
from nailgun import consts
from nailgun.db import db
from nailgun.db.sqlalchemy.models import Node
from nailgun import errors
from nailgun import objects
from nailgun.plugins.manager import PluginManager
from nailgun.utils.restrictions import ComponentsRestrictions
class ClusterValidator(base.BasicValidator):
single_schema = cluster_schema.single_schema
collection_schema = cluster_schema.collection_schema
_blocked_for_update = (
'net_provider',
)
@classmethod
def _validate_common(cls, data, instance=None):
d = cls.validate_json(data)
release_id = d.get("release", d.get("release_id"))
if release_id:
release = objects.Release.get_by_uid(release_id)
if not release:
raise errors.InvalidData(
"Invalid release ID", log_message=True)
if not objects.Release.is_deployable(release):
raise errors.NotAllowed(
"Release with ID '{0}' is not deployable.".format(
release_id), log_message=True)
cls._validate_mode(d, release)
return d
@classmethod
def _validate_components(cls, release_id, components_list):
release = objects.Release.get_by_uid(release_id)
release_components = objects.Release.get_all_components(release)
ComponentsRestrictions.validate_components(
components_list,
release_components,
release.required_component_types)
@classmethod
def validate(cls, data):
d = cls._validate_common(data)
# TODO(ikalnitsky): move it to _validate_common when
# PATCH method will be implemented
release_id = d.get("release", d.get("release_id", None))
if not release_id:
raise errors.InvalidData(
u"Release ID is required", log_message=True)
if "name" in d:
if objects.ClusterCollection.filter_by(
None, name=d["name"]).first():
raise errors.AlreadyExists(
"Environment with this name already exists",
log_message=True
)
if "components" in d:
cls._validate_components(release_id, d['components'])
return d
@classmethod
def validate_update(cls, data, instance):
d = cls._validate_common(data, instance=instance)
if "name" in d:
query = objects.ClusterCollection.filter_by_not(
None, id=instance.id)
if objects.ClusterCollection.filter_by(
query, name=d["name"]).first():
raise errors.AlreadyExists(
"Environment with this name already exists",
log_message=True
)
for k in cls._blocked_for_update:
if k in d and getattr(instance, k) != d[k]:
raise errors.InvalidData(
u"Changing '{0}' for environment is prohibited".format(k),
log_message=True
)
cls._validate_mode(d, instance.release)
if 'nodes' in d:
# Here d['nodes'] is list of node IDs
# to be assigned to the cluster.
cls._validate_nodes(d['nodes'], instance)
return d
@classmethod
def _validate_mode(cls, data, release):
mode = data.get("mode")
if mode and mode not in release.modes:
modes_list = ', '.join(release.modes)
raise errors.InvalidData(
"Cannot deploy in {0} mode in current release."
" Need to be one of: {1}".format(
mode, modes_list),
log_message=True
)
@classmethod
def _validate_nodes(cls, new_node_ids, instance):
set_new_node_ids = set(new_node_ids)
set_old_node_ids = set(objects.Cluster.get_nodes_ids(instance))
nodes_to_add = set_new_node_ids - set_old_node_ids
nodes_to_remove = set_old_node_ids - set_new_node_ids
hostnames_to_add = [x[0] for x in db.query(Node.hostname)
.filter(Node.id.in_(nodes_to_add)).all()]
duplicated = [x[0] for x in db.query(Node.hostname).filter(
sa.and_(
Node.hostname.in_(hostnames_to_add),
Node.cluster_id == instance.id,
Node.id.notin_(nodes_to_remove)
)
).all()]
if duplicated:
raise errors.AlreadyExists(
"Nodes with hostnames [{0}] already exist in cluster {1}."
.format(",".join(duplicated), instance.id)
)
class ClusterAttributesValidator(base.BasicAttributesValidator):
@classmethod
def validate(cls, data, cluster=None, force=False):
d = cls.validate_json(data)
if "generated" in d:
raise errors.InvalidData(
"It is not allowed to update generated attributes",
log_message=True
)
if "editable" in d and not isinstance(d["editable"], dict):
raise errors.InvalidData(
"Editable attributes should be a dictionary",
log_message=True
)
attrs = d
models = None
if cluster is not None:
attrs = objects.Cluster.get_updated_editable_attributes(cluster, d)
cls.validate_provision(cluster, attrs)
cls.validate_allowed_attributes(cluster, d, force)
models = objects.Cluster.get_restrictions_models(
cluster, attrs=attrs.get('editable', {}))
cls.validate_attributes(attrs.get('editable', {}), models, force=force)
return d
@classmethod
def validate_provision(cls, cluster, attrs):
# NOTE(agordeev): disable classic provisioning for 7.0 or higher
if version.StrictVersion(cluster.release.environment_version) >= \
version.StrictVersion(consts.FUEL_IMAGE_BASED_ONLY):
provision_data = attrs['editable'].get('provision')
if provision_data:
if provision_data['method']['value'] != \
consts.PROVISION_METHODS.image:
raise errors.InvalidData(
u"Cannot use classic provisioning for adding "
u"nodes to environment",
log_message=True)
else:
raise errors.InvalidData(
u"Provisioning method is not set. Unable to continue",
log_message=True)
@classmethod
def validate_allowed_attributes(cls, cluster, data, force):
"""Validates if attributes are hot pluggable or not.
:param cluster: A cluster instance
:type cluster: nailgun.db.sqlalchemy.models.cluster.Cluster
:param data: Changed attributes of cluster
:type data: dict
:param force: Allow forcefully update cluster attributes
:type force: bool
:raises: errors.NotAllowed
"""
# TODO(need to enable restrictions check for cluster attributes[1])
# [1] https://bugs.launchpad.net/fuel/+bug/1519904
# Validates only that plugin can be installed on deployed env.
# If cluster is locked we have to check which attributes
# we want to change and block an entire operation if there
# one with always_editable=False.
if not cluster.is_locked or force:
return
editable_cluster = objects.Cluster.get_editable_attributes(
cluster, all_plugins_versions=True)
editable_request = data.get('editable', {})
for attr_name, attr_request in six.iteritems(editable_request):
attr_cluster = editable_cluster.get(attr_name, {})
meta_cluster = attr_cluster.get('metadata', {})
meta_request = attr_request.get('metadata', {})
if PluginManager.is_plugin_data(attr_cluster):
if meta_request['enabled']:
changed_ids = [meta_request['chosen_id']]
if meta_cluster['enabled']:
changed_ids.append(meta_cluster['chosen_id'])
changed_ids = set(changed_ids)
elif meta_cluster['enabled']:
changed_ids = [meta_cluster['chosen_id']]
else:
continue
for plugin in meta_cluster['versions']:
plugin_id = plugin['metadata']['plugin_id']
always_editable = plugin['metadata']\
.get('always_editable', False)
if plugin_id in changed_ids and not always_editable:
raise errors.NotAllowed(
"Plugin '{0}' version '{1}' couldn't be changed "
"after or during deployment."
.format(attr_name,
plugin['metadata']['plugin_version']),
log_message=True
)
elif not meta_cluster.get('always_editable', False):
raise errors.NotAllowed(
"Environment attribute '{0}' couldn't be changed "
"after or during deployment.".format(attr_name),
log_message=True
)
class ClusterChangesValidator(base.BaseDefferedTaskValidator):
@classmethod
def validate(cls, cluster, graph_type=None):
cls.validate_release(cluster=cluster, graph_type=graph_type)
ProvisionSelectedNodesValidator.validate_provision(None, cluster)
class ClusterStopDeploymentValidator(base.BaseDefferedTaskValidator):
@classmethod
def validate(cls, cluster):
super(ClusterStopDeploymentValidator, cls).validate(cluster)
# NOTE(aroma): the check must regard the case when stop deployment
# is called for cluster that was created before master node upgrade
# to versions >= 8.0 and so having 'deployed_before' flag absent
# in their attributes.
# NOTE(vsharshov): task based deployment (>=9.0) implements
# safe way to stop deployment action, so we can enable
# stop deployment for such cluster without restrictions.
# But it is still need to be disabled for old env < 9.0
# which was already deployed once[1]
# [1]: https://bugs.launchpad.net/fuel/+bug/1529691
generated = cluster.attributes.generated
if generated.get('deployed_before', {}).get('value') and\
not objects.Release.is_lcm_supported(cluster.release):
raise errors.CannotBeStopped('Current deployment process is '
'running on a pre-deployed cluster '
'that does not support LCM.')