Setup cluster setting by chosen components

Modifying cluster creation API (POST /api/clusters) for handling
components data and setup proper values of cluster attributes by
components from wizard. Change storage components metadata.

Implements: blueprint component-registry
Change-Id: Ic2b6774881aab47b5a0914518c5f5db9ab28f3b6
This commit is contained in:
ekosareva 2015-11-20 07:19:43 +04:00 committed by Andriy Popovych
parent f47112b607
commit e84f5ba4c7
7 changed files with 352 additions and 37 deletions

View File

@ -15,6 +15,7 @@
import copy
from distutils.version import StrictVersion
from itertools import groupby
import six
import sqlalchemy as sa
@ -95,6 +96,75 @@ class ClusterValidator(BasicValidator):
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)
components_set = set(components_list)
found_release_components = [
c for c in release_components if c['name'] in components_set]
found_release_components_names_set = set(
c['name'] for c in found_release_components)
if found_release_components_names_set != components_set:
raise errors.InvalidData(
u'{0} components are not related to release "{1}".'.format(
sorted(
components_set - found_release_components_names_set),
release.name
),
log_message=True
)
mandatory_component_types = set(['hypervisor', 'network', 'storage'])
for component in found_release_components:
component_name = component['name']
for incompatible in component.get('incompatible', []):
incompatible_component_names = list(
cls._resolve_names_for_dependency(
components_set, incompatible))
if incompatible_component_names:
raise errors.InvalidData(
u"Incompatible components were found: "
u"'{0}' incompatible with {1}.".format(
component_name, incompatible_component_names),
log_message=True
)
component_type = lambda x: x['name'].split(':', 1)[0]
for c_type, group in groupby(
sorted(component.get('requires', []), key=component_type),
component_type):
group_components = list(group)
for require in group_components:
component_exist = any(
cls._resolve_names_for_dependency(
components_set, require))
if component_exist:
break
else:
raise errors.InvalidData(
u"Requires {0} for '{1}' components were not "
u"satisfied.".format(
[c['name'] for c in group_components],
component_name),
log_message=True
)
if component_type(component) in mandatory_component_types:
mandatory_component_types.remove(component_type(component))
if mandatory_component_types:
raise errors.InvalidData(
"Components with {0} types required but wasn't found in data"
.format(sorted(mandatory_component_types)),
log_message=True
)
@staticmethod
def _resolve_names_for_dependency(components_set, dependency):
prefix = dependency['name'].split('*', 1)[0]
return (name for name in components_set if name.startswith(prefix))
@classmethod
def validate(cls, data):
d = cls._validate_common(data)
@ -114,6 +184,9 @@ class ClusterValidator(BasicValidator):
log_message=True
)
if "components" in d:
cls._validate_components(release_id, d['components'])
return d
@classmethod

View File

