562 lines
24 KiB
Python
562 lines
24 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.')
|
|
|
|
|
|
class VmwareAttributesValidator(base.BasicValidator):
|
|
|
|
single_schema = cluster_schema.vmware_attributes_schema
|
|
|
|
@staticmethod
|
|
def _get_target_node_id(nova_compute_data):
|
|
return nova_compute_data['target_node']['current']['id']
|
|
|
|
@classmethod
|
|
def _validate_updated_attributes(cls, attributes, instance):
|
|
"""Validate that attributes contains changes only for allowed fields.
|
|
|
|
:param attributes: new vmware attribute settings for db instance
|
|
:param instance: nailgun.db.sqlalchemy.models.VmwareAttributes instance
|
|
"""
|
|
metadata = instance.editable.get('metadata', {})
|
|
db_editable_attributes = instance.editable.get('value', {})
|
|
new_editable_attributes = attributes.get('editable', {}).get('value')
|
|
for attribute_metadata in metadata:
|
|
if attribute_metadata.get('type') == 'array':
|
|
attribute_name = attribute_metadata['name']
|
|
cls._check_attribute(
|
|
attribute_metadata,
|
|
db_editable_attributes.get(attribute_name),
|
|
new_editable_attributes.get(attribute_name)
|
|
)
|
|
else:
|
|
cls._check_attribute(
|
|
attribute_metadata,
|
|
db_editable_attributes,
|
|
new_editable_attributes
|
|
)
|
|
|
|
@classmethod
|
|
def _check_attribute(cls, metadata, attributes, new_attributes):
|
|
"""Check new_attributes is equal with attributes except editable fields
|
|
|
|
:param metadata: dict describes structure and properties of attributes
|
|
:param attributes: attributes which is the basis for comparison
|
|
:param new_attributes: attributes with modifications to check
|
|
"""
|
|
if type(attributes) != type(new_attributes):
|
|
raise errors.InvalidData(
|
|
"Value type of '{0}' attribute couldn't be changed.".
|
|
format(metadata.get('label') or metadata.get('name')),
|
|
log_message=True
|
|
)
|
|
# if metadata field contains editable_for_deployed = True, attribute
|
|
# and all its childs may be changed too. No need to check it.
|
|
if metadata.get('editable_for_deployed'):
|
|
return
|
|
|
|
# no 'fields' in metadata means that attribute has no any childs(leaf)
|
|
if 'fields' not in metadata:
|
|
if attributes != new_attributes:
|
|
raise errors.InvalidData(
|
|
"Value of '{0}' attribute couldn't be changed.".
|
|
format(metadata.get('label') or metadata.get('name')),
|
|
log_message=True
|
|
)
|
|
return
|
|
|
|
fields_sort_functions = {
|
|
'availability_zones': lambda x: x['az_name'],
|
|
'nova_computes': lambda x: x['vsphere_cluster']
|
|
}
|
|
field_name = metadata['name']
|
|
if isinstance(attributes, (list, tuple)):
|
|
if len(attributes) != len(new_attributes):
|
|
raise errors.InvalidData(
|
|
"Value of '{0}' attribute couldn't be changed.".
|
|
format(metadata.get('label') or metadata.get('name')),
|
|
log_message=True
|
|
)
|
|
attributes = sorted(
|
|
attributes, key=fields_sort_functions.get(field_name))
|
|
new_attributes = sorted(
|
|
new_attributes, key=fields_sort_functions.get(field_name))
|
|
for item, new_item in six.moves.zip(attributes, new_attributes):
|
|
for field_metadata in metadata['fields']:
|
|
cls._check_attribute(field_metadata,
|
|
item.get(field_metadata['name']),
|
|
new_item.get(field_metadata['name']))
|
|
elif isinstance(attributes, dict):
|
|
for field_metadata in metadata['fields']:
|
|
cls._check_attribute(field_metadata,
|
|
attributes.get(field_name),
|
|
new_attributes.get(field_name))
|
|
|
|
@classmethod
|
|
def _validate_nova_computes(cls, attributes, instance):
|
|
"""Validates a 'nova_computes' attributes from vmware_attributes
|
|
|
|
Raise InvalidData exception if new attributes is not valid.
|
|
|
|
:param instance: nailgun.db.sqlalchemy.models.VmwareAttributes instance
|
|
:param attributes: new attributes for db instance for validation
|
|
"""
|
|
input_nova_computes = objects.VmwareAttributes.get_nova_computes_attrs(
|
|
attributes.get('editable'))
|
|
|
|
cls.check_nova_compute_duplicate_and_empty_values(input_nova_computes)
|
|
|
|
db_nova_computes = objects.VmwareAttributes.get_nova_computes_attrs(
|
|
instance.editable)
|
|
if instance.cluster.is_locked:
|
|
cls.check_operational_controllers_settings(input_nova_computes,
|
|
db_nova_computes)
|
|
operational_compute_nodes = objects.Cluster.\
|
|
get_operational_vmware_compute_nodes(instance.cluster)
|
|
cls.check_operational_node_settings(
|
|
input_nova_computes, db_nova_computes, operational_compute_nodes)
|
|
|
|
@classmethod
|
|
def check_nova_compute_duplicate_and_empty_values(cls, attributes):
|
|
"""Check 'nova_computes' attributes for empty and duplicate values."""
|
|
nova_compute_attributes_sets = {
|
|
'vsphere_cluster': set(),
|
|
'service_name': set(),
|
|
'target_node': set()
|
|
}
|
|
for nova_compute_data in attributes:
|
|
for attr, values in six.iteritems(nova_compute_attributes_sets):
|
|
if attr == 'target_node':
|
|
settings_value = cls._get_target_node_id(nova_compute_data)
|
|
if settings_value == 'controllers':
|
|
continue
|
|
else:
|
|
settings_value = nova_compute_data.get(attr)
|
|
if not settings_value:
|
|
raise errors.InvalidData(
|
|
"Empty value for attribute '{0}' is not allowed".
|
|
format(attr),
|
|
log_message=True
|
|
)
|
|
if settings_value in values:
|
|
raise errors.InvalidData(
|
|
"Duplicate value '{0}' for attribute '{1}' is "
|
|
"not allowed".format(settings_value, attr),
|
|
log_message=True
|
|
)
|
|
values.add(settings_value)
|
|
|
|
@classmethod
|
|
def check_operational_node_settings(cls, input_nova_computes,
|
|
db_nova_computes, operational_nodes):
|
|
"""Validates a 'nova_computes' attributes for operational compute nodes
|
|
|
|
Raise InvalidData exception if nova_compute settings will be changed or
|
|
deleted for deployed nodes with role 'compute-vmware' that wasn't
|
|
marked for deletion
|
|
|
|
:param input_nova_computes: new nova_compute attributes
|
|
:type input_nova_computes: list of dicts
|
|
:param db_nova_computes: nova_computes attributes stored in db
|
|
:type db_nova_computes: list of dicts
|
|
:param operational_nodes: list of operational vmware-compute nodes
|
|
:type operational_nodes: list of nailgun.db.sqlalchemy.models.Node
|
|
"""
|
|
input_computes_by_node_name = dict(
|
|
(cls._get_target_node_id(nc), nc) for nc in input_nova_computes)
|
|
db_computes_by_node_name = dict(
|
|
(cls._get_target_node_id(nc), nc) for nc in db_nova_computes)
|
|
|
|
for node in operational_nodes:
|
|
node_hostname = node.hostname
|
|
input_nova_compute = input_computes_by_node_name.get(node_hostname)
|
|
if not input_nova_compute:
|
|
raise errors.InvalidData(
|
|
"The following compute-vmware node couldn't be "
|
|
"deleted from vSphere cluster: {0}".format(node.name),
|
|
log_message=True
|
|
)
|
|
db_nova_compute = db_computes_by_node_name.get(node_hostname)
|
|
for attr, db_value in six.iteritems(db_nova_compute):
|
|
if attr != 'target_node' and \
|
|
db_value != input_nova_compute.get(attr):
|
|
raise errors.InvalidData(
|
|
"Parameter '{0}' of nova compute instance with target "
|
|
"node '{1}' couldn't be changed".format(
|
|
attr, node.name),
|
|
log_message=True
|
|
)
|
|
|
|
@classmethod
|
|
def check_operational_controllers_settings(cls, input_nova_computes,
|
|
db_nova_computes):
|
|
"""Check deployed nova computes settings with target = controllers.
|
|
|
|
Raise InvalidData exception if any deployed nova computes clusters with
|
|
target 'controllers' were added, removed or modified.
|
|
|
|
:param input_nova_computes: new nova_compute settings
|
|
:type input_nova_computes: list of dicts
|
|
:param db_nova_computes: nova_computes settings stored in db
|
|
:type db_nova_computes: list of dicts
|
|
"""
|
|
input_computes_by_vsphere_name = dict(
|
|
(nc['vsphere_cluster'], nc) for nc in input_nova_computes if
|
|
cls._get_target_node_id(nc) == 'controllers'
|
|
)
|
|
db_clusters_names = set()
|
|
for db_nova_compute in db_nova_computes:
|
|
target_name = cls._get_target_node_id(db_nova_compute)
|
|
if target_name == 'controllers':
|
|
vsphere_name = db_nova_compute['vsphere_cluster']
|
|
input_nova_compute = \
|
|
input_computes_by_vsphere_name.get(vsphere_name)
|
|
if not input_nova_compute:
|
|
raise errors.InvalidData(
|
|
"Nova compute instance with target 'controllers' and "
|
|
"vSphere cluster {0} couldn't be deleted from "
|
|
"operational environment.".format(vsphere_name),
|
|
log_message=True
|
|
)
|
|
for attr, db_value in six.iteritems(db_nova_compute):
|
|
input_value = input_nova_compute.get(attr)
|
|
if attr == 'target_node':
|
|
db_value = cls._get_target_node_id(db_nova_compute)
|
|
input_value = cls._get_target_node_id(
|
|
input_nova_compute)
|
|
if db_value != input_value:
|
|
raise errors.InvalidData(
|
|
"Parameter '{0}' of nova compute instance with "
|
|
"vSphere cluster name '{1}' couldn't be changed".
|
|
format(attr, vsphere_name),
|
|
log_message=True
|
|
)
|
|
db_clusters_names.add(vsphere_name)
|
|
|
|
input_clusters_names = set(input_computes_by_vsphere_name)
|
|
if input_clusters_names - db_clusters_names:
|
|
raise errors.InvalidData(
|
|
"Nova compute instances with target 'controllers' couldn't be "
|
|
"added to operational environment. Check nova compute "
|
|
"instances with the following vSphere cluster names: {0}".
|
|
format(', '.join(
|
|
sorted(input_clusters_names - db_clusters_names))),
|
|
log_message=True
|
|
)
|
|
|
|
@classmethod
|
|
def validate(cls, data, instance):
|
|
d = cls.validate_json(data)
|
|
if 'metadata' in d.get('editable'):
|
|
db_metadata = instance.editable.get('metadata')
|
|
input_metadata = d.get('editable').get('metadata')
|
|
if db_metadata != input_metadata:
|
|
raise errors.InvalidData(
|
|
'Metadata shouldn\'t change',
|
|
log_message=True
|
|
)
|
|
|
|
if instance.cluster.is_locked:
|
|
cls._validate_updated_attributes(d, instance)
|
|
cls._validate_nova_computes(d, instance)
|
|
|
|
# TODO(apopovych): write validation processing from
|
|
# openstack.yaml for vmware
|
|
return d
|