Add possibilities to add & delete VMware clusters on operational env
* Allow update VMWareAttributes for locked cluster if has pending addition/deletion 'compute-vmware' nodes * Add validation of 'nova_computes' section in vmware attributes Change-Id: I16c1826de81251f5e6ce626b730988127160202f Implements: blueprint add-vmware-clusters
This commit is contained in:
parent
218e8a2ab3
commit
3450d676de
@ -330,7 +330,8 @@ class VmwareAttributesHandler(BaseHandler):
|
||||
if not attributes:
|
||||
raise self.http(404, "No vmware attributes found")
|
||||
|
||||
if cluster.is_locked:
|
||||
if cluster.is_locked and \
|
||||
not objects.Cluster.has_compute_vmware_changes(cluster):
|
||||
raise self.http(403, "Environment attributes can't be changed "
|
||||
"after or during deployment.")
|
||||
|
||||
|
@ -318,7 +318,7 @@ class AttributesValidator(BasicValidator):
|
||||
for attrs in data.get('editable', {}).values():
|
||||
if not isinstance(attrs, dict):
|
||||
continue
|
||||
for attr_name, attr in attrs.items():
|
||||
for attr_name, attr in six.iteritems(attrs):
|
||||
cls.validate_attribute(attr_name, attr)
|
||||
|
||||
return data
|
||||
@ -441,8 +441,245 @@ class VmwareAttributesValidator(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(cls, data, instance=None):
|
||||
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')
|
||||
@ -453,6 +690,10 @@ class VmwareAttributesValidator(BasicValidator):
|
||||
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
|
||||
|
@ -1529,6 +1529,7 @@
|
||||
-
|
||||
name: "nova_computes"
|
||||
type: "array"
|
||||
editable_for_deployed: true
|
||||
fields:
|
||||
-
|
||||
name: "vsphere_cluster"
|
||||
|
@ -1284,6 +1284,31 @@ class Cluster(NailgunObject):
|
||||
instance.nodes if nodes is None else nodes
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def has_compute_vmware_changes(cls, instance):
|
||||
"""Checks if any 'compute-vmware' nodes are waiting for deployment.
|
||||
|
||||
:param instance: cluster for checking
|
||||
:type instance: nailgun.db.sqlalchemy.models.Cluster instance
|
||||
"""
|
||||
compute_vmware_nodes_query = db().query(models.Node).filter_by(
|
||||
cluster_id=instance.id
|
||||
).filter(sa.or_(
|
||||
sa.and_(models.Node.roles.any('compute-vmware'),
|
||||
models.Node.pending_deletion),
|
||||
models.Node.pending_roles.any('compute-vmware')
|
||||
))
|
||||
return db().query(compute_vmware_nodes_query.exists()).scalar()
|
||||
|
||||
@classmethod
|
||||
def get_operational_vmware_compute_nodes(cls, instance):
|
||||
return db().query(models.Node).filter_by(
|
||||
cluster_id=instance.id
|
||||
).filter(
|
||||
models.Node.roles.any('compute-vmware'),
|
||||
sa.not_(models.Node.pending_deletion)
|
||||
).all()
|
||||
|
||||
|
||||
class ClusterCollection(NailgunCollection):
|
||||
"""Cluster collection."""
|
||||
@ -1294,3 +1319,22 @@ class ClusterCollection(NailgunCollection):
|
||||
|
||||
class VmwareAttributes(NailgunObject):
|
||||
model = models.VmwareAttributes
|
||||
|
||||
@staticmethod
|
||||
def get_nova_computes_attrs(attributes):
|
||||
return attributes.get('value', {}).get(
|
||||
'availability_zones', [{}])[0].get('nova_computes', [])
|
||||
|
||||
@classmethod
|
||||
def get_nova_computes_target_nodes(cls, instance):
|
||||
"""Get data of targets node for all nova computes.
|
||||
|
||||
:param instance: nailgun.db.sqlalchemy.models.Cluster instance
|
||||
:returns: list of dicts that represents nova compute targets
|
||||
"""
|
||||
nova_compute_target_nodes = []
|
||||
for nova_compute in cls.get_nova_computes_attrs(instance.editable):
|
||||
target = nova_compute['target_node']['current']
|
||||
if target['id'] != 'controllers':
|
||||
nova_compute_target_nodes.append(target)
|
||||
return nova_compute_target_nodes
|
||||
|
@ -1267,13 +1267,18 @@ class CheckBeforeDeploymentTask(object):
|
||||
vmware_attributes = task.cluster.vmware_attributes
|
||||
# Old(< 6.1) clusters haven't vmware support
|
||||
if vmware_attributes:
|
||||
cinder_nodes = filter(
|
||||
lambda node: 'cinder' in node.all_roles,
|
||||
task.cluster.nodes)
|
||||
cinder_nodes = [node for node in task.cluster.nodes if
|
||||
'cinder' in node.all_roles]
|
||||
|
||||
if not cinder_nodes:
|
||||
logger.info('There is no any node with "cinder" role provided')
|
||||
|
||||
compute_vmware_nodes = [node for node in task.cluster.nodes if
|
||||
'compute-vmware' in node.all_roles]
|
||||
if compute_vmware_nodes:
|
||||
cls._check_vmware_nova_computes(compute_vmware_nodes,
|
||||
vmware_attributes)
|
||||
|
||||
models = {
|
||||
'settings': attributes,
|
||||
'default': vmware_attributes.editable,
|
||||
@ -1290,6 +1295,63 @@ class CheckBeforeDeploymentTask(object):
|
||||
if errors_msg:
|
||||
raise errors.CheckBeforeDeploymentError('\n'.join(errors_msg))
|
||||
|
||||
@classmethod
|
||||
def _check_vmware_nova_computes(cls, compute_vmware_nodes, attributes):
|
||||
"""Check that nova computes settings is correct for cluster nodes
|
||||
|
||||
:param compute_vmware_nodes: all node with role compute-vmware that
|
||||
belongs to cluster
|
||||
:type compute_vmware_nodes: list of nailgun.db.sqlalchemy.models.Node
|
||||
instances
|
||||
:param attributes: cluster vmware_attributes
|
||||
:type attributes: nailgun.db.sqlalchemy.models.VmwareAttributes
|
||||
:raises: errors.CheckBeforeDeploymentError
|
||||
"""
|
||||
compute_nodes_targets = \
|
||||
objects.VmwareAttributes.get_nova_computes_target_nodes(attributes)
|
||||
compute_nodes_hostnames = set([t['id'] for t in compute_nodes_targets])
|
||||
|
||||
errors_msg = []
|
||||
cluster_nodes_hostname = set()
|
||||
not_deleted_nodes_from_computes = set()
|
||||
not_assigned_nodes_to_computes = set()
|
||||
for node in compute_vmware_nodes:
|
||||
node_hostname = node.hostname
|
||||
if node.pending_deletion:
|
||||
if node_hostname in compute_nodes_hostnames:
|
||||
not_deleted_nodes_from_computes.add(node.name)
|
||||
elif node_hostname not in compute_nodes_hostnames:
|
||||
not_assigned_nodes_to_computes.add(node.name)
|
||||
|
||||
cluster_nodes_hostname.add(node_hostname)
|
||||
|
||||
if not_assigned_nodes_to_computes:
|
||||
errors_msg.append(
|
||||
"The following compute-vmware nodes are not assigned to "
|
||||
"any vCenter cluster: {0}".format(
|
||||
', '.join(sorted(not_assigned_nodes_to_computes))
|
||||
)
|
||||
)
|
||||
if not_deleted_nodes_from_computes:
|
||||
errors_msg.append(
|
||||
"The following nodes are prepared for deletion and "
|
||||
"couldn't be assigned to any vCenter cluster: {0}".format(
|
||||
', '.join(sorted(not_deleted_nodes_from_computes))
|
||||
),
|
||||
)
|
||||
|
||||
alien_nodes_names = [t['label'] for t in compute_nodes_targets if
|
||||
t['id'] not in cluster_nodes_hostname]
|
||||
if alien_nodes_names:
|
||||
errors_msg.append(
|
||||
"The following nodes don't belong to compute-vmware nodes of "
|
||||
"environment and couldn't be assigned to any vSphere cluster: "
|
||||
"{0}".format(', '.join(sorted(alien_nodes_names)))
|
||||
)
|
||||
|
||||
if errors_msg:
|
||||
raise errors.CheckBeforeDeploymentError('\n'.join(errors_msg))
|
||||
|
||||
@classmethod
|
||||
def _validate_network_template(cls, task):
|
||||
cluster = task.cluster
|
||||
|
@ -243,8 +243,8 @@ class EnvironmentManager(object):
|
||||
cluster_data = {
|
||||
'name': 'cluster-api-' + str(randint(0, 1000000)),
|
||||
}
|
||||
editable_attributes = kwargs.pop(
|
||||
'editable_attributes', None)
|
||||
editable_attributes = kwargs.pop('editable_attributes', None)
|
||||
vmware_attributes = kwargs.pop('vmware_attributes', None)
|
||||
|
||||
if kwargs:
|
||||
cluster_data.update(kwargs)
|
||||
@ -277,6 +277,8 @@ class EnvironmentManager(object):
|
||||
if editable_attributes:
|
||||
Cluster.patch_attributes(cluster_db,
|
||||
{'editable': editable_attributes})
|
||||
if vmware_attributes:
|
||||
Cluster.update_vmware_attributes(cluster_db, vmware_attributes)
|
||||
return cluster
|
||||
|
||||
def create_node(
|
||||
|
@ -14,6 +14,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from mock import patch
|
||||
import six
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
@ -653,6 +654,49 @@ class TestVmwareAttributes(BaseIntegrationTest):
|
||||
resp.json_body["message"]
|
||||
)
|
||||
|
||||
@patch('nailgun.db.sqlalchemy.models.Cluster.is_locked', return_value=True)
|
||||
def test_vmware_attributes_update_for_locked_cluster_403(self, locked):
|
||||
self._set_use_vcenter(self.cluster_db)
|
||||
resp = self.app.put(
|
||||
reverse(
|
||||
'VmwareAttributesHandler',
|
||||
kwargs={'cluster_id': self.cluster_db.id}),
|
||||
params=jsonutils.dumps({
|
||||
"editable": {
|
||||
"value": {"foo": "bar"}
|
||||
}
|
||||
}),
|
||||
headers=self.default_headers,
|
||||
expect_errors=True
|
||||
)
|
||||
self.assertEqual(403, resp.status_code)
|
||||
self.assertEqual("Environment attributes can't be changed after or "
|
||||
"during deployment.", resp.json_body["message"])
|
||||
|
||||
@patch('objects.Cluster.has_compute_vmware_changes', return_value=True)
|
||||
@patch('nailgun.db.sqlalchemy.models.Cluster.is_locked', return_value=True)
|
||||
def test_vmware_attributes_update_for_locked_cluster_200(
|
||||
self, is_locked_mock, has_compute_mock):
|
||||
self._set_use_vcenter(self.cluster_db)
|
||||
params = {
|
||||
"editable": {
|
||||
"value": {"foo": "bar"}
|
||||
}}
|
||||
with patch('nailgun.api.v1.handlers.cluster.VmwareAttributesHandler.'
|
||||
'checked_data', return_value=params):
|
||||
resp = self.app.put(
|
||||
reverse(
|
||||
'VmwareAttributesHandler',
|
||||
kwargs={'cluster_id': self.cluster_db.id}),
|
||||
params=jsonutils.dumps(params),
|
||||
headers=self.default_headers
|
||||
)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
attrs = objects.Cluster.get_vmware_attributes(self.cluster_db)
|
||||
self.assertEqual('bar', attrs.editable.get('value', {}).get('foo'))
|
||||
attrs.editable.get('value', {}).pop('foo')
|
||||
self.assertEqual(attrs.editable.get('value'), {})
|
||||
|
||||
def _set_use_vcenter(self, cluster):
|
||||
cluster_attrs = objects.Cluster.get_editable_attributes(cluster)
|
||||
cluster_attrs['common']['use_vcenter']['value'] = True
|
||||
|
@ -1296,6 +1296,27 @@ class TestClusterObject(BaseTestCase):
|
||||
'cluster': {u'net_provider': u'test_provider'}}
|
||||
)
|
||||
|
||||
def test_cluster_has_compute_vmware_changes(self):
|
||||
cluster = self.env.create_cluster(api=False)
|
||||
ready_compute_vmware_node = self.env.create_node(
|
||||
cluster_id=cluster.id,
|
||||
roles=['compute-vmware'],
|
||||
status=consts.NODE_STATUSES.ready
|
||||
)
|
||||
self.env.create_node(cluster_id=cluster.id, pending_addition=True,
|
||||
pending_roles=['controller'])
|
||||
self.assertFalse(objects.Cluster.has_compute_vmware_changes(cluster))
|
||||
|
||||
pending_compute_vmware_node = self.env.create_node(
|
||||
cluster_id=cluster.id,
|
||||
pending_roles=["compute-vmware"]
|
||||
)
|
||||
self.assertTrue(objects.Cluster.has_compute_vmware_changes(cluster))
|
||||
objects.Node.delete(pending_compute_vmware_node)
|
||||
objects.Node.update(
|
||||
ready_compute_vmware_node, {'pending_deletion': True})
|
||||
self.assertTrue(objects.Cluster.has_compute_vmware_changes(cluster))
|
||||
|
||||
def test_enable_settings_by_components(self):
|
||||
components = [{
|
||||
'name': 'network:neutron:tun',
|
||||
|
@ -609,6 +609,84 @@ class TestCheckBeforeDeploymentTask(BaseTestCase):
|
||||
_check_deployment_graph_for_correctness(
|
||||
self.task)
|
||||
|
||||
def test_check_missed_nodes_vmware_nova_computes(self):
|
||||
operational_node = self.env.create_node(
|
||||
roles=['compute-vmware'],
|
||||
cluster_id=self.cluster.id,
|
||||
name='node-1'
|
||||
)
|
||||
pending_addition_node = self.env.create_node(
|
||||
roles=['compute-vmware'],
|
||||
cluster_id=self.cluster.id,
|
||||
pending_addition=True,
|
||||
name='node-2'
|
||||
)
|
||||
msg = ("The following compute-vmware nodes are not assigned to "
|
||||
"any vCenter cluster: {0}").format(', '.join(
|
||||
sorted([operational_node.name, pending_addition_node.name])
|
||||
))
|
||||
with self.assertRaisesRegexp(errors.CheckBeforeDeploymentError, msg):
|
||||
task.CheckBeforeDeploymentTask._check_vmware_consistency(self.task)
|
||||
|
||||
@mock.patch('objects.VmwareAttributes.get_nova_computes_target_nodes')
|
||||
def test_check_not_deleted_nodes_vmware_nova_computes(self, target_nodes):
|
||||
operational_node = self.env.create_node(
|
||||
roles=['compute-vmware'],
|
||||
cluster_id=self.cluster.id,
|
||||
name='node-1'
|
||||
)
|
||||
pending_deletion_node = self.env.create_node(
|
||||
roles=['compute-vmware'],
|
||||
cluster_id=self.cluster.id,
|
||||
pending_deletion=True,
|
||||
name='node-2'
|
||||
)
|
||||
target_nodes.return_value = [{
|
||||
'id': operational_node.hostname,
|
||||
'label': operational_node.name
|
||||
}, {
|
||||
'id': pending_deletion_node.hostname,
|
||||
'label': pending_deletion_node.name
|
||||
}]
|
||||
msg = ("The following nodes are prepared for deletion and couldn't be "
|
||||
"assigned to any vCenter cluster: {0}".format(
|
||||
pending_deletion_node.name))
|
||||
with self.assertRaisesRegexp(errors.CheckBeforeDeploymentError, msg):
|
||||
task.CheckBeforeDeploymentTask._check_vmware_consistency(self.task)
|
||||
|
||||
@mock.patch('objects.VmwareAttributes.get_nova_computes_target_nodes')
|
||||
def test_check_extra_nodes_vmware_nova_computes(self, target_nodes):
|
||||
operational_node = self.env.create_node(
|
||||
roles=['compute-vmware'],
|
||||
cluster_id=self.cluster.id,
|
||||
name='node-1'
|
||||
)
|
||||
non_cluster_node = self.env.create_node(
|
||||
roles=['compute-vmware'],
|
||||
name='node-2'
|
||||
)
|
||||
other_role_node = self.env.create_node(
|
||||
cluster_id=self.cluster.id,
|
||||
name='node-3'
|
||||
)
|
||||
target_nodes.return_value = [{
|
||||
'id': operational_node.hostname,
|
||||
'label': operational_node.name
|
||||
}, {
|
||||
'id': non_cluster_node.hostname,
|
||||
'label': non_cluster_node.name
|
||||
}, {
|
||||
'id': other_role_node.hostname,
|
||||
'label': other_role_node.name
|
||||
}]
|
||||
msg = ("The following nodes don't belong to compute-vmware nodes of "
|
||||
"environment and couldn't be assigned to any vSphere cluster: "
|
||||
"{0}".format(', '.join(
|
||||
sorted([non_cluster_node.name, other_role_node.name]))
|
||||
))
|
||||
with self.assertRaisesRegexp(errors.CheckBeforeDeploymentError, msg):
|
||||
task.CheckBeforeDeploymentTask._check_vmware_consistency(self.task)
|
||||
|
||||
|
||||
class TestDeployTask(BaseTestCase):
|
||||
|
||||
|
444
nailgun/nailgun/test/unit/test_vmware_attributes_validator.py
Normal file
444
nailgun/nailgun/test/unit/test_vmware_attributes_validator.py
Normal file
@ -0,0 +1,444 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015 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 copy import deepcopy
|
||||
from mock import patch
|
||||
|
||||
from nailgun.api.v1.validators.cluster import VmwareAttributesValidator
|
||||
from nailgun import consts
|
||||
from nailgun.errors import errors
|
||||
from nailgun import objects
|
||||
from nailgun.test.base import BaseTestCase
|
||||
|
||||
|
||||
class TestAttributesValidator(BaseTestCase):
|
||||
def setUp(self):
|
||||
super(TestAttributesValidator, self).setUp()
|
||||
self.env.create(
|
||||
cluster_kwargs={
|
||||
"api": False,
|
||||
"vmware_attributes": {
|
||||
"editable": self._get_value_vmware_attributes()
|
||||
}
|
||||
},
|
||||
nodes_kwargs=[{
|
||||
"hostname": "controller-node",
|
||||
"status": consts.NODE_STATUSES.ready
|
||||
}]
|
||||
)
|
||||
self.cluster = self.env.clusters[0]
|
||||
self.ready_compute_node = self.env.create_node(
|
||||
hostname="node-1",
|
||||
name="Node 1",
|
||||
roles=["compute-vmware"],
|
||||
status=consts.NODE_STATUSES.ready,
|
||||
cluster_id=self.cluster.id
|
||||
)
|
||||
|
||||
def _get_target_id(self, nova_compute):
|
||||
return nova_compute["target_node"]["current"]["id"]
|
||||
|
||||
def _get_default_nova_computes(self):
|
||||
return [
|
||||
{
|
||||
"datastore_regex": ".*",
|
||||
"vsphere_cluster": "Cluster1",
|
||||
"target_node": {
|
||||
"current": {
|
||||
"id": "node-1",
|
||||
"label": "node-1"
|
||||
}
|
||||
},
|
||||
"service_name": "ns1"
|
||||
},
|
||||
{
|
||||
"datastore_regex": ".*",
|
||||
"vsphere_cluster": "Cluster0",
|
||||
"target_node": {
|
||||
"current": {
|
||||
"id": "controllers",
|
||||
"label": "controllers"
|
||||
}
|
||||
},
|
||||
"service_name": "ns0"
|
||||
}
|
||||
]
|
||||
|
||||
def _get_value_vmware_attributes(self, nova_computes=None):
|
||||
return {
|
||||
"value": {
|
||||
"availability_zones": [{
|
||||
"vcenter_username": "admin",
|
||||
"az_name": "vcenter",
|
||||
"vcenter_password": "pass",
|
||||
"vcenter_host": "172.16.0.254",
|
||||
"nova_computes":
|
||||
nova_computes or self._get_default_nova_computes()
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
def validate_nova_compute_raises_regexp(self, nova_computes, error_msg):
|
||||
with self.assertRaisesRegexp(errors.InvalidData, error_msg):
|
||||
VmwareAttributesValidator._validate_nova_computes(
|
||||
{"editable": self._get_value_vmware_attributes(nova_computes)},
|
||||
self.cluster.vmware_attributes
|
||||
)
|
||||
|
||||
def test_change_exist_nova_compute(self):
|
||||
nova_computes = self._get_default_nova_computes()
|
||||
changed_attribute = 'vsphere_cluster'
|
||||
for nc in nova_computes:
|
||||
if self._get_target_id(nc) == self.ready_compute_node.hostname:
|
||||
nc[changed_attribute] = "ClusterXX"
|
||||
break
|
||||
|
||||
self.validate_nova_compute_raises_regexp(
|
||||
nova_computes,
|
||||
"Parameter '{0}' of nova compute instance with target node '{1}' "
|
||||
"couldn't be changed".format(
|
||||
changed_attribute, self.ready_compute_node.name
|
||||
)
|
||||
)
|
||||
|
||||
def test_delete_operational_nova_compute_node(self):
|
||||
nova_computes = [
|
||||
nc for nc in self._get_default_nova_computes() if
|
||||
self._get_target_id(nc) != self.ready_compute_node.hostname
|
||||
]
|
||||
self.validate_nova_compute_raises_regexp(
|
||||
nova_computes,
|
||||
"The following compute-vmware node couldn't be deleted from "
|
||||
"vSphere cluster: {0}".format(self.ready_compute_node.name)
|
||||
)
|
||||
|
||||
def test_duplicate_values_for_nova_computes(self):
|
||||
for attr in ("vsphere_cluster", "target_node", "service_name"):
|
||||
exist_nova_computes = self._get_default_nova_computes()
|
||||
duplicate_value = exist_nova_computes[0][attr]
|
||||
new_nova_compute = {
|
||||
"datastore_regex": ".*",
|
||||
"vsphere_cluster": "ClusterXX",
|
||||
"target_node": {
|
||||
"current": {
|
||||
"id": "node-X",
|
||||
"label": "node-X"
|
||||
},
|
||||
},
|
||||
"service_name": "nsXX"
|
||||
}
|
||||
new_nova_compute.update({attr: duplicate_value})
|
||||
exist_nova_computes.append(new_nova_compute)
|
||||
duplicate_value = duplicate_value if attr != "target_node" \
|
||||
else duplicate_value["current"]["id"]
|
||||
|
||||
self.validate_nova_compute_raises_regexp(
|
||||
exist_nova_computes,
|
||||
"Duplicate value '{0}' for attribute '{1}' is not allowed".
|
||||
format(duplicate_value, attr)
|
||||
)
|
||||
|
||||
def test_empty_values_for_nova_computes(self):
|
||||
nova_computes = self._get_default_nova_computes()
|
||||
nova_computes[0]["vsphere_cluster"] = ""
|
||||
self.validate_nova_compute_raises_regexp(
|
||||
nova_computes,
|
||||
"Empty value for attribute 'vsphere_cluster' is not allowed"
|
||||
)
|
||||
|
||||
def test_nova_compute_setting_validate_pass(self):
|
||||
new_compute_vmware_node = self.env.create_node(
|
||||
hostname="node-2",
|
||||
name="Node 2",
|
||||
pending_roles=["compute-vmware"],
|
||||
pending_addition=True,
|
||||
cluster_id=self.cluster.id
|
||||
)
|
||||
self.env.create_node(
|
||||
hostname="node-3",
|
||||
name="Node 3",
|
||||
roles=["compute-vmware"],
|
||||
pending_deletion=True,
|
||||
cluster_id=self.cluster.id
|
||||
)
|
||||
attributes = self.cluster.vmware_attributes
|
||||
new_nova_computes = self._get_default_nova_computes()
|
||||
new_nova_computes.extend([{
|
||||
"datastore_regex": ".*",
|
||||
"vsphere_cluster": "Cluster2",
|
||||
"target_node": {
|
||||
"current": {
|
||||
"id": new_compute_vmware_node.hostname,
|
||||
"label": new_compute_vmware_node.name
|
||||
}
|
||||
},
|
||||
"service_name": "ns2"
|
||||
}])
|
||||
self.assertNotRaises(errors.InvalidData,
|
||||
VmwareAttributesValidator._validate_nova_computes,
|
||||
{"editable": self._get_value_vmware_attributes(
|
||||
new_nova_computes)},
|
||||
attributes)
|
||||
|
||||
def test_change_controller_nova_computes_pass(self):
|
||||
cluster = self.env.create(
|
||||
cluster_kwargs={
|
||||
"api": False,
|
||||
"status": consts.CLUSTER_STATUSES.new,
|
||||
"vmware_attributes": {
|
||||
"editable": self._get_value_vmware_attributes()
|
||||
}
|
||||
}
|
||||
)
|
||||
new_nova_computes = [nc for nc in self._get_default_nova_computes()
|
||||
if self._get_target_id(nc) == "controllers"]
|
||||
new_nova_computes[0]["vsphere_cluster"] = "new vsphere name"
|
||||
new_nova_computes.append({
|
||||
"datastore_regex": ".*",
|
||||
"vsphere_cluster": "Cluster10",
|
||||
"target_node": {
|
||||
"current": {
|
||||
"id": "controllers",
|
||||
"label": "controllers"
|
||||
}
|
||||
},
|
||||
"service_name": "ns10"
|
||||
})
|
||||
self.assertNotRaises(errors.InvalidData,
|
||||
VmwareAttributesValidator._validate_nova_computes,
|
||||
{"editable": self._get_value_vmware_attributes(
|
||||
new_nova_computes)},
|
||||
cluster.vmware_attributes)
|
||||
|
||||
@patch("nailgun.db.sqlalchemy.models.Cluster.is_locked", return_value=True)
|
||||
def test_change_controllers_nova_compute_setting(self, lock_mock):
|
||||
new_nova_computes = self._get_default_nova_computes()
|
||||
changed_vsphere_cluster = None
|
||||
changed_attribute = "service_name"
|
||||
for nc in new_nova_computes:
|
||||
if self._get_target_id(nc) == "controllers":
|
||||
nc[changed_attribute] = "new_service_name"
|
||||
changed_vsphere_cluster = nc
|
||||
break
|
||||
|
||||
self.validate_nova_compute_raises_regexp(
|
||||
new_nova_computes,
|
||||
"Parameter '{0}' of nova compute instance with vSphere cluster "
|
||||
"name '{1}' couldn't be changed".format(
|
||||
changed_attribute, changed_vsphere_cluster["vsphere_cluster"])
|
||||
)
|
||||
|
||||
@patch("nailgun.db.sqlalchemy.models.Cluster.is_locked", return_value=True)
|
||||
def test_add_controllers_nova_compute_setting(self, lock_mock):
|
||||
new_nova_computes = self._get_default_nova_computes()
|
||||
new_nova_computes.extend([{
|
||||
"datastore_regex": ".*",
|
||||
"vsphere_cluster": "Cluster20",
|
||||
"target_node": {
|
||||
"current": {
|
||||
"id": "controllers",
|
||||
"label": "controllers"
|
||||
}
|
||||
},
|
||||
"service_name": "ns20"
|
||||
}, {
|
||||
"datastore_regex": ".*",
|
||||
"vsphere_cluster": "Cluster30",
|
||||
"target_node": {
|
||||
"current": {
|
||||
"id": "controllers",
|
||||
"label": "controllers"
|
||||
}
|
||||
},
|
||||
"service_name": "ns30"
|
||||
}])
|
||||
self.validate_nova_compute_raises_regexp(
|
||||
new_nova_computes,
|
||||
"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(["Cluster30", "Cluster20"]))
|
||||
)
|
||||
)
|
||||
|
||||
@patch("nailgun.db.sqlalchemy.models.Cluster.is_locked", return_value=True)
|
||||
def test_remove_controllers_nova_compute_setting(self, lock_mock):
|
||||
new_nova_computes = [nc for nc in self._get_default_nova_computes()
|
||||
if self._get_target_id(nc) != "controllers"]
|
||||
self.validate_nova_compute_raises_regexp(
|
||||
new_nova_computes,
|
||||
"Nova compute instance with target 'controllers' and vSphere "
|
||||
"cluster {0} couldn't be deleted from operational environment."
|
||||
.format("Cluster0")
|
||||
)
|
||||
|
||||
def test_update_non_editable_attributes(self):
|
||||
metadata = [
|
||||
{
|
||||
"name": "foo",
|
||||
"label": "foo",
|
||||
"type": "object",
|
||||
"fields": [{
|
||||
"name": "foo_field_name",
|
||||
"label": "foo_field_name",
|
||||
}]
|
||||
}, {
|
||||
"name": "availability_zones",
|
||||
"label": "availability_zones",
|
||||
"type": "array",
|
||||
"fields": [{
|
||||
"name": "az_name",
|
||||
"label": "az_name",
|
||||
}, {
|
||||
"name": "nova_computes",
|
||||
"type": "array",
|
||||
"fields": [{
|
||||
"name": "vsphere_cluster",
|
||||
"label": "vsphere_cluster",
|
||||
}, {
|
||||
"name": "target_node",
|
||||
"label": "target_node",
|
||||
}]
|
||||
}, {
|
||||
"name": "vcenter_host",
|
||||
"label": "vcenter_host",
|
||||
}]
|
||||
}
|
||||
]
|
||||
db_attributes_value = {
|
||||
"availability_zones": [{
|
||||
"az_name": "az_1",
|
||||
"vcenter_host": "127.0.0.1",
|
||||
"nova_computes": [{
|
||||
"vsphere_cluster": "Cluster1",
|
||||
"target_node": {
|
||||
"current": {"id": "node-1"}
|
||||
}
|
||||
}]
|
||||
}],
|
||||
"foo": {
|
||||
"foo_field_name": "foo_field_value"
|
||||
}
|
||||
}
|
||||
instance = objects.VmwareAttributes.create(
|
||||
{"editable": {"metadata": metadata, "value": db_attributes_value}}
|
||||
)
|
||||
|
||||
new_attributes = deepcopy(db_attributes_value)
|
||||
new_attributes["foo"] = ["foo_field_name"]
|
||||
msg = "Value type of 'foo_field_name' attribute couldn't be changed."
|
||||
with self.assertRaisesRegexp(errors.InvalidData, msg):
|
||||
VmwareAttributesValidator._validate_updated_attributes(
|
||||
{"editable": {"value": new_attributes}},
|
||||
instance)
|
||||
|
||||
new_attributes = deepcopy(db_attributes_value)
|
||||
new_attributes["foo"]["foo_field_name"] = "new_foo_field_value"
|
||||
msg = "Value of 'foo_field_name' attribute couldn't be changed."
|
||||
with self.assertRaisesRegexp(errors.InvalidData, msg):
|
||||
VmwareAttributesValidator._validate_updated_attributes(
|
||||
{"editable": {"value": new_attributes}},
|
||||
instance)
|
||||
|
||||
new_attributes = deepcopy(db_attributes_value)
|
||||
new_attributes["availability_zones"].append({
|
||||
"az_name": "az_2",
|
||||
"vcenter_host": "127.0.0.1",
|
||||
"nova_computes": []
|
||||
})
|
||||
msg = "Value of 'availability_zones' attribute couldn't be changed."
|
||||
with self.assertRaisesRegexp(errors.InvalidData, msg):
|
||||
VmwareAttributesValidator._validate_updated_attributes(
|
||||
{"editable": {"value": new_attributes}}, instance)
|
||||
|
||||
new_attributes = deepcopy(db_attributes_value)
|
||||
new_attributes["availability_zones"][0]["nova_computes"][0].update(
|
||||
{"target_node": {"current": {"id": "node-2"}}}
|
||||
)
|
||||
msg = "Value of 'target_node' attribute couldn't be changed."
|
||||
with self.assertRaisesRegexp(errors.InvalidData, msg):
|
||||
VmwareAttributesValidator._validate_updated_attributes(
|
||||
{"editable": {"value": new_attributes}}, instance)
|
||||
|
||||
def test_update_editable_attributes(self):
|
||||
metadata = [
|
||||
{
|
||||
"name": "foo",
|
||||
"label": "foo",
|
||||
"type": "object",
|
||||
"editable_for_deployed": True,
|
||||
"fields": [{
|
||||
"name": "foo_field_name",
|
||||
"label": "foo_field_name",
|
||||
}]
|
||||
}, {
|
||||
"name": "availability_zones",
|
||||
"type": "array",
|
||||
"label": "availability_zones",
|
||||
"fields": [{
|
||||
"name": "az_name",
|
||||
"label": "az_name",
|
||||
}, {
|
||||
"name": "nova_computes",
|
||||
"editable_for_deployed": True,
|
||||
"type": "array",
|
||||
"fields": [{
|
||||
"name": "vsphere_cluster",
|
||||
"label": "vsphere_cluster",
|
||||
}, {
|
||||
"name": "target_node",
|
||||
"label": "target_node",
|
||||
}]
|
||||
}, {
|
||||
"name": "vcenter_host",
|
||||
"label": "vcenter_host",
|
||||
}]
|
||||
}
|
||||
]
|
||||
db_attributes_value = {
|
||||
"availability_zones": [{
|
||||
"az_name": "az_1",
|
||||
"vcenter_host": "127.0.0.1",
|
||||
"nova_computes": [{
|
||||
"vsphere_cluster": "Cluster1",
|
||||
"target_node": {
|
||||
"current": {"id": "node-1"}
|
||||
}
|
||||
}]
|
||||
}],
|
||||
"foo": {
|
||||
"foo_field_name": "foo_field_value"
|
||||
}
|
||||
}
|
||||
instance = objects.VmwareAttributes.create(
|
||||
{"editable": {"metadata": metadata, "value": db_attributes_value}}
|
||||
)
|
||||
|
||||
new_attributes = deepcopy(db_attributes_value)
|
||||
new_attributes["foo"]["foo_field_name"] = 1
|
||||
new_attributes["availability_zones"][0]["nova_computes"][0].update(
|
||||
{"target_node": {"current": {"id": "node-2"}}}
|
||||
)
|
||||
new_attributes["availability_zones"][0]["nova_computes"].append({
|
||||
"vsphere_cluster": "Cluster2",
|
||||
"target_node": {
|
||||
"current": {"id": "node-2"}
|
||||
}
|
||||
})
|
||||
self.assertNotRaises(
|
||||
errors.InvalidData,
|
||||
VmwareAttributesValidator._validate_updated_attributes,
|
||||
{"editable": {"value": new_attributes}},
|
||||
instance)
|
Loading…
Reference in New Issue
Block a user