@ -1658,9 +1658,10 @@
compatible:
- name: hypervisor:libvirt:kvm
- name: hypervisor:libvirt:qemu
incompatible:
- name: network:neutron:*
message: "dialog.create_cluster_wizard.network.hypervisor_alert"
- name: network:neutron:core:ml2
weight: 1000
label: "dialog.create_cluster_wizard.network.neutron_ml2"
description: "dialog.create_cluster_wizard.network.neutron_ml2_description"
- name: network:neutron:vlan
default: true
bind: !!pairs
@ -1704,17 +1705,14 @@
incompatible:
- name: additional_service:ironic
message: "dialog.create_cluster_wizard.additional.ironic_network_mode_alert"
- name: storage:default
- name: storage:block:lvm
label: "dialog.create_cluster_wizard.storage.lvm"
description: "dialog.create_cluster_wizard.storage.default_provider"
default: true
bind: !!pairs
- "settings:storage.volumes_lvm.value": true
- "settings:storage.volumes_ceph.value": false
- "settings:storage.objects_ceph.value": false
- "settings:storage.ephemeral_ceph.value": false
- "settings:storage.images_ceph.value": false
weight: 10
label: "dialog.create_cluster_wizard.storage.default_provider"
compatible:
weight: 5
bind:
- "settings:storage.volumes_lvm.value"
compatible: &default_storage_compatibility
- name: hypervisor:libvirt:kvm
- name: hypervisor:libvirt:qemu
- name: hypervisor:vmware
@ -1722,28 +1720,39 @@
- name: network:neutron:tun
- name: network:nova_network
incompatible:
- name: storage:ceph
message: ""
- name: storage:ceph
bind: !!pairs
- "settings:storage.volumes_ceph.value": true
- "settings:storage.objects_ceph.value": true
- "settings:storage.ephemeral_ceph.value": true
- "settings:storage.images_ceph.value": true
- "settings:storage.volumes_lvm.value": false
weight: 20
label: "dialog.create_cluster_wizard.storage.ceph_provider"
- name: storage:block:ceph
message: "LVM not compatible with Ceph"
- name: storage:block:ceph
label: "dialog.create_cluster_wizard.storage.ceph"
description: "dialog.create_cluster_wizard.storage.ceph_help"
compatible:
- name: hypervisor:libvirt:kvm
- name: hypervisor:libvirt:qemu
- name: hypervisor:vmware
- name: network:neutron:vlan
- name: network:neutron:tun
- name: network:nova_network
weight: 10
bind:
- "settings:storage.volumes_ceph.value"
compatible: *default_storage_compatibility
incompatible:
- name: storage:default
message: ""
- name: storage:block:lvm
message: "Ceph not compatible with LVM"
- name: storage:object:ceph
label: "dialog.create_cluster_wizard.storage.ceph"
description: "dialog.create_cluster_wizard.storage.ceph_help"
weight: 10
bind:
- "settings:storage.objects_ceph.value"
compatible: *default_storage_compatibility
- name: storage:ephemeral:ceph
label: "dialog.create_cluster_wizard.storage.ceph"
description: "dialog.create_cluster_wizard.storage.ceph_help"
weight: 10
bind:
- "settings:storage.ephemeral_ceph.value"
compatible: *default_storage_compatibility
- name: storage:image:ceph
label: "dialog.create_cluster_wizard.storage.ceph"
description: "dialog.create_cluster_wizard.storage.ceph_help"
weight: 10
bind:
- "settings:storage.images_ceph.value"
compatible: *default_storage_compatibility
- name: additional_service:sahara
bind:
- "settings:additional_components.sahara.value"

View File

