Add node aggregate commands support

Partially Implements: bp node-aggregate

Change-Id: I37a3da4e4f35aba5efda66a62777e70476b07676
This commit is contained in:
liusheng 2017-08-09 14:37:38 +08:00
parent bab9a4fc01
commit 01c02f8f7b
6 changed files with 580 additions and 1 deletions

View File

@ -0,0 +1,228 @@
# Copyright 2017 Huawei, Inc. 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.
#
"""Mogan v1 Baremetal node aggregate action implementations"""
import copy
import logging
from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
from moganclient.common.i18n import _
LOG = logging.getLogger(__name__)
class CreateAggregate(command.ShowOne):
"""Create a node aggregate"""
def get_parser(self, prog_name):
parser = super(CreateAggregate, self).get_parser(prog_name)
parser.add_argument(
"name",
metavar="<name>",
help=_("Name of baremetal node aggregate")
)
parser.add_argument(
"--property",
metavar="<key=value>",
action=parseractions.KeyValueAction,
help=_("Property to add to this node aggregate "
"(repeat option to set multiple properties)")
)
return parser
def take_action(self, parsed_args):
bc_client = self.app.client_manager.baremetal_compute
data = bc_client.aggregate.create(parsed_args.name,
parsed_args.property)
# Special mapping for columns to make the output easier to read:
# 'metadata' --> 'properties'
data._info.update(
{
'properties': utils.format_dict(data._info.pop('metadata')),
},
)
info = copy.copy(data._info)
return zip(*sorted(info.items()))
class DeleteAggregate(command.Command):
"""Delete existing baremetal node aggregate(s)"""
def get_parser(self, prog_name):
parser = super(DeleteAggregate, self).get_parser(prog_name)
parser.add_argument(
'aggregate',
metavar='<aggregate>',
nargs='+',
help=_("Aggregate(s) to delete (name or UUID)")
)
return parser
def take_action(self, parsed_args):
bc_client = self.app.client_manager.baremetal_compute
result = 0
for one_aggregate in parsed_args.aggregate:
try:
data = utils.find_resource(
bc_client.aggregate, one_aggregate)
bc_client.aggregate.delete(data.uuid)
except Exception as e:
result += 1
LOG.error(_("Failed to delete node aggregate with name or "
"UUID '%(aggregate)s': %(e)s") %
{'aggregate': one_aggregate, 'e': e})
if result > 0:
total = len(parsed_args.aggregate)
msg = (_("%(result)s of %(total)s node aggregates failed "
"to delete.") % {'result': result, 'total': total})
raise exceptions.CommandError(msg)
class ListAggregate(command.Lister):
"""List all baremetal node aggregates"""
def take_action(self, parsed_args):
bc_client = self.app.client_manager.baremetal_compute
data = bc_client.aggregate.list()
column_headers = (
"UUID",
"Name",
"Properties",
)
columns = (
"UUID",
"Name",
"Metadata",
)
formatters = {'Metadata': utils.format_dict}
return (column_headers,
(utils.get_item_properties(
s, columns, formatters=formatters
) for s in data))
class ShowAggregate(command.ShowOne):
"""Display baremetal node aggregate details"""
def get_parser(self, prog_name):
parser = super(ShowAggregate, self).get_parser(prog_name)
parser.add_argument(
'aggregate',
metavar='<aggregate>',
help=_("Aggregate to display (name or UUID)")
)
return parser
def take_action(self, parsed_args):
bc_client = self.app.client_manager.baremetal_compute
data = utils.find_resource(
bc_client.aggregate,
parsed_args.aggregate,
)
# Special mapping for columns to make the output easier to read:
# 'metadata' --> 'properties'
data._info.update(
{
'properties': utils.format_dict(data._info.pop('metadata')),
},
)
info = {}
info.update(data._info)
return zip(*sorted(info.items()))
class SetAggregate(command.Command):
"""Set properties for a baremetal node aggregate"""
def get_parser(self, prog_name):
parser = super(SetAggregate, self).get_parser(prog_name)
parser.add_argument(
'aggregate',
metavar='<aggregate>',
help=_("Aggregate(s) to delete (name or UUID)")
)
parser.add_argument(
'--name',
metavar='<name>',
help=_('Set a new name to a node aggregate (admin only)')
)
parser.add_argument(
"--property",
metavar="<key=value>",
action=parseractions.KeyValueAction,
help=_("Property to set on this node aggregate "
"(repeat option to set multiple properties)")
)
return parser
def take_action(self, parsed_args):
bc_client = self.app.client_manager.baremetal_compute
aggregate = utils.find_resource(
bc_client.aggregate,
parsed_args.aggregate,
)
updates = []
if parsed_args.name:
updates.append({"op": "replace",
"path": "/name",
"value": parsed_args.name})
for key, value in (parsed_args.property or {}).items():
updates.append({"op": "add",
"path": "/metadata/%s" % key,
"value": value})
if updates:
bc_client.aggregate.update(aggregate, updates)
class UnsetAggregate(command.Command):
"""Unset properties for a baremetal node aggregate"""
def get_parser(self, prog_name):
parser = super(UnsetAggregate, self).get_parser(prog_name)
parser.add_argument(
'aggregate',
metavar='<aggregate>',
help=_("Aggregate(s) to delete (name or UUID)")
)
parser.add_argument(
"--property",
metavar="<key>",
action='append',
help=_("Property to remove from this node aggregate "
"(repeat option to remove multiple properties)")
)
return parser
def take_action(self, parsed_args):
bc_client = self.app.client_manager.baremetal_compute
aggregate = utils.find_resource(
bc_client.aggregate,
parsed_args.aggregate,
)
updates = []
for key in parsed_args.property or []:
updates.append({"op": "remove",
"path": "/metadata/%s" % key})
if updates:
bc_client.aggregate.update(aggregate, updates)

