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
(cherry picked from commit 934cf54854)
This commit is contained in:
Theodoros Tsioutsias 2019-03-26 15:51:36 +00:00 committed by Spyros Trigazis
parent a429ccc253
commit 8106c5ff00
6 changed files with 483 additions and 5 deletions

View File

@ -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='<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):
_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='<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',
'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.

View File

@ -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,
),
)

View File

@ -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)

View File

@ -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)

View File

@ -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