From ea7c571162e781804985086fa5f9da63d326ccd9 Mon Sep 17 00:00:00 2001 From: Theodoros Tsioutsias Date: Tue, 26 Mar 2019 15:48:27 +0000 Subject: [PATCH] Add nodegroup list/show commands This adds the commands needed to list and show nodegroups in a cluster. Specifically the commands added are: * openstack coe nodegroup list * openstack coe nodegroup show Depends-On: I5607c27eb0e84677acda29af006335374b60dd27 Change-Id: I910c7c7caff34aba2ce08ca5c9362403d11f324c --- magnumclient/osc/v1/nodegroups.py | 114 +++++++++ magnumclient/tests/osc/unit/v1/fakes.py | 62 +++++ .../tests/osc/unit/v1/test_nodegroups.py | 158 ++++++++++++ magnumclient/tests/v1/test_nodegroups.py | 224 ++++++++++++++++++ magnumclient/v1/client.py | 2 + magnumclient/v1/nodegroups.py | 67 ++++++ setup.cfg | 3 + 7 files changed, 630 insertions(+) create mode 100644 magnumclient/osc/v1/nodegroups.py create mode 100644 magnumclient/tests/osc/unit/v1/test_nodegroups.py create mode 100644 magnumclient/tests/v1/test_nodegroups.py create mode 100644 magnumclient/v1/nodegroups.py diff --git a/magnumclient/osc/v1/nodegroups.py b/magnumclient/osc/v1/nodegroups.py new file mode 100644 index 00000000..58a3db6a --- /dev/null +++ b/magnumclient/osc/v1/nodegroups.py @@ -0,0 +1,114 @@ +# Copyright (c) 2018 European Organization for Nuclear Research. +# All Rights Reserved. +# +# 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 magnumclient.i18n import _ + +from osc_lib.command import command +from osc_lib import utils + + +NODEGROUP_ATTRIBUTES = [ + 'uuid', + 'name', + 'cluster_id', + 'project_id', + 'docker_volume_size', + 'labels', + 'flavor_id', + 'image_id', + 'node_addresses', + 'node_count', + 'role', + 'max_node_count', + 'min_node_count', + 'is_default' +] + + +class ListNodeGroup(command.Lister): + _description = _("List nodegroups") + + def get_parser(self, prog_name): + parser = super(ListNodeGroup, self).get_parser(prog_name) + + parser.add_argument( + 'cluster', + metavar='', + help=_('ID or name of the cluster where the nodegroup belongs.')) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Maximum number of nodegroups to return')) + parser.add_argument( + '--sort-key', + metavar='', + help=_('Column to sort results by')) + parser.add_argument( + '--sort-dir', + metavar='', + choices=['desc', 'asc'], + help=_('Direction to sort. "asc" or "desc".')) + parser.add_argument( + '--role', + metavar='', + help=_('List the nodegroups in the cluster with this role')) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + mag_client = self.app.client_manager.container_infra + columns = ['uuid', 'name', 'flavor_id', 'node_count', 'role'] + cluster_id = parsed_args.cluster + nodegroups = mag_client.nodegroups.list(cluster_id, + limit=parsed_args.limit, + sort_key=parsed_args.sort_key, + sort_dir=parsed_args.sort_dir, + role=parsed_args.role) + return ( + columns, + (utils.get_item_properties(n, columns) for n in nodegroups) + ) + + +class ShowNodeGroup(command.ShowOne): + _description = _("Show a nodegroup") + + def get_parser(self, prog_name): + parser = super(ShowNodeGroup, self).get_parser(prog_name) + parser.add_argument( + 'cluster', + metavar='', + help=_('ID or name of the cluster where the nodegroup belongs.')) + parser.add_argument( + 'nodegroup', + metavar='', + help=_('ID or name of the nodegroup to show.') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + columns = NODEGROUP_ATTRIBUTES + + mag_client = self.app.client_manager.container_infra + cluster_id = parsed_args.cluster + nodegroup = mag_client.nodegroups.get(cluster_id, + parsed_args.nodegroup) + + return (columns, utils.get_item_properties(nodegroup, columns)) diff --git a/magnumclient/tests/osc/unit/v1/fakes.py b/magnumclient/tests/osc/unit/v1/fakes.py index c0902825..662709c1 100644 --- a/magnumclient/tests/osc/unit/v1/fakes.py +++ b/magnumclient/tests/osc/unit/v1/fakes.py @@ -62,6 +62,24 @@ class FakeQuotasModelManager(object): pass +class FakeNodeGroupManager(object): + def list(self, cluster_id, limit=None, marker=None, sort_key=None, + sort_dir=None, detail=False): + pass + + def get(self, cluster_id, id): + pass + + def create(self, cluster_id, **kwargs): + pass + + def delete(self, cluster_id, id): + pass + + def update(self, cluster_id, id, patch): + pass + + class FakeCertificatesModelManager(FakeBaseModelManager): def get(self, cluster_uuid): pass @@ -75,6 +93,7 @@ class MagnumFakeContainerInfra(object): self.certificates = FakeCertificatesModelManager() self.stats = FakeStatsModelManager() self.quotas = FakeQuotasModelManager() + self.nodegroups = FakeNodeGroupManager() class MagnumFakeClientManager(osc_fakes.FakeClientManager): @@ -279,3 +298,46 @@ class FakeQuota(object): quota = osc_fakes.FakeResource(info=copy.deepcopy(quota_info), loaded=True) return quota + + +class FakeNodeGroup(object): + """Fake one or more NodeGroup.""" + + @staticmethod + def create_one_nodegroup(attrs=None): + """Create a fake NodeGroup. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with flavor_id, image_id, and so on + """ + + attrs = attrs or {} + + # set default attributes. + nodegroup_info = { + 'created_at': '2017-03-16T18:40:39+00:00', + 'updated_at': '2017-03-16T18:40:45+00:00', + 'uuid': '3a369884-b6ba-484f-a206-919b4b718aff', + 'cluster_id': 'fake-cluster', + 'docker_volume_size': None, + 'node_addresses': [], + 'labels': {}, + 'node_count': 1, + 'name': 'fake-nodegroup', + 'flavor_id': 'm1.medium', + 'image_id': 'fedora-latest', + 'project_id': None, + 'role': 'worker', + 'max_node_count': 10, + 'min_node_count': 1, + 'is_default': False + } + + # Overwrite default attributes. + nodegroup_info.update(attrs) + + nodegroup = osc_fakes.FakeResource(info=copy.deepcopy(nodegroup_info), + loaded=True) + return nodegroup diff --git a/magnumclient/tests/osc/unit/v1/test_nodegroups.py b/magnumclient/tests/osc/unit/v1/test_nodegroups.py new file mode 100644 index 00000000..649c6741 --- /dev/null +++ b/magnumclient/tests/osc/unit/v1/test_nodegroups.py @@ -0,0 +1,158 @@ +# Copyright (c) 2018 European Organization for Nuclear Research. +# All Rights Reserved. +# +# 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. + +import mock + +from magnumclient.osc.v1 import nodegroups as osc_nodegroups +from magnumclient.tests.osc.unit.v1 import fakes as magnum_fakes + + +class TestNodeGroup(magnum_fakes.TestMagnumClientOSCV1): + + def setUp(self): + super(TestNodeGroup, self).setUp() + self.ng_mock = self.app.client_manager.container_infra.nodegroups + + +class TestNodeGroupShow(TestNodeGroup): + + def setUp(self): + super(TestNodeGroupShow, self).setUp() + + self.nodegroup = magnum_fakes.FakeNodeGroup.create_one_nodegroup() + self.ng_mock.get = mock.Mock() + self.ng_mock.get.return_value = self.nodegroup + + self.data = tuple(map(lambda x: getattr(self.nodegroup, x), + osc_nodegroups.NODEGROUP_ATTRIBUTES)) + + # Get the command object to test + self.cmd = osc_nodegroups.ShowNodeGroup(self.app, None) + + def test_nodegroup_show_pass(self): + arglist = ['fake-cluster', 'fake-nodegroup'] + verifylist = [ + ('cluster', 'fake-cluster'), + ('nodegroup', 'fake-nodegroup') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.ng_mock.get.assert_called_with( + 'fake-cluster', 'fake-nodegroup') + self.assertEqual(osc_nodegroups.NODEGROUP_ATTRIBUTES, columns) + self.assertEqual(self.data, data) + + def test_nodegroup_show_no_nodegroup_fail(self): + arglist = ['fake-cluster'] + verifylist = [ + ('cluster', 'fake-cluster'), + ('nodegroup', '') + ] + + self.assertRaises(magnum_fakes.MagnumParseException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_nodegroup_show_no_args(self): + arglist = [] + verifylist = [ + ('cluster', ''), + ('nodegroup', '') + ] + + self.assertRaises(magnum_fakes.MagnumParseException, + self.check_parser, self.cmd, arglist, verifylist) + + +class TestNodeGroupList(TestNodeGroup): + + nodegroup = magnum_fakes.FakeNodeGroup.create_one_nodegroup() + + columns = ['uuid', 'name', 'flavor_id', 'node_count', 'role'] + + datalist = ( + ( + nodegroup.uuid, + nodegroup.name, + nodegroup.flavor_id, + nodegroup.node_count, + nodegroup.role, + ), + ) + + def setUp(self): + super(TestNodeGroupList, self).setUp() + self.ng_mock.list = mock.Mock() + self.ng_mock.list.return_value = [self.nodegroup] + + # Get the command object to test + self.cmd = osc_nodegroups.ListNodeGroup(self.app, None) + + def test_nodegroup_list_no_options(self): + arglist = [] + verifylist = [ + ('cluster', ''), + ('limit', None), + ('sort_key', None), + ('sort_dir', None), + ] + self.assertRaises(magnum_fakes.MagnumParseException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_nodegroup_list_ok(self): + arglist = ['fake-cluster'] + verifylist = [ + ('cluster', 'fake-cluster'), + ('limit', None), + ('sort_key', None), + ('sort_dir', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.ng_mock.list.assert_called_with( + 'fake-cluster', + limit=None, + sort_dir=None, + sort_key=None, + role=None, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_nodegroup_list_options(self): + arglist = [ + 'fake-cluster', + '--limit', '1', + '--sort-key', 'key', + '--sort-dir', 'asc' + ] + verifylist = [ + ('cluster', 'fake-cluster'), + ('limit', 1), + ('sort_key', 'key'), + ('sort_dir', 'asc') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.ng_mock.list.assert_called_with( + 'fake-cluster', + limit=1, + sort_dir='asc', + sort_key='key', + role=None + ) diff --git a/magnumclient/tests/v1/test_nodegroups.py b/magnumclient/tests/v1/test_nodegroups.py new file mode 100644 index 00000000..d4c613c4 --- /dev/null +++ b/magnumclient/tests/v1/test_nodegroups.py @@ -0,0 +1,224 @@ +# Copyright (c) 2018 European Organization for Nuclear Research. +# All Rights Reserved. +# +# 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. + +import testtools +from testtools import matchers + +from magnumclient.tests import utils +from magnumclient.v1 import nodegroups + + +NODEGROUP1 = { + 'id': 123, + 'uuid': '66666666-7777-8888-9999-000000000001', + 'cluster_id': '66666666-7777-8888-9999-000000000000', + 'name': 'test-worker', + 'node_addresses': ['172.17.2.3'], + 'node_count': 2, + 'project_id': 'fake_project', + 'labels': {}, + 'flavor_id': 'fake_flavor_1', + 'image_id': 'fake_image', + 'is_default': True, + 'role': 'worker', + 'max_node_count': 10, + 'min_node_count': 1 +} +NODEGROUP2 = { + 'id': 124, + 'uuid': '66666666-7777-8888-9999-000000000002', + 'cluster_id': '66666666-7777-8888-9999-000000000000', + 'name': 'test-master', + 'node_addresses': ['172.17.2.4'], + 'node_count': 2, + 'project_id': 'fake_project', + 'labels': {}, + 'flavor_id': 'fake_flavor_1', + 'image_id': 'fake_image', + 'is_default': True, + 'role': 'master', + 'max_node_count': 10, + 'min_node_count': 1 +} + + +fake_responses = { + '/v1/clusters/test/nodegroups/': + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP1, NODEGROUP2]}, + ), + }, + '/v1/clusters/test/nodegroups/%s' % NODEGROUP1['id']: + { + 'GET': ( + {}, + NODEGROUP1 + ), + }, + '/v1/clusters/test/nodegroups/%s' % NODEGROUP1['name']: + { + 'GET': ( + {}, + NODEGROUP1 + ), + }, + '/v1/clusters/test/nodegroups/?limit=2': + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP1, NODEGROUP2]}, + ), + }, + '/v1/clusters/test/nodegroups/?marker=%s' % NODEGROUP2['uuid']: + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP1, NODEGROUP2]}, + ), + }, + '/v1/clusters/test/nodegroups/?limit=2&marker=%s' % NODEGROUP2['uuid']: + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP1, NODEGROUP2]}, + ), + }, + '/v1/clusters/test/nodegroups/?sort_dir=asc': + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP1, NODEGROUP2]}, + ), + }, + '/v1/clusters/test/nodegroups/?sort_key=uuid': + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP1, NODEGROUP2]}, + ), + }, + '/v1/clusters/test/nodegroups/?sort_key=uuid&sort_dir=desc': + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP2, NODEGROUP1]}, + ), + }, +} + + +class NodeGroupManagerTest(testtools.TestCase): + + def setUp(self): + super(NodeGroupManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = nodegroups.NodeGroupManager(self.api) + self.cluster_id = 'test' + self.base_path = '/v1/clusters/test/nodegroups/' + + def test_nodegroup_list(self): + clusters = self.mgr.list(self.cluster_id) + expect = [ + ('GET', self.base_path, {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(clusters, matchers.HasLength(2)) + + def _test_nodegroup_list_with_filters(self, cluster_id, limit=None, + marker=None, sort_key=None, + sort_dir=None, detail=False, + expect=[]): + nodegroup_filter = self.mgr.list(cluster_id, + limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir, + detail=detail) + self.assertEqual(expect, self.api.calls) + self.assertThat(nodegroup_filter, matchers.HasLength(2)) + + def test_nodegroup_list_with_limit(self): + expect = [ + ('GET', self.base_path + '?limit=2', {}, None), + ] + self._test_nodegroup_list_with_filters( + self.cluster_id, + limit=2, + expect=expect) + + def test_nodegroup_list_with_marker(self): + filter_ = '?marker=%s' % NODEGROUP2['uuid'] + expect = [ + ('GET', self.base_path + filter_, {}, None), + ] + self._test_nodegroup_list_with_filters( + self.cluster_id, + marker=NODEGROUP2['uuid'], + expect=expect) + + def test_nodegroup_list_with_marker_limit(self): + filter_ = '?limit=2&marker=%s' % NODEGROUP2['uuid'] + expect = [ + ('GET', self.base_path + filter_, {}, None), + ] + self._test_nodegroup_list_with_filters( + self.cluster_id, + limit=2, marker=NODEGROUP2['uuid'], + expect=expect) + + def test_nodegroup_list_with_sort_dir(self): + expect = [ + ('GET', '/v1/clusters/test/nodegroups/?sort_dir=asc', {}, None), + ] + self._test_nodegroup_list_with_filters( + self.cluster_id, + sort_dir='asc', + expect=expect) + + def test_nodegroup_list_with_sort_key(self): + expect = [ + ('GET', '/v1/clusters/test/nodegroups/?sort_key=uuid', {}, None), + ] + self._test_nodegroup_list_with_filters( + self.cluster_id, + sort_key='uuid', + expect=expect) + + def test_nodegroup_list_with_sort_key_dir(self): + expect = [ + ('GET', self.base_path + '?sort_key=uuid&sort_dir=desc', {}, None), + ] + self._test_nodegroup_list_with_filters( + self.cluster_id, + sort_key='uuid', sort_dir='desc', + expect=expect) + + def test_nodegroup_show_by_name(self): + nodegroup = self.mgr.get(self.cluster_id, NODEGROUP1['name']) + expect = [ + ('GET', self.base_path + '%s' % NODEGROUP1['name'], {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NODEGROUP1['name'], nodegroup.name) + + def test_nodegroup_show_by_id(self): + nodegroup = self.mgr.get(self.cluster_id, NODEGROUP1['id']) + expect = [ + ('GET', self.base_path + '%s' % NODEGROUP1['id'], {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NODEGROUP1['name'], nodegroup.name) diff --git a/magnumclient/v1/client.py b/magnumclient/v1/client.py index 8a8c72ed..cb98cf95 100644 --- a/magnumclient/v1/client.py +++ b/magnumclient/v1/client.py @@ -25,6 +25,7 @@ from magnumclient.v1 import certificates from magnumclient.v1 import cluster_templates from magnumclient.v1 import clusters from magnumclient.v1 import mservices +from magnumclient.v1 import nodegroups from magnumclient.v1 import quotas from magnumclient.v1 import stats @@ -215,3 +216,4 @@ class Client(object): profiler.init(profile) self.stats = stats.StatsManager(self.http_client) self.quotas = quotas.QuotasManager(self.http_client) + self.nodegroups = nodegroups.NodeGroupManager(self.http_client) diff --git a/magnumclient/v1/nodegroups.py b/magnumclient/v1/nodegroups.py new file mode 100644 index 00000000..dbe7e003 --- /dev/null +++ b/magnumclient/v1/nodegroups.py @@ -0,0 +1,67 @@ +# Copyright (c) 2018 European Organization for Nuclear Research. +# All Rights Reserved. +# +# 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 magnumclient.common import utils +from magnumclient.v1 import baseunit + + +CREATION_ATTRIBUTES = ['docker_volume_size', 'labels', 'flavor_id', 'image_id', + 'project_id', 'node_count', 'name', 'role', + 'min_node_count', 'max_node_count'] + + +class NodeGroup(baseunit.BaseTemplate): + template_name = "NodeGroups" + + +class NodeGroupManager(baseunit.BaseTemplateManager): + resource_class = NodeGroup + template_name = 'nodegroups' + api_name = 'nodegroups' + + @classmethod + def _path(cls, cluster_id, id=None): + path = '/v1/clusters/%s/%s/' % (cluster_id, cls.template_name) + if id: + path += str(id) + return path + + def list(self, cluster_id, limit=None, marker=None, sort_key=None, + sort_dir=None, role=None, detail=False): + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir) + path = '' + if role: + filters.append('role=%s' % role) + if detail: + path += 'detail' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(cluster_id, id=path), + self.__class__.api_name) + else: + return self._list_pagination(self._path(cluster_id, id=path), + self.__class__.api_name, + limit=limit) + + def get(self, cluster_id, id): + try: + return self._list(self._path(cluster_id, id=id))[0] + except IndexError: + return None diff --git a/setup.cfg b/setup.cfg index 84ae627c..c0b07568 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,6 +56,9 @@ openstack.container_infra.v1 = coe_service_list = magnumclient.osc.v1.mservices:ListService + coe_nodegroup_list = magnumclient.osc.v1.nodegroups:ListNodeGroup + coe_nodegroup_show = magnumclient.osc.v1.nodegroups:ShowNodeGroup + [compile_catalog] directory = magnumclient/locale