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:
parent
f47112b607
commit
e84f5ba4c7
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue