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:
ekosareva 2015-11-30 13:10:52 +03:00 committed by Elena Kosareva
parent 218e8a2ab3
commit 3450d676de
10 changed files with 946 additions and 8 deletions

View File

@ -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.")

View File

@ -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

View File

@ -1529,6 +1529,7 @@
-
name: "nova_computes"
type: "array"
editable_for_deployed: true
fields:
-
name: "vsphere_cluster"

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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',

View File

@ -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):

View 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)