diff --git a/magnumclient/osc/v1/nodegroups.py b/magnumclient/osc/v1/nodegroups.py index 58a3db6a..04967bf3 100644 --- a/magnumclient/osc/v1/nodegroups.py +++ b/magnumclient/osc/v1/nodegroups.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from magnumclient.common import utils as magnum_utils from magnumclient.i18n import _ from osc_lib.command import command @@ -33,10 +34,133 @@ NODEGROUP_ATTRIBUTES = [ 'role', 'max_node_count', 'min_node_count', - 'is_default' + 'is_default', + 'stack_id', + 'status', + 'status_reason' ] +class CreateNodeGroup(command.Command): + _description = _("Create a nodegroup") + + def get_parser(self, prog_name): + parser = super(CreateNodeGroup, self).get_parser(prog_name) + # NOTE: All arguments are positional and, if not provided + # with a default, required. + parser.add_argument('--docker-volume-size', + dest='docker_volume_size', + type=int, + metavar='', + help=('The size in GB for the docker volume to ' + 'use.')) + parser.add_argument('--labels', + metavar='', + action='append', + help=_('Arbitrary labels in the form of key=value' + 'pairs to associate with a nodegroup. ' + 'May be used multiple times.')) + parser.add_argument('cluster', + metavar='', + help='Name of the nodegroup to create.') + parser.add_argument('name', + metavar='', + help='Name of the nodegroup to create.') + parser.add_argument('--node-count', + dest='node_count', + type=int, + default=1, + metavar='', + help='The nodegroup node count.') + parser.add_argument('--min-nodes', + dest='min_node_count', + type=int, + default=1, + metavar='', + help='The nodegroup minimum node count.') + parser.add_argument('--max-nodes', + dest='max_node_count', + type=int, + default=None, + metavar='', + help='The nodegroup maximum node count.') + parser.add_argument('--role', + dest='role', + type=str, + default='worker', + metavar='', + help=('The role of the nodegroup')) + parser.add_argument( + '--image', + metavar='', + help=_('The name or UUID of the base image to customize for the ' + 'NodeGroup.')) + parser.add_argument( + '--flavor', + metavar='', + help=_('The nova flavor name or UUID to use when launching the ' + 'nodes in this NodeGroup.')) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + mag_client = self.app.client_manager.container_infra + args = { + 'name': parsed_args.name, + 'node_count': parsed_args.node_count, + 'max_node_count': parsed_args.max_node_count, + 'min_node_count': parsed_args.min_node_count, + 'role': parsed_args.role, + } + + if parsed_args.labels is not None: + args['labels'] = magnum_utils.handle_labels(parsed_args.labels) + + if parsed_args.docker_volume_size is not None: + args['docker_volume_size'] = parsed_args.docker_volume_size + + if parsed_args.flavor is not None: + args['flavor_id'] = parsed_args.flavor + + if parsed_args.image is not None: + args['image_id'] = parsed_args.image + + cluster_id = parsed_args.cluster + nodegroup = mag_client.nodegroups.create(cluster_id, **args) + print("Request to create nodegroup %s accepted" + % nodegroup.uuid) + + +class DeleteNodeGroup(command.Command): + _description = _("Delete a nodegroup") + + def get_parser(self, prog_name): + parser = super(DeleteNodeGroup, self).get_parser(prog_name) + parser.add_argument( + 'cluster', + metavar='', + help=_('ID or name of the cluster where the nodegroup(s) ' + 'belong(s).')) + parser.add_argument( + 'nodegroup', + nargs='+', + metavar='', + help='ID or name of the nodegroup(s) to delete.') + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + mag_client = self.app.client_manager.container_infra + cluster_id = parsed_args.cluster + for ng in parsed_args.nodegroup: + mag_client.nodegroups.delete(cluster_id, ng) + print("Request to delete nodegroup %s has been accepted." % ng) + + class ListNodeGroup(command.Lister): _description = _("List nodegroups") @@ -72,7 +196,8 @@ class ListNodeGroup(command.Lister): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra - columns = ['uuid', 'name', 'flavor_id', 'node_count', 'role'] + columns = ['uuid', 'name', 'flavor_id', 'image_id', 'node_count', + 'status', 'role'] cluster_id = parsed_args.cluster nodegroups = mag_client.nodegroups.list(cluster_id, limit=parsed_args.limit, @@ -112,3 +237,50 @@ class ShowNodeGroup(command.ShowOne): parsed_args.nodegroup) return (columns, utils.get_item_properties(nodegroup, columns)) + + +class UpdateNodeGroup(command.Command): + _description = _("Update a Nodegroup") + + def get_parser(self, prog_name): + parser = super(UpdateNodeGroup, 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=_('The name or UUID of cluster to update')) + + parser.add_argument( + 'op', + metavar='', + choices=['add', 'replace', 'remove'], + help=_("Operations: one of 'add', 'replace' or 'remove'")) + + parser.add_argument( + 'attributes', + metavar='', + nargs='+', + action='append', + default=[], + help=_( + "Attributes to add/replace or remove (only PATH is necessary " + "on remove)")) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + mag_client = self.app.client_manager.container_infra + + patch = magnum_utils.args_array_to_patch(parsed_args.op, + parsed_args.attributes[0]) + + cluster_id = parsed_args.cluster + mag_client.nodegroups.update(cluster_id, parsed_args.nodegroup, + patch) + print("Request to update nodegroup %s has been accepted." % + parsed_args.nodegroup) diff --git a/magnumclient/tests/osc/unit/v1/fakes.py b/magnumclient/tests/osc/unit/v1/fakes.py index 662709c1..2a25df5f 100644 --- a/magnumclient/tests/osc/unit/v1/fakes.py +++ b/magnumclient/tests/osc/unit/v1/fakes.py @@ -332,7 +332,10 @@ class FakeNodeGroup(object): 'role': 'worker', 'max_node_count': 10, 'min_node_count': 1, - 'is_default': False + 'is_default': False, + 'stack_id': '3a369884-b6ba-484f-fake-stackb718aff', + 'status': 'CREATE_COMPLETE', + 'status_reason': 'None' } # Overwrite default attributes. diff --git a/magnumclient/tests/osc/unit/v1/test_nodegroups.py b/magnumclient/tests/osc/unit/v1/test_nodegroups.py index 649c6741..06f1b555 100644 --- a/magnumclient/tests/osc/unit/v1/test_nodegroups.py +++ b/magnumclient/tests/osc/unit/v1/test_nodegroups.py @@ -13,7 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import mock +from mock import call from magnumclient.osc.v1 import nodegroups as osc_nodegroups from magnumclient.tests.osc.unit.v1 import fakes as magnum_fakes @@ -26,6 +28,176 @@ class TestNodeGroup(magnum_fakes.TestMagnumClientOSCV1): self.ng_mock = self.app.client_manager.container_infra.nodegroups +class TestNodeGroupCreate(TestNodeGroup): + + def setUp(self): + super(TestNodeGroupCreate, self).setUp() + self.nodegroup = magnum_fakes.FakeNodeGroup.create_one_nodegroup() + + self.ng_mock.create = mock.Mock() + self.ng_mock.create.return_value = self.nodegroup + + self.ng_mock.get = mock.Mock() + self.ng_mock.get.return_value = copy.deepcopy(self.nodegroup) + + self.ng_mock.update = mock.Mock() + self.ng_mock.update.return_value = self.nodegroup + + self._default_args = { + 'name': 'fake-nodegroup', + 'node_count': 1, + 'role': 'worker', + 'min_node_count': 1, + 'max_node_count': None, + } + + # Get the command object to test + self.cmd = osc_nodegroups.CreateNodeGroup(self.app, None) + + self.data = tuple(map(lambda x: getattr(self.nodegroup, x), + osc_nodegroups.NODEGROUP_ATTRIBUTES)) + + def test_nodegroup_create_required_args_pass(self): + """Verifies required arguments.""" + + arglist = [ + self.nodegroup.cluster_id, + self.nodegroup.name + ] + verifylist = [ + ('cluster', self.nodegroup.cluster_id), + ('name', self.nodegroup.name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.ng_mock.create.assert_called_with(self.nodegroup.cluster_id, + **self._default_args) + + def test_nodegroup_create_missing_required_arg(self): + """Verifies missing required arguments.""" + + arglist = [ + self.nodegroup.name + ] + verifylist = [ + ('name', self.nodegroup.name) + ] + self.assertRaises(magnum_fakes.MagnumParseException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_nodegroup_create_with_labels(self): + """Verifies labels are properly parsed when given as argument.""" + + expected_args = self._default_args + expected_args['labels'] = { + 'arg1': 'value1', 'arg2': 'value2' + } + + arglist = [ + '--labels', 'arg1=value1', + '--labels', 'arg2=value2', + self.nodegroup.cluster_id, + self.nodegroup.name + ] + verifylist = [ + ('labels', ['arg1=value1', 'arg2=value2']), + ('name', self.nodegroup.name), + ('cluster', self.nodegroup.cluster_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.ng_mock.create.assert_called_with(self.nodegroup.cluster_id, + **expected_args) + + +class TestNodeGroupDelete(TestNodeGroup): + + def setUp(self): + super(TestNodeGroupDelete, self).setUp() + + self.ng_mock.delete = mock.Mock() + self.ng_mock.delete.return_value = None + + # Get the command object to test + self.cmd = osc_nodegroups.DeleteNodeGroup(self.app, None) + + def test_nodegroup_delete_one(self): + arglist = ['foo', 'fake-nodegroup'] + verifylist = [ + ('cluster', 'foo'), + ('nodegroup', ['fake-nodegroup']) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.ng_mock.delete.assert_called_with('foo', 'fake-nodegroup') + + def test_nodegroup_delete_multiple(self): + arglist = ['foo', 'fake-nodegroup1', 'fake-nodegroup2'] + verifylist = [ + ('cluster', 'foo'), + ('nodegroup', ['fake-nodegroup1', 'fake-nodegroup2']) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.ng_mock.delete.assert_has_calls( + [call('foo', 'fake-nodegroup1'), call('foo', 'fake-nodegroup2')] + ) + + def test_nodegroup_delete_no_args(self): + arglist = [] + verifylist = [ + ('cluster', ''), + ('nodegroup', []) + ] + + self.assertRaises(magnum_fakes.MagnumParseException, + self.check_parser, self.cmd, arglist, verifylist) + + +class TestNodeGroupUpdate(TestNodeGroup): + + def setUp(self): + super(TestNodeGroupUpdate, self).setUp() + + self.ng_mock.update = mock.Mock() + self.ng_mock.update.return_value = None + + # Get the command object to test + self.cmd = osc_nodegroups.UpdateNodeGroup(self.app, None) + + def test_nodegroup_update_pass(self): + arglist = ['foo', 'ng1', 'remove', 'bar'] + verifylist = [ + ('cluster', 'foo'), + ('nodegroup', 'ng1'), + ('op', 'remove'), + ('attributes', [['bar']]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.ng_mock.update.assert_called_with( + 'foo', 'ng1', + [{'op': 'remove', 'path': '/bar'}] + ) + + def test_nodegroup_update_bad_op(self): + arglist = ['cluster', 'ng1', 'foo', 'bar'] + verifylist = [ + ('cluster', 'cluster'), + ('nodegroup', 'ng1'), + ('op', 'foo'), + ('attributes', ['bar']) + ] + + self.assertRaises(magnum_fakes.MagnumParseException, + self.check_parser, self.cmd, arglist, verifylist) + + class TestNodeGroupShow(TestNodeGroup): def setUp(self): @@ -81,14 +253,17 @@ class TestNodeGroupList(TestNodeGroup): nodegroup = magnum_fakes.FakeNodeGroup.create_one_nodegroup() - columns = ['uuid', 'name', 'flavor_id', 'node_count', 'role'] + columns = ['uuid', 'name', 'flavor_id', 'image_id', 'node_count', + 'status', 'role'] datalist = ( ( nodegroup.uuid, nodegroup.name, nodegroup.flavor_id, + nodegroup.image_id, nodegroup.node_count, + nodegroup.status, nodegroup.role, ), ) diff --git a/magnumclient/tests/v1/test_nodegroups.py b/magnumclient/tests/v1/test_nodegroups.py index d4c613c4..40cbcf66 100644 --- a/magnumclient/tests/v1/test_nodegroups.py +++ b/magnumclient/tests/v1/test_nodegroups.py @@ -13,9 +13,12 @@ # License for the specific language governing permissions and limitations # under the License. +import copy + import testtools from testtools import matchers +from magnumclient import exceptions from magnumclient.tests import utils from magnumclient.v1 import nodegroups @@ -53,6 +56,17 @@ NODEGROUP2 = { 'min_node_count': 1 } +CREATE_NODEGROUP = copy.deepcopy(NODEGROUP1) +del CREATE_NODEGROUP['id'] +del CREATE_NODEGROUP['uuid'] +del CREATE_NODEGROUP['node_addresses'] +del CREATE_NODEGROUP['is_default'] +del CREATE_NODEGROUP['cluster_id'] + +UPDATED_NODEGROUP = copy.deepcopy(NODEGROUP1) +NEW_NODE_COUNT = 9 +UPDATED_NODEGROUP['node_count'] = NEW_NODE_COUNT + fake_responses = { '/v1/clusters/test/nodegroups/': @@ -61,6 +75,10 @@ fake_responses = { {}, {'nodegroups': [NODEGROUP1, NODEGROUP2]}, ), + 'POST': ( + {}, + CREATE_NODEGROUP, + ), }, '/v1/clusters/test/nodegroups/%s' % NODEGROUP1['id']: { @@ -68,13 +86,36 @@ fake_responses = { {}, NODEGROUP1 ), + 'DELETE': ( + {}, + None, + ), + 'PATCH': ( + {}, + UPDATED_NODEGROUP, + ), }, + '/v1/clusters/test/nodegroups/%s/?rollback=True' % NODEGROUP1['id']: + { + 'PATCH': ( + {}, + UPDATED_NODEGROUP, + ), + }, '/v1/clusters/test/nodegroups/%s' % NODEGROUP1['name']: { 'GET': ( {}, NODEGROUP1 ), + 'DELETE': ( + {}, + None, + ), + 'PATCH': ( + {}, + UPDATED_NODEGROUP, + ), }, '/v1/clusters/test/nodegroups/?limit=2': { @@ -222,3 +263,71 @@ class NodeGroupManagerTest(testtools.TestCase): ] self.assertEqual(expect, self.api.calls) self.assertEqual(NODEGROUP1['name'], nodegroup.name) + + def test_nodegroup_delete_by_id(self): + nodegroup = self.mgr.delete(self.cluster_id, NODEGROUP1['id']) + expect = [ + ('DELETE', self.base_path + '%s' % NODEGROUP1['id'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(nodegroup) + + def test_nodegroup_delete_by_name(self): + nodegroup = self.mgr.delete(self.cluster_id, NODEGROUP1['name']) + expect = [ + ('DELETE', self.base_path + '%s' % NODEGROUP1['name'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(nodegroup) + + def test_nodegroup_update(self): + patch = {'op': 'replace', + 'value': NEW_NODE_COUNT, + 'path': '/node_count'} + nodegroup = self.mgr.update(self.cluster_id, id=NODEGROUP1['id'], + patch=patch) + expect = [ + ('PATCH', self.base_path + '%s' % NODEGROUP1['id'], {}, patch), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NEW_NODE_COUNT, nodegroup.node_count) + + def test_nodegroup_create(self): + nodegroup = self.mgr.create(self.cluster_id, **CREATE_NODEGROUP) + expect = [ + ('POST', self.base_path, {}, CREATE_NODEGROUP), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(nodegroup) + + def test_nodegroup_create_with_docker_volume_size(self): + ng_with_volume_size = dict() + ng_with_volume_size.update(CREATE_NODEGROUP) + ng_with_volume_size['docker_volume_size'] = 20 + nodegroup = self.mgr.create(self.cluster_id, **ng_with_volume_size) + expect = [ + ('POST', self.base_path, {}, ng_with_volume_size), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(nodegroup) + + def test_nodegroup_create_with_labels(self): + ng_with_labels = dict() + ng_with_labels.update(CREATE_NODEGROUP) + ng_with_labels['labels'] = "key=val" + nodegroup = self.mgr.create(self.cluster_id, **ng_with_labels) + expect = [ + ('POST', self.base_path, {}, ng_with_labels), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(nodegroup) + + def test_nodegroup_create_fail(self): + CREATE_NODEGROUP_FAIL = copy.deepcopy(CREATE_NODEGROUP) + CREATE_NODEGROUP_FAIL["wrong_key"] = "wrong" + self.assertRaisesRegex(exceptions.InvalidAttribute, + ("Key must be in %s" % + ','.join(nodegroups.CREATION_ATTRIBUTES)), + self.mgr.create, self.cluster_id, + **CREATE_NODEGROUP_FAIL) + self.assertEqual([], self.api.calls) diff --git a/magnumclient/v1/nodegroups.py b/magnumclient/v1/nodegroups.py index dbe7e003..cd799349 100644 --- a/magnumclient/v1/nodegroups.py +++ b/magnumclient/v1/nodegroups.py @@ -14,6 +14,7 @@ # under the License. from magnumclient.common import utils +from magnumclient import exceptions from magnumclient.v1 import baseunit @@ -65,3 +66,19 @@ class NodeGroupManager(baseunit.BaseTemplateManager): return self._list(self._path(cluster_id, id=id))[0] except IndexError: return None + + def create(self, cluster_id, **kwargs): + new = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + new[key] = value + else: + raise exceptions.InvalidAttribute( + "Key must be in %s" % ",".join(CREATION_ATTRIBUTES)) + return self._create(self._path(cluster_id), new) + + def delete(self, cluster_id, id): + return self._delete(self._path(cluster_id, id=id)) + + def update(self, cluster_id, id, patch): + return self._update(self._path(cluster_id, id=id), patch) diff --git a/setup.cfg b/setup.cfg index 3a3f6b3d..860e210c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,7 +59,9 @@ openstack.container_infra.v1 = coe_nodegroup_list = magnumclient.osc.v1.nodegroups:ListNodeGroup coe_nodegroup_show = magnumclient.osc.v1.nodegroups:ShowNodeGroup - + coe_nodegroup_create = magnumclient.osc.v1.nodegroups:CreateNodeGroup + coe_nodegroup_delete = magnumclient.osc.v1.nodegroups:DeleteNodeGroup + coe_nodegroup_update = magnumclient.osc.v1.nodegroups:UpdateNodeGroup [compile_catalog] directory = magnumclient/locale