Add nodegroup CRUD commands

The commands added are:

* openstack coe nodegroup create <params> <cluster> <nodegroup>
* openstack coe nodegroup delete <cluster> <nodegroup>
* openstack coe nodegroup update <op> <params> <cluster> <nodegroup>

Depends-On: I4ad60994ad6b4cb9cac18129557e1e87e61ae98c
Change-Id: I98b662b5a95f16d80852e3b30683c75e78acb3e5
This commit is contained in:
Theodoros Tsioutsias 2019-03-26 15:51:36 +00:00
parent 69be0ac44e
commit 934cf54854
6 changed files with 483 additions and 5 deletions

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from magnumclient.common import utils as magnum_utils
from magnumclient.i18n import _ from magnumclient.i18n import _
from osc_lib.command import command from osc_lib.command import command
@ -33,10 +34,133 @@ NODEGROUP_ATTRIBUTES = [
'role', 'role',
'max_node_count', 'max_node_count',
'min_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='<docker-volume-size>',
help=('The size in GB for the docker volume to '
'use.'))
parser.add_argument('--labels',
metavar='<KEY1=VALUE1,KEY2=VALUE2;KEY3=VALUE3...>',
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='<cluster>',
help='Name of the nodegroup to create.')
parser.add_argument('name',
metavar='<name>',
help='Name of the nodegroup to create.')
parser.add_argument('--node-count',
dest='node_count',
type=int,
default=1,
metavar='<node-count>',
help='The nodegroup node count.')
parser.add_argument('--min-nodes',
dest='min_node_count',
type=int,
default=1,
metavar='<min-nodes>',
help='The nodegroup minimum node count.')
parser.add_argument('--max-nodes',
dest='max_node_count',
type=int,
default=None,
metavar='<max-nodes>',
help='The nodegroup maximum node count.')
parser.add_argument('--role',
dest='role',
type=str,
default='worker',
metavar='<role>',
help=('The role of the nodegroup'))
parser.add_argument(
'--image',
metavar='<image>',
help=_('The name or UUID of the base image to customize for the '
'NodeGroup.'))
parser.add_argument(
'--flavor',
metavar='<flavor>',
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='<cluster>',
help=_('ID or name of the cluster where the nodegroup(s) '
'belong(s).'))
parser.add_argument(
'nodegroup',
nargs='+',
metavar='<nodegroup>',
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): class ListNodeGroup(command.Lister):
_description = _("List nodegroups") _description = _("List nodegroups")
@ -72,7 +196,8 @@ class ListNodeGroup(command.Lister):
self.log.debug("take_action(%s)", parsed_args) self.log.debug("take_action(%s)", parsed_args)
mag_client = self.app.client_manager.container_infra 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 cluster_id = parsed_args.cluster
nodegroups = mag_client.nodegroups.list(cluster_id, nodegroups = mag_client.nodegroups.list(cluster_id,
limit=parsed_args.limit, limit=parsed_args.limit,
@ -112,3 +237,50 @@ class ShowNodeGroup(command.ShowOne):
parsed_args.nodegroup) parsed_args.nodegroup)
return (columns, utils.get_item_properties(nodegroup, columns)) 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='<cluster>',
help=_('ID or name of the cluster where the nodegroup belongs.'))
parser.add_argument(
'nodegroup',
metavar='<nodegroup>',
help=_('The name or UUID of cluster to update'))
parser.add_argument(
'op',
metavar='<op>',
choices=['add', 'replace', 'remove'],
help=_("Operations: one of 'add', 'replace' or 'remove'"))
parser.add_argument(
'attributes',
metavar='<path=value>',
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)

View File

@ -332,7 +332,10 @@ class FakeNodeGroup(object):
'role': 'worker', 'role': 'worker',
'max_node_count': 10, 'max_node_count': 10,
'min_node_count': 1, '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. # Overwrite default attributes.

View File

@ -13,7 +13,9 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import copy
import mock import mock
from mock import call
from magnumclient.osc.v1 import nodegroups as osc_nodegroups from magnumclient.osc.v1 import nodegroups as osc_nodegroups
from magnumclient.tests.osc.unit.v1 import fakes as magnum_fakes 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 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): class TestNodeGroupShow(TestNodeGroup):
def setUp(self): def setUp(self):
@ -81,14 +253,17 @@ class TestNodeGroupList(TestNodeGroup):
nodegroup = magnum_fakes.FakeNodeGroup.create_one_nodegroup() 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 = ( datalist = (
( (
nodegroup.uuid, nodegroup.uuid,
nodegroup.name, nodegroup.name,
nodegroup.flavor_id, nodegroup.flavor_id,
nodegroup.image_id,
nodegroup.node_count, nodegroup.node_count,
nodegroup.status,
nodegroup.role, nodegroup.role,
), ),
) )

View File

@ -13,9 +13,12 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import copy
import testtools import testtools
from testtools import matchers from testtools import matchers
from magnumclient import exceptions
from magnumclient.tests import utils from magnumclient.tests import utils
from magnumclient.v1 import nodegroups from magnumclient.v1 import nodegroups
@ -53,6 +56,17 @@ NODEGROUP2 = {
'min_node_count': 1 '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 = { fake_responses = {
'/v1/clusters/test/nodegroups/': '/v1/clusters/test/nodegroups/':
@ -61,6 +75,10 @@ fake_responses = {
{}, {},
{'nodegroups': [NODEGROUP1, NODEGROUP2]}, {'nodegroups': [NODEGROUP1, NODEGROUP2]},
), ),
'POST': (
{},
CREATE_NODEGROUP,
),
}, },
'/v1/clusters/test/nodegroups/%s' % NODEGROUP1['id']: '/v1/clusters/test/nodegroups/%s' % NODEGROUP1['id']:
{ {
@ -68,13 +86,36 @@ fake_responses = {
{}, {},
NODEGROUP1 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']: '/v1/clusters/test/nodegroups/%s' % NODEGROUP1['name']:
{ {
'GET': ( 'GET': (
{}, {},
NODEGROUP1 NODEGROUP1
), ),
'DELETE': (
{},
None,
),
'PATCH': (
{},
UPDATED_NODEGROUP,
),
}, },
'/v1/clusters/test/nodegroups/?limit=2': '/v1/clusters/test/nodegroups/?limit=2':
{ {
@ -222,3 +263,71 @@ class NodeGroupManagerTest(testtools.TestCase):
] ]
self.assertEqual(expect, self.api.calls) self.assertEqual(expect, self.api.calls)
self.assertEqual(NODEGROUP1['name'], nodegroup.name) 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)

View File

@ -14,6 +14,7 @@
# under the License. # under the License.
from magnumclient.common import utils from magnumclient.common import utils
from magnumclient import exceptions
from magnumclient.v1 import baseunit from magnumclient.v1 import baseunit
@ -65,3 +66,19 @@ class NodeGroupManager(baseunit.BaseTemplateManager):
return self._list(self._path(cluster_id, id=id))[0] return self._list(self._path(cluster_id, id=id))[0]
except IndexError: except IndexError:
return None 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)

View File

@ -59,7 +59,9 @@ openstack.container_infra.v1 =
coe_nodegroup_list = magnumclient.osc.v1.nodegroups:ListNodeGroup coe_nodegroup_list = magnumclient.osc.v1.nodegroups:ListNodeGroup
coe_nodegroup_show = magnumclient.osc.v1.nodegroups:ShowNodeGroup 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] [compile_catalog]
directory = magnumclient/locale directory = magnumclient/locale