View File

@ -21,6 +21,7 @@ from oslo_utils import uuidutils
from requests import Response
from moganclient.common import base
from moganclient.v1 import aggregate
from moganclient.v1 import availability_zone
from moganclient.v1 import flavor
from moganclient.v1 import node
@ -67,6 +68,7 @@ class FakeBaremetalComputeV1Client(object):
self.availability_zone = availability_zone.AvailabilityZoneManager(
self.fake_http_client)
self.node = node.NodeManager(self.fake_http_client)
self.aggregate = aggregate.AggregateManager(self.fake_http_client)
class FakeHTTPClient(object):
@ -323,3 +325,74 @@ class FakeServer(object):
if servers is None:
servers = FakeServer.create_servers(count)
return mock.Mock(side_effect=servers)
class FakeAggregate(object):
"""Fake one baremetal node aggregate."""
@staticmethod
def create_one_aggregate(attrs=None):
"""Create a fake baremetal aggregate.
:param Dictionary attrs:
A dictionary with all attributes
:return:
A FakeResource object, with uuid and other attributes
"""
attrs = attrs or {}
# Set default attribute
agg_info = {
"created_at": "2016-09-27T02:37:21.966342+00:00",
"metadata": {"key1": "value1"},
"links": [],
"name": "agg-name-" + uuidutils.generate_uuid(dashed=False),
"updated_at": None,
"uuid": "agg-id-" + uuidutils.generate_uuid(dashed=False),
}
# Overwrite default attributes.
agg_info.update(attrs)
agg = FakeResource(
manager=None,
info=copy.deepcopy(agg_info),
loaded=True)
return agg
@staticmethod
def create_aggregates(attrs=None, count=2):
"""Create multiple fake baremetal node aggregates.
:param Dictionary attrs:
A dictionary with all attributes
:param int count:
The number of aggregates to fake
:return:
A list of FakeResource objects faking the aggregates
"""
aggs = []
for i in range(0, count):
aggs.append(
FakeAggregate.create_one_aggregate(attrs))
return aggs
@staticmethod
def get_aggregates(aggregates=None, count=2):
"""Get an iterable Mock object with a list of faked aggregates.
If aggregates list is provided, then initialize the Mock object
with the list. Otherwise create one.
:param List aggregates:
A list of FakeResource objects faking aggregates
:param int count:
The number of aggregates to fake
:return:
An iterable Mock object with side_effect set to a list of faked
baremetal aggregates
"""
if aggregates is None:
aggregates = FakeAggregate.create_aggregates(count)
return mock.Mock(side_effect=aggregates)