@ -152,12 +152,19 @@ class Cluster(NailgunObject):
# remove read-only attribute
data.pop("is_locked", None)
assign_nodes = data.pop("nodes", [])
enabled_editable_attributes = None
if 'components' in data:
enabled_core_attributes = cls.get_cluster_attributes_by_components(
data['components'], data["release_id"])
data = dict_merge(data, enabled_core_attributes['cluster'])
enabled_editable_attributes = enabled_core_attributes['editable']
data["fuel_version"] = settings.VERSION["release"]
cluster = super(Cluster, cls).create(data)
cls.create_default_group(cluster)
cls.create_attributes(cluster)
cls.create_attributes(cluster, enabled_editable_attributes)
cls.create_vmware_attributes(cluster)
cls.create_default_extensions(cluster)
@ -187,6 +194,54 @@ class Cluster(NailgunObject):
return cluster
@classmethod
def get_cluster_attributes_by_components(cls, components, release_id):
"""Enable cluster attributes by given components
:param components: list of component names
:type components: list of strings
:param release_id: Release model id
:type release_id: str
:returns: dict -- objects with enabled attributes for cluster
"""
def _update_attributes_dict_by_binds_exp(bind_exp, value):
"""Update cluster and attributes data with bound values
:param bind_exp: path to specific attribute for model in format
model:some.attribute.value. Model can be
settings|cluster
:type bind_exp: str
:param value: value for specific attribute
:type value: bool|str|int
:returns: None
"""
model, attr_expr = bind_exp.split(':')
if model not in ('settings', 'cluster'):
return
path_items = attr_expr.split('.')
path_items.insert(0, model)
attributes = cluster_attributes
for i in six.moves.range(0, len(path_items) - 1):
attributes = attributes.setdefault(path_items[i], {})
attributes[path_items[-1]] = value
release = Release.get_by_uid(release_id)
cluster_attributes = {}
for component in release.components_metadata:
if component['name'] in components:
for bind_item in component.get('bind', []):
if isinstance(bind_item, six.string_types):
_update_attributes_dict_by_binds_exp(bind_item, True)
elif isinstance(bind_item, list):
_update_attributes_dict_by_binds_exp(bind_item[0],
bind_item[1])
return {
'editable': cluster_attributes.get('settings', {}),
'cluster': cluster_attributes.get('cluster', {})
}
@classmethod
def delete(cls, instance):
node_ids = [
@ -204,17 +259,24 @@ class Cluster(NailgunObject):
return kernel_params.get("kernel", {}).get("value")
@classmethod
def create_attributes(cls, instance):
def create_attributes(cls, instance, editable_attributes=None):
"""Create attributes for Cluster instance, generate their values
(see :func:`Attributes.generate_fields`)
:param instance: Cluster instance
:param editable_attributes: key-value dictionary represents editable
attributes that will be merged with default editable attributes
:returns: None
"""
merged_editable_attributes = \
cls.get_default_editable_attributes(instance)
if editable_attributes:
merged_editable_attributes = dict_merge(
merged_editable_attributes, editable_attributes)
attributes = Attributes.create(
{
"editable": cls.get_default_editable_attributes(instance),
"editable": merged_editable_attributes,
"generated": instance.release.attributes_metadata.get(
"generated"
),

View File

@ -50,6 +50,8 @@ class ComponentSerializer(BasicSerializer):
@classmethod
def serialize(cls, instance):
# binds use for mapping components on cluster_attributes options,
# it's only back-end logic and no need send it to client
instance.pop('bind', None)
return instance

View File

@ -301,3 +301,86 @@ class TestClusterModes(BaseIntegrationTest):
headers=self.default_headers,
expect_errors=True
)
class TestClusterComponents(BaseIntegrationTest):
def setUp(self):
super(TestClusterComponents, self).setUp()
self.release = self.env.create_release(
version='2015.1-8.0',
operating_system='Ubuntu',
modes=[consts.CLUSTER_MODES.ha_compact],
components_metadata=[
{
'name': 'hypervisor:test_hypervisor'
},
{
'name': 'network:core:test_network_1',
'incompatible': [
{'name': 'hypervisor:test_hypervisor'}
]
},
{
'name': 'network:core:test_network_2'
},
{
'name': 'storage:test_storage',
'compatible': [
{'name': 'hypervisors:test_hypervisor'}
],
'requires': [
{'name': 'hypervisors:test_hypervisor'}
]
}
])
self.cluster_data = {
'name': 'TestCluster',
'release_id': self.release.id,
'mode': consts.CLUSTER_MODES.ha_compact
}
def test_components_not_in_release(self):
self.cluster_data.update(
{'components': ['storage:not_existing_component']})
resp = self._create_cluster_with_expected_errors(self.cluster_data)
self.assertEqual(resp.status_code, 400)
self.assertEqual(
u"[u'storage:not_existing_component'] components are not "
"related to release \"release_name_2015.1-8.0\".",
resp.json_body['message']
)
def test_incompatible_components_found(self):
self.cluster_data.update(
{'components': [
'hypervisor:test_hypervisor',
'network:core:test_network_1']})
resp = self._create_cluster_with_expected_errors(self.cluster_data)
self.assertEqual(resp.status_code, 400)
self.assertEqual(
u"Incompatible components were found: "
"'network:core:test_network_1' incompatible with "
"[u'hypervisor:test_hypervisor'].",
resp.json_body['message']
)
def test_requires_components_not_found(self):
self.cluster_data.update(
{'components': ['storage:test_storage']})
resp = self._create_cluster_with_expected_errors(self.cluster_data)
self.assertEqual(resp.status_code, 400)
self.assertEqual(
u"Requires [u'hypervisors:test_hypervisor'] for "
"'storage:test_storage' components were not satisfied.",
resp.json_body['message']
)
def _create_cluster_with_expected_errors(self, cluster_data):
return self.app.post(
reverse('ClusterCollectionHandler'),
jsonutils.dumps(cluster_data),
headers=self.default_headers,
expect_errors=True
)

View File

@ -1270,6 +1270,90 @@ class TestClusterObject(BaseTestCase):
self.assertTrue(objects.Cluster.is_component_enabled(cluster,
'ironic'))
def test_get_cluster_attributes_by_components(self):
release = self.env.create_release(
components_metadata=[{
'name': 'hypervisor:libvirt:test',
'bind': [['settings:common.libvirt_type.value', 'test'],
['wrong_model:field.value', 'smth']]
}, {
'name': 'additional_service:new',
'bind': ['settings:additional_components.new.value']
}, {
'name': 'network:some_net',
'bind': [['cluster:net_provider', 'test_provider'],
'settings:some_net.checkbox']
}]
)
selected_components = ['network:some_net', 'hypervisor:libvirt:test',
'additional_service:new_plugin_service']
result_attrs = objects.Cluster.get_cluster_attributes_by_components(
selected_components, release.id)
self.assertDictEqual(
result_attrs,
{'editable': {u'some_net': {u'checkbox': True},
u'common': {u'libvirt_type': {u'value': u'test'}}},
'cluster': {u'net_provider': u'test_provider'}}
)
def test_enable_settings_by_components(self):
components = [{
'name': 'network:neutron:tun',
'bind': [['cluster:net_provider', 'neutron'],
['cluster:net_segment_type', 'tun']]
}, {
'name': 'network:nova_network',
'bind': [['cluster:net_provider', 'nova_network']]
}, {
'name': 'hypervisor:libvirt:kvm',
'bind': [['settings:common.libvirt_type.value', 'kvm']]
}, {
'name': 'additional_service:sahara',
'bind': ['settings:additional_components.sahara.value']
}]
default_editable_attributes = {
'common': {'libvirt_type': {'value': 'qemu'}},
'additional_components': {'sahara': {'value': False}}
}
release = self.env.create_release(components_metadata=components)
tests_data = [{
'selected_components': ['network:neutron:tun',
'hypervisor:libvirt:kvm',
'additional_service:sahara'],
'expected_values': {
'net_provider': consts.CLUSTER_NET_PROVIDERS.neutron,
'segmentation_type': consts.NEUTRON_SEGMENT_TYPES.tun
}
}, {
'selected_components': ['network:nova_network',
'hypervisor:libvirt:kvm',
'additional_service:sahara'],
'expected_values': {
'net_provider': consts.CLUSTER_NET_PROVIDERS.nova_network,
'segmentation_type': None
}
}]
for i, test_data in enumerate(tests_data):
with mock.patch('objects.Cluster.get_default_editable_attributes',
return_value=default_editable_attributes):
cluster = objects.Cluster.create({
'name': 'test-{0}'.format(i),
'release_id': release.id,
'components': test_data.get('selected_components', [])
})
editable_attrs = cluster.attributes.editable
expected_values = test_data['expected_values']
self.assertEqual(cluster.net_provider,
expected_values['net_provider'])
if expected_values['segmentation_type']:
self.assertEqual(cluster.network_config.segmentation_type,
expected_values['segmentation_type'])
self.assertEqual(
editable_attrs[u'common'][u'libvirt_type'][u'value'], u'kvm')
self.assertTrue(
editable_attrs[u'additional_components'][u'sahara'][u'value'])
class TestClusterObjectVirtRoles(BaseTestCase):

View File

@ -849,7 +849,9 @@
"neutron_gre_description": "Your network hardware must support GRE segmentation. This option supports up to 65535 networks.",
"neutron_vlan": "Neutron with VLAN segmentation (default)",
"neutron_vlan_description": "Your network hardware must be configured for VLAN segmentation. This option supports up to 4095 networks.",
"neutron_tun_description": "By default VXLAN tunnels will be used (can be changed to GRE using the Fuel CLI). Your network hardware must support VXLAN segmentation. This option supports millions of tenant data networks."
"neutron_tun_description": "By default VXLAN tunnels will be used (can be changed to GRE using the Fuel CLI). Your network hardware must support VXLAN segmentation. This option supports millions of tenant data networks.",
"neutron_ml2": "ML2 plugin",
"neutron_ml2_description": "Core ML2 plugin"
},
"storage": {
"title": "Storage Backends",