diff --git a/fuelclient/commands/release.py b/fuelclient/commands/release.py index 45557003..ea904d70 100644 --- a/fuelclient/commands/release.py +++ b/fuelclient/commands/release.py @@ -79,3 +79,56 @@ class ReleaseReposUpdate(ReleaseMixIn, base.BaseCommand): file=parsed_args.file ) ) + + +class ReleaseComponentList(ReleaseMixIn, base.BaseListCommand): + """Show list of components for a given release.""" + + columns = ("name", + "requires", + "compatible", + "incompatible", + "default") + + @staticmethod + def retrieve_predicates(statement): + """Retrieve predicates with respective 'items' components + + :param statement: the dictionary to extract predicate values from + :return: retrieval result as a string + """ + predicates = ('any_of', 'all_of', 'one_of', 'none_of') + for predicate in predicates: + if predicate in statement: + result = ', '.join(statement[predicate].get('items')) + return "{0} ({1})".format(predicate, result) + raise ValueError('Predicates not found.') + + @classmethod + def retrieve_data(cls, value): + """Retrieve names of components or predicates from nested data + + :param value: data to extract name or to retrieve predicates from + :return: names of components or predicates as a string + """ + if isinstance(value, list): + # get only "name" of components otherwise retrieve predicates + return ', '.join([v['name'] if 'name' in v + else cls.retrieve_predicates(v) + for v in value]) + return value + + def get_parser(self, prog_name): + parser = super(ReleaseComponentList, self).get_parser(prog_name) + parser.add_argument('id', type=int, + help='Id of the {0}.'.format(self.entity_name)) + return parser + + def take_action(self, parsed_args): + data = self.client.get_components_by_id(parsed_args.id) + # some keys (columns) can be missed in origin data + # then create them with respective '-' value + data = [{k: self.retrieve_data(d.get(k, '-')) for k in self.columns} + for d in data] + data = data_utils.get_display_data_multi(self.columns, data) + return self.columns, data diff --git a/fuelclient/objects/release.py b/fuelclient/objects/release.py index 0a9031b7..a4a91d30 100644 --- a/fuelclient/objects/release.py +++ b/fuelclient/objects/release.py @@ -22,6 +22,7 @@ class Release(BaseObject): networks_path = 'releases/{0}/networks' attributes_metadata_path = 'releases/{0}/attributes_metadata' deployment_tasks_path = 'releases/{0}/deployment_tasks' + components_path = 'releases/{0}/components' def get_networks(self): url = self.networks_path.format(self.id) @@ -46,3 +47,7 @@ class Release(BaseObject): def update_deployment_tasks(self, data): url = self.deployment_tasks_path.format(self.id) return self.connection.put_request(url, data) + + def get_components(self): + url = self.components_path.format(self.id) + return self.connection.get_request(url) diff --git a/fuelclient/tests/unit/common/test_release.py b/fuelclient/tests/unit/common/test_release.py new file mode 100644 index 00000000..d024a9e6 --- /dev/null +++ b/fuelclient/tests/unit/common/test_release.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 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 fuelclient.commands.release import ReleaseComponentList +from fuelclient.tests.unit.v1 import base + + +class TestReleaseComponent(base.UnitTestCase): + + def test_retrieve_predicates(self): + predicates = ('any_of', 'all_of', 'one_of', 'none_of') + items = { + "items": ["fake:component:1", + "fake:component:2"] + } + + for predicate in predicates: + test_data = {predicate: items} + real_data = ReleaseComponentList.retrieve_predicates(test_data) + expected_data = "{} (fake:component:1, fake:component:2)".format( + predicate) + self.assertEqual(expected_data, real_data) + + def test_retrieve_predicates_w_wrong_predicate(self): + test_data = { + "bad_predicate": { + "items": ["fake:component:1", + "fake:component:2"], + } + } + + self.assertRaisesRegexp(ValueError, + "Predicates not found.", + ReleaseComponentList.retrieve_predicates, + test_data) + + def test_retrieve_data(self): + test_data = "fake:component:1" + real_data = ReleaseComponentList.retrieve_data(test_data) + self.assertEqual("fake:component:1", real_data) + + test_data = [{"name": "fake:component:1"}] + real_data = ReleaseComponentList.retrieve_data(test_data) + self.assertEqual("fake:component:1", real_data) + + test_data = [ + { + "one_of": { + "items": ["fake:component:1"] + } + }, + { + "any_of": { + "items": ["fake:component:1", + "fake:component:2"] + } + }, + { + "all_of": { + "items": ["fake:component:1", + "fake:component:2"] + } + }, + { + "none_of": { + "items": ["fake:component:1"] + } + } + ] + real_data = ReleaseComponentList.retrieve_data(test_data) + expected_data = ("one_of (fake:component:1), " + "any_of (fake:component:1, fake:component:2), " + "all_of (fake:component:1, fake:component:2), " + "none_of (fake:component:1)") + self.assertEqual(expected_data, real_data) diff --git a/fuelclient/tests/unit/v2/cli/test_release.py b/fuelclient/tests/unit/v2/cli/test_release.py index 4796d48d..21d0cd9a 100644 --- a/fuelclient/tests/unit/v2/cli/test_release.py +++ b/fuelclient/tests/unit/v2/cli/test_release.py @@ -28,6 +28,8 @@ class TestReleaseCommand(test_engine.BaseCLITest): self.m_client.get_by_id.return_value = fake_release.get_fake_release() self.m_client.get_attributes_metadata_by_id.return_value = \ fake_release.get_fake_attributes_metadata() + self.m_client.get_components_by_id.return_value = \ + fake_release.get_fake_release_components(10) def test_release_list(self): args = 'release list' @@ -63,3 +65,10 @@ class TestReleaseCommand(test_engine.BaseCLITest): self.m_client.update_attributes_metadata_by_id \ .assert_called_once_with(1, data) self.m_get_client.assert_called_once_with('release', mock.ANY) + + def test_release_component_list(self): + release_id = 42 + args = 'release component list {0}'.format(release_id) + self.exec_command(args) + self.m_client.get_components_by_id.assert_called_once_with(release_id) + self.m_get_client.assert_called_once_with('release', mock.ANY) diff --git a/fuelclient/tests/unit/v2/lib/test_release.py b/fuelclient/tests/unit/v2/lib/test_release.py index f85c97f9..e6afa636 100644 --- a/fuelclient/tests/unit/v2/lib/test_release.py +++ b/fuelclient/tests/unit/v2/lib/test_release.py @@ -29,6 +29,7 @@ class TestReleaseFacade(test_api.BaseLibTest): self.version = 'v1' self.res_uri = '/api/{version}/releases/'.format(version=self.version) self.fake_releases = utils.get_fake_releases(10) + self.fake_release_components = utils.get_fake_release_components(10) self.fake_attributes_metadata = utils.get_fake_attributes_metadata() self.client = fuelclient.get_client('release', self.version) @@ -63,3 +64,12 @@ class TestReleaseFacade(test_api.BaseLibTest): self.assertTrue(m_put.called) self.assertEqual(m_put.last_request.json(), self.fake_attributes_metadata) + + def test_release_component_list(self): + release_id = 42 + expected_uri = self.get_object_uri(self.res_uri, release_id, + '/components') + matcher = self.m_request.get(expected_uri, + json=self.fake_release_components) + self.client.get_components_by_id(release_id) + self.assertTrue(matcher.called) diff --git a/fuelclient/tests/utils/__init__.py b/fuelclient/tests/utils/__init__.py index f9222231..bfa73987 100644 --- a/fuelclient/tests/utils/__init__.py +++ b/fuelclient/tests/utils/__init__.py @@ -41,6 +41,8 @@ from fuelclient.tests.utils.fake_plugin import get_fake_plugins from fuelclient.tests.utils.fake_release import get_fake_release from fuelclient.tests.utils.fake_release import get_fake_releases from fuelclient.tests.utils.fake_release import get_fake_attributes_metadata +from fuelclient.tests.utils.fake_release import get_fake_release_component +from fuelclient.tests.utils.fake_release import get_fake_release_components __all__ = (get_fake_deployment_history, @@ -52,6 +54,8 @@ __all__ = (get_fake_deployment_history, get_fake_release, get_fake_releases, get_fake_attributes_metadata, + get_fake_release_component, + get_fake_release_components, get_fake_fuel_version, get_fake_interface_config, get_fake_network_group, diff --git a/fuelclient/tests/utils/fake_release.py b/fuelclient/tests/utils/fake_release.py index d6f3fe1a..9a74649d 100644 --- a/fuelclient/tests/utils/fake_release.py +++ b/fuelclient/tests/utils/fake_release.py @@ -66,3 +66,64 @@ def get_fake_releases(release_count, **kwargs): """Create a random fake release list.""" return [get_fake_release(release_id=i, **kwargs) for i in range(1, release_count + 1)] + + +def get_fake_release_component(name=None, requires=None, incompatible=None, + compatible=None, default=None): + """Create a random fake component of release + + Returns the serialized and parametrized representation of a dumped Fuel + component of release. Represents the average amount of data. + + """ + return { + 'name': name or 'network:neutron:ml2:vlan', + 'description': + 'dialog.create_cluster_wizard.network.neutron_vlan_description', + 'weight': 5, + 'requires': requires or [ + { + 'one_of': { + 'items': ['hypervisor:qemu'], + 'message': 'dialog.create_cluster_wizard.compute.' + 'vcenter_warning' + } + }, + { + 'one_of': { + 'items': ['network:neutron:ml2:dvs', + 'network:neutron:ml2:nsx'], + 'message': 'dialog.create_cluster_wizard.compute.' + 'vcenter_requires_network_backend', + 'message_invalid': 'dialog.create_cluster_wizard.compute.' + 'vcenter_requires_network_plugins' + } + } + ], + 'incompatible': incompatible or [ + {'message': 'dialog.create_cluster_wizard.network.vlan_tun_alert', + 'name': 'network:neutron:ml2:tun'} + ], + 'compatible': compatible or [ + {'name': 'network:neutron:core:ml2'}, + {'name': 'hypervisor:qemu'}, + {'name': 'hypervisor:vmware'}, + {'name': 'storage:block:lvm'}, + {'name': 'storage:block:ceph'}, + {'name': 'storage:object:ceph'}, + {'name': 'storage:ephemeral:ceph'}, + {'name': 'storage:image:ceph'}, + {'name': 'additional_service:sahara'}, + {'name': 'additional_service:murano'}, + {'name': 'additional_service:ceilometer'}, + {'name': 'additional_service:ironic'} + ], + 'default': default, + 'label': 'common.network.neutron_vlan', + } + + +def get_fake_release_components(component_count, **kwargs): + """Create a random fake list of release components.""" + return [get_fake_release_component(**kwargs) + for _ in range(component_count)] diff --git a/fuelclient/v1/release.py b/fuelclient/v1/release.py index 31ccf4b7..5554c1e9 100644 --- a/fuelclient/v1/release.py +++ b/fuelclient/v1/release.py @@ -28,6 +28,10 @@ class ReleaseClient(base_v1.BaseV1Client): release_obj = self._entity_wrapper(obj_id=release_id) return release_obj.get_attributes_metadata() + def get_components_by_id(self, release_id): + release_obj = self._entity_wrapper(obj_id=release_id) + return release_obj.get_components() + def get_client(connection): return ReleaseClient(connection) diff --git a/setup.cfg b/setup.cfg index 0c524eaa..69a308c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -70,6 +70,7 @@ fuelclient = openstack-config_upload=fuelclient.commands.openstack_config:OpenstackConfigUpload plugins_list=fuelclient.commands.plugins:PluginsList plugins_sync=fuelclient.commands.plugins:PluginsSync + release_component_list=fuelclient.commands.release:ReleaseComponentList release_list=fuelclient.commands.release:ReleaseList release_repos_list=fuelclient.commands.release:ReleaseReposList release_repos_update=fuelclient.commands.release:ReleaseReposUpdate