View File

@ -0,0 +1,224 @@
# Copyright 2017 Huawei, Inc. 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 osc_lib import utils
from moganclient.common import base
from moganclient.osc.v1 import aggregate
from moganclient.tests.unit import base as test_base
from moganclient.tests.unit import fakes
from moganclient.v1 import aggregate as aggregate_mgr
class TestAggregateBase(test_base.TestBaremetalComputeV1):
def setUp(self):
super(TestAggregateBase, self).setUp()
self.fake_agg = fakes.FakeAggregate.create_one_aggregate()
self.columns = (
'created_at',
'links',
'name',
'properties',
'updated_at',
'uuid',
)
self.data = (
self.fake_agg.created_at,
self.fake_agg.links,
self.fake_agg.name,
utils.format_dict(self.fake_agg.metadata),
self.fake_agg.updated_at,
self.fake_agg.uuid,
)
@mock.patch.object(aggregate_mgr.AggregateManager, '_create')
class TestAggregateCreate(TestAggregateBase):
def setUp(self):
super(TestAggregateCreate, self).setUp()
self.cmd = aggregate.CreateAggregate(self.app, None)
def test_aggregate_create(self, mock_create):
arglist = [
'test_agg',
'--property', 'k1=v1'
]
verifylist = [
('name', 'test_agg'),
('property', {'k1': 'v1'}),
]
mock_create.return_value = self.fake_agg
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
mock_create.assert_called_once_with('/aggregates',
data={
'name': 'test_agg',
'metadata': {'k1': 'v1'},
})
self.assertEqual(self.columns, columns)
self.assertEqual(self.data, data)
@mock.patch.object(utils, 'find_resource')
@mock.patch.object(aggregate_mgr.AggregateManager, '_delete')
class TestAggregateDelete(TestAggregateBase):
def setUp(self):
super(TestAggregateDelete, self).setUp()
self.cmd = aggregate.DeleteAggregate(self.app, None)
def test_aggregate_delete(self, mock_delete, mock_find):
arglist = [
'test_agg1',
]
verifylist = [
('aggregate', ['test_agg1']),
]
mock_find.return_value = self.fake_agg
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
expected_url = '/aggregates/%s' % base.getid(self.fake_agg)
mock_delete.assert_called_once_with(expected_url)
self.assertIsNone(result)
def test_aggregate_multiple_delete(self, mock_delete, mock_find):
arglist = [
'agg1',
'agg2',
'agg3'
]
verifylist = [
('aggregate', ['agg1', 'agg2', 'agg3']),
]
mock_find.return_value = self.fake_agg
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
expected_url = '/aggregates/%s' % base.getid(self.fake_agg)
expected_call = [mock.call(expected_url), mock.call(expected_url),
mock.call(expected_url)]
mock_delete.assert_has_calls(expected_call)
self.assertIsNone(result)
@mock.patch.object(aggregate_mgr.AggregateManager, '_list')
class TestAggregateList(TestAggregateBase):
def setUp(self):
super(TestAggregateList, self).setUp()
self.list_columns = (
"UUID",
"Name",
"Properties"
)
self.list_data = (
(self.fake_agg.uuid,
self.fake_agg.name,
utils.format_dict(self.fake_agg.metadata),
),)
self.cmd = aggregate.ListAggregate(self.app, None)
def test_aggregate_list(self, mock_list):
arglist = []
verifylist = []
mock_list.return_value = [self.fake_agg]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
mock_list.assert_called_once_with('/aggregates',
response_key='aggregates')
self.assertEqual(self.list_columns, columns)
self.assertEqual(self.list_data, tuple(data))
@mock.patch.object(aggregate_mgr.AggregateManager, '_get')
class TestAggregateShow(TestAggregateBase):
def setUp(self):
super(TestAggregateShow, self).setUp()
self.cmd = aggregate.ShowAggregate(self.app, None)
def test_agregate_show(self, mock_get):
arglist = [
'agg1',
]
verifylist = [
('aggregate', 'agg1'),
]
mock_get.return_value = self.fake_agg
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
expected_url = '/aggregates/%s' % parsed_args.aggregate
mock_get.assert_called_once_with(expected_url)
self.assertEqual(self.columns, columns)
self.assertEqual(self.data, data)
@mock.patch.object(utils, 'find_resource')
@mock.patch.object(aggregate_mgr.AggregateManager, '_update')
class TestAggregateSet(TestAggregateBase):
def setUp(self):
super(TestAggregateSet, self).setUp()
self.cmd = aggregate.SetAggregate(self.app, None)
def test_aggregate_update(self, mock_update, mock_find):
mock_find.return_value = self.fake_agg
arglist = [
'--name', 'test_agg',
'--property', 'k1=v1',
self.fake_agg.uuid,
]
verifylist = [
('aggregate', self.fake_agg.uuid),
('name', 'test_agg'),
('property', {'k1': 'v1'}),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
expected_url = '/aggregates/%s' % base.getid(self.fake_agg)
expected_args = [
{'path': '/name', 'value': 'test_agg', 'op': 'replace'},
{'path': '/metadata/k1', 'value': 'v1', 'op': 'add'},
]
mock_update.assert_called_once_with(expected_url,
data=expected_args)
@mock.patch.object(utils, 'find_resource')
@mock.patch.object(aggregate_mgr.AggregateManager, '_update')
class TestAggregateUnset(TestAggregateBase):
def setUp(self):
super(TestAggregateUnset, self).setUp()
self.cmd = aggregate.UnsetAggregate(self.app, None)
def test_aggregate_update(self, mock_update, mock_find):
mock_find.return_value = self.fake_agg
arglist = [
'--property', 'key1',
self.fake_agg.uuid,
]
verifylist = [
('aggregate', self.fake_agg.uuid),
('property', ['key1']),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
expected_url = '/aggregates/%s' % base.getid(self.fake_agg)
expected_args = [
{'path': '/metadata/key1', 'op': 'remove'}
]
mock_update.assert_called_once_with(expected_url,
data=expected_args)

View File

@ -0,0 +1,47 @@
# Copyright 2017 Huawei, Inc. 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 moganclient.common import base
class Aggregate(base.Resource):
pass
class AggregateManager(base.ManagerWithFind):
resource_class = Aggregate
def create(self, name, metadata=None):
url = '/aggregates'
data = {'name': name}
if metadata:
data['metadata'] = metadata
return self._create(url, data=data)
def delete(self, aggregate):
url = '/aggregates/%s' % base.getid(aggregate)
return self._delete(url)
def get(self, aggregate):
url = '/aggregates/%s' % base.getid(aggregate)
return self._get(url)
def list(self):
url = '/aggregates'
return self._list(url, response_key='aggregates')
def update(self, aggregate, updates):
url = '/aggregates/%s' % base.getid(aggregate)
return self._update(url, data=updates)

View File

@ -14,6 +14,7 @@
#
from moganclient.common import http
from moganclient.v1 import aggregate
from moganclient.v1 import availability_zone
from moganclient.v1 import flavor
from moganclient.v1 import keypair
@ -35,3 +36,4 @@ class Client(object):
self.http_client)
self.keypair = keypair.KeyPairManager(self.http_client)
self.node = node.NodeManager(self.http_client)
self.aggregate = aggregate.AggregateManager(self.http_client)

View File

@ -59,7 +59,12 @@ openstack.baremetal_compute.v1 =
# TODO(liusheng): may change the "baremetal" to another word to avoid
# conflict with Ironic
baremetal_compute_node_list = moganclient.osc.v1.node:ListNode
baremetal_aggregate_create = moganclient.osc.v1.aggregate:CreateAggregate
baremetal_aggregate_show = moganclient.osc.v1.aggregate:ShowAggregate
baremetal_aggregate_list = moganclient.osc.v1.aggregate:ListAggregate
baremetal_aggregate_delete = moganclient.osc.v1.aggregate:DeleteAggregate
baremetal_aggregate_set = moganclient.osc.v1.aggregate:SetAggregate
baremetal_aggregate_unset = moganclient.osc.v1.aggregate:UnsetAggregate
[build_sphinx]
source-dir = doc/source