From a5a15e40a5484975243afb8519cc47261cf01f91 Mon Sep 17 00:00:00 2001 From: "Brad P. Crochet" Date: Thu, 30 Jul 2015 09:50:36 -0400 Subject: [PATCH] Introduce openstackclient plugin Adds the code necessary for ironicclient to work as a plugin for openstackclient. * baremetal list * baremetal show * baremetal delete * baremetal create * baremetal set * baremetal unset Change-Id: I91ea4f4a63b0e7a8ea004c47422d19ca0f288dcd --- ironicclient/osc/__init__.py | 0 ironicclient/osc/client.py | 51 ++ ironicclient/osc/plugin.py | 70 +++ ironicclient/osc/v1/__init__.py | 0 ironicclient/osc/v1/baremetal.py | 320 +++++++++++ ironicclient/tests/unit/osc/__init__.py | 0 ironicclient/tests/unit/osc/fakes.py | 58 ++ ironicclient/tests/unit/osc/v1/__init__.py | 0 ironicclient/tests/unit/osc/v1/fakes.py | 52 ++ .../tests/unit/osc/v1/test_baremetal.py | 515 ++++++++++++++++++ requirements.txt | 2 + setup.cfg | 11 + 12 files changed, 1079 insertions(+) create mode 100644 ironicclient/osc/__init__.py create mode 100644 ironicclient/osc/client.py create mode 100644 ironicclient/osc/plugin.py create mode 100644 ironicclient/osc/v1/__init__.py create mode 100644 ironicclient/osc/v1/baremetal.py create mode 100644 ironicclient/tests/unit/osc/__init__.py create mode 100644 ironicclient/tests/unit/osc/fakes.py create mode 100644 ironicclient/tests/unit/osc/v1/__init__.py create mode 100644 ironicclient/tests/unit/osc/v1/fakes.py create mode 100644 ironicclient/tests/unit/osc/v1/test_baremetal.py diff --git a/ironicclient/osc/__init__.py b/ironicclient/osc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ironicclient/osc/client.py b/ironicclient/osc/client.py new file mode 100644 index 000000000..5beaebf32 --- /dev/null +++ b/ironicclient/osc/client.py @@ -0,0 +1,51 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 OpenStack Foundation +# Copyright 2011 Piston Cloud Computing, Inc. +# +# Modified by: Brad P. Crochet +# +# 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. + +""" +OpenStack Client factory. +""" + +from oslo_utils import importutils + +from ironicclient.common.i18n import _ +from ironicclient import exceptions + + +def get_client_class(version): + version_map = { + '1': 'ironicclient.v1.client.Client', + '1.5': 'ironicclient.v1.client.Client', + '1.6': 'ironicclient.v1.client.Client', + '1.9': 'ironicclient.v1.client.Client', + } + try: + client_path = version_map[str(version)] + except (KeyError, ValueError): + msg = _("Invalid client version '%(version)s'. must be one of: " + "%(keys)s") % {'version': version, + 'keys': ', '.join(version_map)} + raise exceptions.UnsupportedVersion(msg) + + return importutils.import_class(client_path) + + +def Client(version, *args, **kwargs): + client_class = get_client_class(version) + return client_class(*args, **kwargs) diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py new file mode 100644 index 000000000..f50b35277 --- /dev/null +++ b/ironicclient/osc/plugin.py @@ -0,0 +1,70 @@ +# +# Copyright 2015 Red Hat, Inc. +# +# 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 logging + +from openstackclient.common import exceptions +from openstackclient.common import utils + +from ironicclient.osc import client as ironic_client + +LOG = logging.getLogger(__name__) + +DEFAULT_BAREMETAL_API_VERSION = '1.6' +API_VERSION_OPTION = 'os_baremetal_api_version' +API_NAME = 'baremetal' +API_VERSIONS = { + '1': 'ironicclient.osc.client', + '1.5': 'ironicclient.osc.client', + '1.6': 'ironicclient.osc.client', + '1.9': 'ironicclient.osc.client', +} + + +def make_client(instance): + """Returns a baremetal service client.""" + try: + baremetal_client = ironic_client.get_client_class( + instance._api_version[API_NAME]) + except Exception: + msg = "Invalid %s client version '%s'. Must be one of %s" % ( + (API_NAME, instance._api_version[API_NAME], + ", ".join(sorted(API_VERSIONS)))) + raise exceptions.UnsupportedVersion(msg) + LOG.debug('Instantiating baremetal client: %s', baremetal_client) + + client = baremetal_client( + os_ironic_api_version=instance._api_version[API_NAME], + session=instance.session, + region_name=instance._region_name, + endpoint=instance.auth_ref.auth_url, + ) + + return client + + +def build_option_parser(parser): + """Hook to add global options.""" + parser.add_argument( + '--os-baremetal-api-version', + metavar='', + default=utils.env( + 'OS_BAREMETAL_API_VERSION', + default=DEFAULT_BAREMETAL_API_VERSION), + help='Baremetal API version, default=' + + DEFAULT_BAREMETAL_API_VERSION + + ' (Env: OS_BAREMETAL_API_VERSION)') + return parser diff --git a/ironicclient/osc/v1/__init__.py b/ironicclient/osc/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ironicclient/osc/v1/baremetal.py b/ironicclient/osc/v1/baremetal.py new file mode 100644 index 000000000..737555fc0 --- /dev/null +++ b/ironicclient/osc/v1/baremetal.py @@ -0,0 +1,320 @@ +# +# Copyright 2015 Red Hat, Inc. +# +# 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 logging +import six + +from cliff import command +from cliff import lister +from cliff import show +from openstackclient.common import utils as oscutils + +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient import exc +from ironicclient.v1 import resource_fields as res_fields + + +class CreateBaremetal(show.ShowOne): + """Register a new node with the baremetal service""" + + log = logging.getLogger(__name__ + ".CreateBaremetal") + + def get_parser(self, prog_name): + parser = super(CreateBaremetal, self).get_parser(prog_name) + + parser.add_argument( + '--chassis-uuid', + dest='chassis_uuid', + metavar='', + help='UUID of the chassis that this node belongs to.') + parser.add_argument( + '--driver', + metavar='', + required=True, + help='Driver used to control the node [REQUIRED].') + parser.add_argument( + '--driver-info', + metavar='', + action='append', + help='Key/value pair used by the driver, such as out-of-band ' + 'management credentials. Can be specified multiple times.') + parser.add_argument( + '--property', + dest='properties', + metavar='', + action='append', + help='Key/value pair describing the physical characteristics of ' + 'the node. This is exported to Nova and used by the ' + 'scheduler. Can be specified multiple times.') + parser.add_argument( + '--extra', + metavar='', + action='append', + help="Record arbitrary key/value metadata. " + "Can be specified multiple times.") + parser.add_argument( + '--uuid', + metavar='', + help="Unique UUID for the node.") + parser.add_argument( + '--name', + metavar='', + help="Unique name for the node.") + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + + field_list = ['chassis_uuid', 'driver', 'driver_info', + 'properties', 'extra', 'uuid', 'name'] + fields = dict((k, v) for (k, v) in vars(parsed_args).items() + if k in field_list and not (v is None)) + fields = utils.args_array_to_dict(fields, 'driver_info') + fields = utils.args_array_to_dict(fields, 'extra') + fields = utils.args_array_to_dict(fields, 'properties') + node = baremetal_client.node.create(**fields)._info + + node.pop('links', None) + + return self.dict2columns(node) + + +class DeleteBaremetal(command.Command): + """Unregister a baremetal node""" + + log = logging.getLogger(__name__ + ".DeleteBaremetal") + + def get_parser(self, prog_name): + parser = super(DeleteBaremetal, self).get_parser(prog_name) + parser.add_argument( + "node", + metavar="", + help="Node to delete (name or ID)") + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + + node = oscutils.find_resource(baremetal_client.node, + parsed_args.node) + baremetal_client.node.delete(node.uuid) + + +class ListBaremetal(lister.Lister): + """List baremetal nodes""" + + log = logging.getLogger(__name__ + ".ListBaremetal") + + def get_parser(self, prog_name): + parser = super(ListBaremetal, self).get_parser(prog_name) + parser.add_argument( + '--limit', + metavar='', + type=int, + help='Maximum number of nodes to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Baremetal API Service.' + ) + parser.add_argument( + '--marker', + metavar='', + help='Node UUID (for example, of the last node in the list from ' + 'a previous request). Returns the list of nodes after this ' + 'UUID.' + ) + parser.add_argument( + '--sort', + metavar="[:]", + help='Sort output by selected keys and directions(asc or desc) ' + '(default: asc), multiple keys and directions can be ' + 'specified separated by comma', + ) + parser.add_argument( + '--maintenance', + dest='maintenance', + action='store_true', + default=False, + help="List nodes in maintenance mode.", + ) + parser.add_argument( + '--associated', + dest='associated', + action='store_true', + default=False, + help="List only nodes associated with an instance." + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help="Show detailed information about the nodes." + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + client = self.app.client_manager.baremetal + + columns = res_fields.NODE_RESOURCE + + params = {} + if parsed_args.limit is not None and parsed_args.limit < 0: + raise exc.CommandError( + _('Expected non-negative --limit, got %s') % + parsed_args.limit) + params['limit'] = parsed_args.limit + params['marker'] = parsed_args.marker + if parsed_args.associated: + params['associated'] = parsed_args.associated + if parsed_args.maintenance: + params['maintenance'] = parsed_args.maintenance + + if parsed_args.long: + columns = res_fields.NODE_DETAILED_RESOURCE + params['detail'] = parsed_args.long + + self.log.debug("params(%s)" % params) + data = client.node.list(**params) + + data = oscutils.sort_items(data, parsed_args.sort) + + return (columns.labels, + (oscutils.get_item_properties(s, columns.fields, formatters={ + 'Properties': oscutils.format_dict},) for s in data)) + + +class SetBaremetal(command.Command): + """Set baremetal properties""" + + log = logging.getLogger(__name__ + ".SetBaremetal") + + def get_parser(self, prog_name): + parser = super(SetBaremetal, self).get_parser(prog_name) + + parser.add_argument( + 'node', + metavar='', + help="Name or UUID of the node." + ) + parser.add_argument( + "--property", + metavar="", + action='append', + help='Property to add to this baremetal host ' + '(repeat option to set multiple properties)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + + properties = [] + if parsed_args.property: + properties = utils.args_array_to_patch( + 'add', parsed_args.property) + baremetal_client.node.update(parsed_args.node, properties) + + +class ShowBaremetal(show.ShowOne): + """Show baremetal node details""" + + log = logging.getLogger(__name__ + ".ShowBaremetal") + LONG_FIELDS = [ + 'extra', + 'properties', + 'ports', + 'driver_info', + 'driver_internal_info', + 'instance_info', + ] + + def get_parser(self, prog_name): + parser = super(ShowBaremetal, self).get_parser(prog_name) + parser.add_argument( + "node", + metavar="", + help="Name or UUID of the node (or instance UUID if --instance " + "is specified)") + parser.add_argument( + '--instance', + dest='instance_uuid', + action='store_true', + default=False, + help=' is an instance UUID.') + parser.add_argument( + '--long', + action='store_true') + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + if parsed_args.instance_uuid: + node = baremetal_client.node.get_by_instance_uuid( + parsed_args.node)._info + else: + node = oscutils.find_resource(baremetal_client.node, + parsed_args.node)._info + node.pop("links", None) + if not parsed_args.long: + for field in self.LONG_FIELDS: + node.pop(field, None) + + return zip(*sorted(six.iteritems(node))) + + +class UnsetBaremetal(command.Command): + """Unset baremetal properties""" + log = logging.getLogger(__name__ + ".UnsetBaremetal") + + def get_parser(self, prog_name): + parser = super(UnsetBaremetal, self).get_parser(prog_name) + + parser.add_argument( + 'node', + metavar='', + help="Name or UUID of the node." + ) + parser.add_argument( + '--property', + metavar='', + action='append', + help='Property to unset on this baremetal host ' + '(repeat option to unset multiple properties)', + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + + if not parsed_args.node and not parsed_args.property: + return + + patch = utils.args_array_to_patch('remove', parsed_args.property) + baremetal_client.node.update(parsed_args.node, patch) diff --git a/ironicclient/tests/unit/osc/__init__.py b/ironicclient/tests/unit/osc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ironicclient/tests/unit/osc/fakes.py b/ironicclient/tests/unit/osc/fakes.py new file mode 100644 index 000000000..219c5b01c --- /dev/null +++ b/ironicclient/tests/unit/osc/fakes.py @@ -0,0 +1,58 @@ +# +# Copyright 2015 Red Hat, Inc. +# +# 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 sys + +import six + + +AUTH_TOKEN = "foobar" +AUTH_URL = "http://0.0.0.0" + + +class FakeApp(object): + def __init__(self): + _stdout = None + self.client_manager = None + self.stdin = sys.stdin + self.stdout = _stdout or sys.stdout + self.stderr = sys.stderr + self.restapi = None + + +class FakeClientManager(object): + def __init__(self): + self.identity = None + self.auth_ref = None + + +class FakeResource(object): + def __init__(self, manager, info, loaded=False): + self.__name__ = type(self).__name__ + self.manager = manager + self._info = info + self._add_details(info) + self._loaded = loaded + + def _add_details(self, info): + for (k, v) in six.iteritems(info): + setattr(self, k, v) + + def __repr__(self): + reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and + k != 'manager') + info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + return "<%s %s>" % (self.__class__.__name__, info) diff --git a/ironicclient/tests/unit/osc/v1/__init__.py b/ironicclient/tests/unit/osc/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py new file mode 100644 index 000000000..f4d209452 --- /dev/null +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -0,0 +1,52 @@ +# +# Copyright 2015 Red Hat, Inc. +# +# 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 openstackclient.tests import utils + +from ironicclient.tests.unit.osc import fakes + +baremetal_uuid = 'xxx-xxxxxx-xxxx' +baremetal_name = 'fake name' +baremetal_instance_uuid = 'yyy-yyyyyy-yyyy' +baremetal_power_state = None +baremetal_provision_state = None +baremetal_maintenance = False + +BAREMETAL = { + 'uuid': baremetal_uuid, + 'name': baremetal_name, + 'instance_uuid': baremetal_instance_uuid, + 'power_state': baremetal_power_state, + 'provision_state': baremetal_provision_state, + 'maintenance': baremetal_maintenance, + 'links': [] +} + + +class TestBaremetal(utils.TestCommand): + + def setUp(self): + super(TestBaremetal, self).setUp() + + self.app.client_manager.auth_ref = mock.Mock(auth_token="TOKEN") + self.app.client_manager.baremetal = mock.Mock() + + +class FakeBaremetalResource(fakes.FakeResource): + + def get_keys(self): + return {'property': 'value'} diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal.py b/ironicclient/tests/unit/osc/v1/test_baremetal.py new file mode 100644 index 000000000..cf7479e3f --- /dev/null +++ b/ironicclient/tests/unit/osc/v1/test_baremetal.py @@ -0,0 +1,515 @@ +# +# Copyright 2015 Red Hat, Inc. +# +# 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 copy + +from openstackclient.tests import utils as oscutils + +from ironicclient.osc.v1 import baremetal +from ironicclient.tests.unit.osc.v1 import fakes as baremetal_fakes + + +class TestBaremetal(baremetal_fakes.TestBaremetal): + + def setUp(self): + super(TestBaremetal, self).setUp() + + # Get a shortcut to the FlavorManager Mock + self.baremetal_mock = self.app.client_manager.baremetal + self.baremetal_mock.reset_mock() + + +class TestBaremetalCreate(TestBaremetal): + def setUp(self): + super(TestBaremetalCreate, self).setUp() + + self.baremetal_mock.node.create.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.BAREMETAL), + loaded=True, + )) + + # Get the command object to test + self.cmd = baremetal.CreateBaremetal(self.app, None) + self.arglist = ['--driver', 'fake_driver'] + self.verifylist = [('driver', 'fake_driver')] + self.collist = ( + 'instance_uuid', + 'maintenance', + 'name', + 'power_state', + 'provision_state', + 'uuid' + ) + self.datalist = ( + 'yyy-yyyyyy-yyyy', + baremetal_fakes.baremetal_maintenance, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_power_state, + baremetal_fakes.baremetal_provision_state, + baremetal_fakes.baremetal_uuid, + ) + self.actual_kwargs = { + 'driver': 'fake_driver', + } + + def check_with_options(self, addl_arglist, addl_verifylist, addl_kwargs): + arglist = copy.copy(self.arglist) + addl_arglist + verifylist = copy.copy(self.verifylist) + addl_verifylist + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = copy.copy(self.collist) + self.assertEqual(collist, columns) + + datalist = copy.copy(self.datalist) + self.assertEqual(datalist, tuple(data)) + + kwargs = copy.copy(self.actual_kwargs) + kwargs.update(addl_kwargs) + + self.baremetal_mock.node.create.assert_called_once_with(**kwargs) + + def test_baremetal_create_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_create_with_driver(self): + arglist = copy.copy(self.arglist) + + verifylist = copy.copy(self.verifylist) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = copy.copy(self.collist) + self.assertEqual(collist, columns) + + datalist = copy.copy(self.datalist) + self.assertEqual(datalist, tuple(data)) + + kwargs = copy.copy(self.actual_kwargs) + + self.baremetal_mock.node.create.assert_called_once_with(**kwargs) + + def test_baremetal_create_with_chassis(self): + self.check_with_options(['--chassis', 'chassis_uuid'], + [('chassis_uuid', 'chassis_uuid')], + {'chassis_uuid': 'chassis_uuid'}) + + def test_baremetal_create_with_driver_info(self): + self.check_with_options(['--driver-info', 'arg1=val1', + '--driver-info', 'arg2=val2'], + [('driver_info', + ['arg1=val1', + 'arg2=val2'])], + {'driver_info': { + 'arg1': 'val1', + 'arg2': 'val2'}}) + + def test_baremetal_create_with_properties(self): + self.check_with_options(['--property', 'arg1=val1', + '--property', 'arg2=val2'], + [('properties', + ['arg1=val1', + 'arg2=val2'])], + {'properties': { + 'arg1': 'val1', + 'arg2': 'val2'}}) + + def test_baremetal_create_with_extra(self): + self.check_with_options(['--extra', 'arg1=val1', + '--extra', 'arg2=val2'], + [('extra', + ['arg1=val1', + 'arg2=val2'])], + {'extra': { + 'arg1': 'val1', + 'arg2': 'val2'}}) + + def test_baremetal_create_with_uuid(self): + self.check_with_options(['--uuid', 'uuid'], + [('uuid', 'uuid')], + {'uuid': 'uuid'}) + + def test_baremetal_create_with_name(self): + self.check_with_options(['--name', 'name'], + [('name', 'name')], + {'name': 'name'}) + + +class TestBaremetalDelete(TestBaremetal): + def setUp(self): + super(TestBaremetalDelete, self).setUp() + + self.baremetal_mock.node.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.BAREMETAL), + loaded=True, + )) + + # Get the command object to test + self.cmd = baremetal.DeleteBaremetal(self.app, None) + + def test_baremetal_delete(self): + arglist = ['xxx-xxxxxx-xxxx'] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Set expected values + args = ['xxx-xxxxxx-xxxx'] + + self.baremetal_mock.node.delete.assert_called_with( + *args + ) + + +class TestBaremetalList(TestBaremetal): + + def setUp(self): + super(TestBaremetalList, self).setUp() + + self.baremetal_mock.node.list.return_value = [ + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.BAREMETAL), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = baremetal.ListBaremetal(self.app, None) + + def test_baremetal_list_no_options(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'detail': False, + 'marker': None, + 'limit': None, + } + + self.baremetal_mock.node.list.assert_called_with( + **kwargs + ) + + collist = ( + "UUID", + "Name", + "Instance UUID", + "Power State", + "Provisioning State", + "Maintenance" + ) + self.assertEqual(collist, columns) + datalist = (( + baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_instance_uuid, + baremetal_fakes.baremetal_power_state, + baremetal_fakes.baremetal_provision_state, + baremetal_fakes.baremetal_maintenance, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'detail': True, + 'marker': None, + 'limit': None, + } + + self.baremetal_mock.node.list.assert_called_with( + **kwargs + ) + + collist = ('Chassis UUID', 'Created At', 'Clean Step', + 'Console Enabled', 'Driver', 'Driver Info', + 'Driver Internal Info', 'Extra', 'Instance Info', + 'Instance UUID', 'Last Error', 'Maintenance', + 'Maintenance Reason', 'Power State', 'Properties', + 'Provisioning State', 'Provision Updated At', 'Reservation', + 'Target Power State', 'Target Provision State', + 'Updated At', 'Inspection Finished At', + 'Inspection Started At', 'UUID', 'Name') + self.assertEqual(collist, columns) + datalist = (( + '', + '', + '', + '', + '', + '', + '', + '', + '', + baremetal_fakes.baremetal_instance_uuid, + '', + baremetal_fakes.baremetal_maintenance, + '', + baremetal_fakes.baremetal_power_state, + '', + baremetal_fakes.baremetal_provision_state, + '', + '', + '', + '', + '', + '', + '', + baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name, + ), ) + self.assertEqual(datalist, tuple(data)) + + +class TestBaremetalSet(TestBaremetal): + def setUp(self): + super(TestBaremetalSet, self).setUp() + + self.baremetal_mock.node.update.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.BAREMETAL), + loaded=True, + )) + + # Get the command object to test + self.cmd = baremetal.SetBaremetal(self.app, None) + + def test_baremetal_set_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_set_one_property(self): + arglist = ['node_uuid', '--property', 'path/to/property=value'] + verifylist = [ + ('node', 'node_uuid'), + ('property', ['path/to/property=value']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.update.assert_called_once_with( + 'node_uuid', + [{'path': '/path/to/property', 'value': 'value', 'op': 'add'}]) + + def test_baremetal_set_multiple_properties(self): + arglist = [ + 'node_uuid', + '--property', 'path/to/property=value', + '--property', 'other/path=value2' + ] + verifylist = [ + ('node', 'node_uuid'), + ('property', + [ + 'path/to/property=value', + 'other/path=value2', + ]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.update.assert_called_once_with( + 'node_uuid', + [{'path': '/path/to/property', 'value': 'value', 'op': 'add'}, + {'path': '/other/path', 'value': 'value2', 'op': 'add'}] + ) + + +class TestBaremetalShow(TestBaremetal): + def setUp(self): + super(TestBaremetalShow, self).setUp() + + self.baremetal_mock.node.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.BAREMETAL), + loaded=True, + )) + + self.baremetal_mock.node.get_by_instance_uuid.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.BAREMETAL), + loaded=True, + )) + + # Get the command object to test + self.cmd = baremetal.ShowBaremetal(self.app, None) + + def test_baremetal_show(self): + arglist = ['xxx-xxxxxx-xxxx'] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + args = ['xxx-xxxxxx-xxxx'] + + self.baremetal_mock.node.get.assert_called_with( + *args + ) + + collist = ( + 'instance_uuid', + 'maintenance', + 'name', + 'power_state', + 'provision_state', + 'uuid' + ) + self.assertEqual(collist, columns) + datalist = ( + 'yyy-yyyyyy-yyyy', + baremetal_fakes.baremetal_maintenance, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_power_state, + baremetal_fakes.baremetal_provision_state, + baremetal_fakes.baremetal_uuid + ) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_show_no_node(self): + arglist = [] + verifylist = [] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_show_with_instance_uuid(self): + arglist = [ + 'xxx-xxxxxx-xxxx', + '--instance', + ] + + verifylist = [ + ('instance_uuid', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + args = ['xxx-xxxxxx-xxxx'] + + self.baremetal_mock.node.get_by_instance_uuid.assert_called_with( + *args + ) + + +class TestBaremetalUnset(TestBaremetal): + def setUp(self): + super(TestBaremetalUnset, self).setUp() + + self.baremetal_mock.node.update.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.BAREMETAL), + loaded=True, + )) + + # Get the command object to test + self.cmd = baremetal.UnsetBaremetal(self.app, None) + + def test_baremetal_unset_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_unset_one_property(self): + arglist = ['node_uuid', '--property', 'path/to/property'] + verifylist = [('node', 'node_uuid'), + ('property', ['path/to/property'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.update.assert_called_once_with( + 'node_uuid', + [{'path': '/path/to/property', 'op': 'remove'}]) + + def test_baremetal_unset_multiple_properties(self): + arglist = ['node_uuid', + '--property', 'path/to/property', + '--property', 'other/path'] + verifylist = [('node', 'node_uuid'), + ('property', + ['path/to/property', + 'other/path'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.update.assert_called_once_with( + 'node_uuid', + [{'path': '/path/to/property', 'op': 'remove'}, + {'path': '/other/path', 'op': 'remove'}] + ) diff --git a/requirements.txt b/requirements.txt index bf9ea9030..b4340c5ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,10 +5,12 @@ pbr<2.0,>=1.4 anyjson>=0.3.3 appdirs>=1.3.0 # MIT License dogpile.cache>=0.5.4 +cliff>=1.14.0 # Apache-2.0 httplib2>=0.7.5 lxml>=2.3 oslo.i18n>=1.5.0 # Apache-2.0 oslo.utils>=2.0.0 # Apache-2.0 PrettyTable<0.8,>=0.7 python-keystoneclient>=1.6.0 +python-openstackclient>=1.5.0 six>=1.9.0 diff --git a/setup.cfg b/setup.cfg index 01ef65448..43b5cf24e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,6 +25,17 @@ packages = ironicclient console_scripts = ironic = ironicclient.shell:main +openstack.cli.extension = + baremetal = ironicclient.osc.plugin + +openstack.baremetal.v1 = + baremetal_create = ironicclient.osc.v1.baremetal:CreateBaremetal + baremetal_delete = ironicclient.osc.v1.baremetal:DeleteBaremetal + baremetal_list = ironicclient.osc.v1.baremetal:ListBaremetal + baremetal_set = ironicclient.osc.v1.baremetal:SetBaremetal + baremetal_show = ironicclient.osc.v1.baremetal:ShowBaremetal + baremetal_unset = ironicclient.osc.v1.baremetal:UnsetBaremetal + [pbr] autodoc_index_modules = True