From 911b8cc0bb3fade170cee082d04cba3e07b8d58b Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Wed, 12 Jul 2017 11:41:31 +0900 Subject: [PATCH] Add Ironic CLI commands for volume connector This patch adds the following commands for volume connector. - ironic volume-connector-list - ironic volume-connector-show - ironic volume-connector-create - ironic volume-connector-update - ironic volume-connector-delete Co-Authored-By: Tomoki Sekiyama Co-Authored-By: Satoru Moriya Co-Authored-By: Stephane Miller Change-Id: I9ebbb4bc82afa001d2cf53c834e8efd320b7ba16 Partial-Bug: 1526231 --- .../unit/v1/test_volume_connector_shell.py | 284 ++++++++++++++++++ ironicclient/v1/shell.py | 2 + ironicclient/v1/volume_connector_shell.py | 209 +++++++++++++ ...volume-connector-cli-873090474d5e41b8.yaml | 10 + 4 files changed, 505 insertions(+) create mode 100644 ironicclient/tests/unit/v1/test_volume_connector_shell.py create mode 100644 ironicclient/v1/volume_connector_shell.py create mode 100644 releasenotes/notes/add-volume-connector-cli-873090474d5e41b8.yaml diff --git a/ironicclient/tests/unit/v1/test_volume_connector_shell.py b/ironicclient/tests/unit/v1/test_volume_connector_shell.py new file mode 100644 index 000000000..495cc7750 --- /dev/null +++ b/ironicclient/tests/unit/v1/test_volume_connector_shell.py @@ -0,0 +1,284 @@ +# Copyright 2017 Hitachi Data Systems +# +# 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 oslo_utils import uuidutils + +from ironicclient.common.apiclient import exceptions +from ironicclient.common import cliutils +from ironicclient.common import utils as commonutils +from ironicclient.tests.unit import utils +import ironicclient.v1.volume_connector_shell as vc_shell + + +class Volume_ConnectorShellTest(utils.BaseTestCase): + + def test_volume_connector_show(self): + actual = {} + fake_print_dict = lambda data, *args, **kwargs: actual.update(data) + with mock.patch.object(cliutils, 'print_dict', fake_print_dict): + volume_connector = object() + vc_shell._print_volume_connector_show(volume_connector) + exp = ['created_at', 'extra', 'node_uuid', 'type', 'updated_at', + 'uuid', 'connector_id'] + act = actual.keys() + self.assertEqual(sorted(exp), sorted(act)) + + def test_do_volume_connector_show(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = 'volume_connector_uuid' + args.fields = None + args.json = False + + vc_shell.do_volume_connector_show(client_mock, args) + client_mock.volume_connector.get.assert_called_once_with( + 'volume_connector_uuid', fields=None) + + def test_do_volume_connector_show_space_uuid(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = ' ' + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_show, + client_mock, args) + + def test_do_volume_connector_show_empty_uuid(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = '' + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_show, + client_mock, args) + + def test_do_volume_connector_show_fields(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = 'volume_connector_uuid' + args.fields = [['uuid', 'connector_id']] + args.json = False + vc_shell.do_volume_connector_show(client_mock, args) + client_mock.volume_connector.get.assert_called_once_with( + 'volume_connector_uuid', fields=['uuid', 'connector_id']) + + def test_do_volume_connector_show_invalid_fields(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = 'volume_connector_uuid' + args.fields = [['foo', 'bar']] + args.json = False + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_show, + client_mock, args) + + def test_do_volume_connector_update(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = 'volume_connector_uuid' + args.op = 'add' + args.attributes = [['arg1=val1', 'arg2=val2']] + args.json = False + + vc_shell.do_volume_connector_update(client_mock, args) + patch = commonutils.args_array_to_patch(args.op, args.attributes[0]) + client_mock.volume_connector.update.assert_called_once_with( + 'volume_connector_uuid', patch) + + def test_do_volume_connector_update_wrong_op(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = 'volume_connector_uuid' + args.op = 'foo' + args.attributes = [['arg1=val1', 'arg2=val2']] + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_update, + client_mock, args) + self.assertFalse(client_mock.volume_connector.update.called) + + def _get_client_mock_args(self, node=None, marker=None, limit=None, + sort_dir=None, sort_key=None, detail=False, + fields=None, json=False): + args = mock.MagicMock(spec=True) + args.node = node + args.marker = marker + args.limit = limit + args.sort_dir = sort_dir + args.sort_key = sort_key + args.detail = detail + args.fields = fields + args.json = json + + return args + + def test_do_volume_connector_list(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args() + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with(detail=False) + + def test_do_volume_connector_list_detail(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(detail=True) + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with(detail=True) + + def test_do_volume_connector_list_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='uuid', + detail=False) + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with( + sort_key='uuid', detail=False) + + def test_do_volume_connector_list_wrong_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='node_uuid', detail=False) + + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_list, + client_mock, args) + self.assertFalse(client_mock.volume_connector.list.called) + + def test_do_volume_connector_list_detail_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='uuid', + detail=True) + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with( + sort_key='uuid', detail=True) + + def test_do_volume_connector_list_detail_wrong_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='node_uuid', + detail=True) + + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_list, + client_mock, args) + self.assertFalse(client_mock.volume_connector.list.called) + + def test_do_volume_connector_list_fields(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(fields=[['uuid', 'connector_id']]) + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with( + fields=['uuid', 'connector_id'], detail=False) + + def test_do_volume_connector_list_invalid_fields(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(fields=[['foo', 'bar']]) + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_list, + client_mock, args) + + def test_do_volume_connector_list_sort_dir(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_dir='desc', detail=False) + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with( + sort_dir='desc', detail=False) + + def test_do_volume_connector_list_detail_sort_dir(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_dir='asc', detail=True) + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with( + sort_dir='asc', detail=True) + + def test_do_volume_connector_wrong_sort_dir(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_dir='abc', detail=False) + + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_list, + client_mock, args) + self.assertFalse(client_mock.volume_connector.list.called) + + def test_do_volume_connector_create(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.json = False + vc_shell.do_volume_connector_create(client_mock, args) + client_mock.volume_connector.create.assert_called_once_with() + + def test_do_volume_connector_create_with_uuid(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.uuid = uuidutils.generate_uuid() + args.json = False + + vc_shell.do_volume_connector_create(client_mock, args) + client_mock.volume_connector.create.assert_called_once_with( + uuid=args.uuid) + + def test_do_volume_connector_create_valid_fields_values(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.type = 'type' + args.connector_id = 'connector_id' + args.node_uuid = 'uuid' + args.extra = ["key1=val1", "key2=val2"] + args.json = False + vc_shell.do_volume_connector_create(client_mock, args) + client_mock.volume_connector.create.assert_called_once_with( + type='type', connector_id='connector_id', node_uuid='uuid', + extra={'key1': 'val1', 'key2': 'val2'}) + + def test_do_volume_connector_create_invalid_extra_fields(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.type = 'type' + args.connector_id = 'connector_id' + args.node_uuid = 'uuid' + args.extra = ["foo"] + args.json = False + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_create, + client_mock, args) + + def test_do_volume_connector_delete(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = ['volume_connector_uuid'] + vc_shell.do_volume_connector_delete(client_mock, args) + client_mock.volume_connector.delete.assert_called_once_with( + 'volume_connector_uuid') + + def test_do_volume_connector_delete_multi(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = ['uuid1', 'uuid2'] + vc_shell.do_volume_connector_delete(client_mock, args) + self.assertEqual([mock.call('uuid1'), mock.call('uuid2')], + client_mock.volume_connector.delete.call_args_list) + + def test_do_volume_connector_delete_multi_error(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = ['uuid1', 'uuid2'] + client_mock.volume_connector.delete.side_effect = [ + exceptions.ClientException('error'), None] + self.assertRaises(exceptions.ClientException, + vc_shell.do_volume_connector_delete, + client_mock, args) + self.assertEqual([mock.call('uuid1'), mock.call('uuid2')], + client_mock.volume_connector.delete.call_args_list) diff --git a/ironicclient/v1/shell.py b/ironicclient/v1/shell.py index 81ee51d33..4a9742c7c 100644 --- a/ironicclient/v1/shell.py +++ b/ironicclient/v1/shell.py @@ -18,6 +18,7 @@ from ironicclient.v1 import driver_shell from ironicclient.v1 import node_shell from ironicclient.v1 import port_shell from ironicclient.v1 import portgroup_shell +from ironicclient.v1 import volume_connector_shell COMMAND_MODULES = [ chassis_shell, @@ -26,6 +27,7 @@ COMMAND_MODULES = [ portgroup_shell, driver_shell, create_resources_shell, + volume_connector_shell, ] diff --git a/ironicclient/v1/volume_connector_shell.py b/ironicclient/v1/volume_connector_shell.py new file mode 100644 index 000000000..c6d3fa04b --- /dev/null +++ b/ironicclient/v1/volume_connector_shell.py @@ -0,0 +1,209 @@ +# Copyright 2017 Hitachi Data Systems +# +# 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 ironicclient.common.apiclient import exceptions +from ironicclient.common import cliutils +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient.v1 import resource_fields as res_fields + + +def _print_volume_connector_show(volume_connector, fields=None, json=False): + if fields is None: + fields = res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields + + data = dict([(f, getattr(volume_connector, f, '')) for f in fields]) + cliutils.print_dict(data, wrap=72, json_flag=json) + + +@cliutils.arg( + 'volume_connector', + metavar='', + help=_("UUID of the volume connector.")) +@cliutils.arg( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + help=_("One or more volume connector fields. Only these fields will be " + "fetched from the server.")) +def do_volume_connector_show(cc, args): + """Show detailed information about a volume connector.""" + fields = args.fields[0] if args.fields else None + utils.check_for_invalid_fields( + fields, res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields) + utils.check_empty_arg(args.volume_connector, '') + volume_connector = cc.volume_connector.get(args.volume_connector, + fields=fields) + _print_volume_connector_show(volume_connector, fields=fields, + json=args.json) + + +@cliutils.arg( + '--detail', + dest='detail', + action='store_true', + default=False, + help=_("Show detailed information about volume connectors.")) +@cliutils.arg( + '-n', '--node', + metavar='', + help=_('Only list volume connectors of this node (name or UUID)')) +@cliutils.arg( + '--limit', + metavar='', + type=int, + help=_('Maximum number of volume connectors to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Baremetal API Service.')) +@cliutils.arg( + '--marker', + metavar='', + help=_('Volume connector UUID (for example, of the last volume connector ' + 'in the list from a previous request). Returns the list of volume ' + 'connectors after this UUID.')) +@cliutils.arg( + '--sort-key', + metavar='', + help=_('Volume connector field that will be used for sorting.')) +@cliutils.arg( + '--sort-dir', + metavar='', + choices=['asc', 'desc'], + help=_('Sort direction: "asc" (the default) or "desc".')) +@cliutils.arg( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + help=_("One or more volume connector fields. Only these fields will be " + "fetched from the server. Can not be used when '--detail' is " + "specified.")) +def do_volume_connector_list(cc, args): + """List the volume connectors.""" + params = {} + + if args.node is not None: + params['node'] = args.node + + if args.detail: + fields = res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields + field_labels = res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.labels + elif args.fields: + utils.check_for_invalid_fields( + args.fields[0], + res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields) + resource = res_fields.Resource(args.fields[0]) + fields = resource.fields + field_labels = resource.labels + else: + fields = res_fields.VOLUME_CONNECTOR_RESOURCE.fields + field_labels = res_fields.VOLUME_CONNECTOR_RESOURCE.labels + + sort_fields = res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.sort_fields + sort_field_labels = ( + res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.sort_labels) + + params.update(utils.common_params_for_list(args, + sort_fields, + sort_field_labels)) + + volume_connector = cc.volume_connector.list(**params) + cliutils.print_list(volume_connector, fields, + field_labels=field_labels, + sortby_index=None, + json_flag=args.json) + + +@cliutils.arg( + '-e', '--extra', + metavar="", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) +@cliutils.arg( + '-n', '--node', + dest='node_uuid', + metavar='', + required=True, + help=_('UUID of the node that this volume connector belongs to.')) +@cliutils.arg( + '-t', '--type', + metavar="", + required=True, + choices=['iqn', 'ip', 'mac', 'wwnn', 'wwpn'], + help=_("Type of the volume connector. Can be 'iqn', 'ip', 'mac', 'wwnn', " + "'wwpn'.")) +@cliutils.arg( + '-i', '--connector_id', + metavar="", + required=True, + help=_("ID of the Volume connector in the specified type.")) +@cliutils.arg( + '-u', '--uuid', + metavar='', + help=_("UUID of the volume connector.")) +def do_volume_connector_create(cc, args): + """Create a new volume connector.""" + field_list = ['extra', 'type', 'connector_id', 'node_uuid', 'uuid'] + fields = dict((k, v) for (k, v) in vars(args).items() + if k in field_list and not (v is None)) + fields = utils.args_array_to_dict(fields, 'extra') + volume_connector = cc.volume_connector.create(**fields) + + data = dict([(f, getattr(volume_connector, f, '')) for f in field_list]) + cliutils.print_dict(data, wrap=72, json_flag=args.json) + + +@cliutils.arg('volume_connector', metavar='', nargs='+', + help=_("UUID of the volume connector.")) +def do_volume_connector_delete(cc, args): + """Delete a volume connector.""" + failures = [] + for vc in args.volume_connector: + try: + cc.volume_connector.delete(vc) + print(_('Deleted volume connector %s') % vc) + except exceptions.ClientException as e: + failures.append(_("Failed to delete volume connector %(vc)s: " + "%(error)s") + % {'vc': vc, 'error': e}) + if failures: + raise exceptions.ClientException("\n".join(failures)) + + +@cliutils.arg('volume_connector', metavar='', + help=_("UUID of the volume connector.")) +@cliutils.arg( + 'op', + metavar='', + choices=['add', 'replace', 'remove'], + help=_("Operation: 'add', 'replace', or 'remove'.")) +@cliutils.arg( + 'attributes', + metavar='', + nargs='+', + action='append', + default=[], + help=_("Attribute to add, replace, or remove. Can be specified multiple " + "times. For 'remove', only is necessary.")) +def do_volume_connector_update(cc, args): + """Update information about a volume connector.""" + patch = utils.args_array_to_patch(args.op, args.attributes[0]) + volume_connector = cc.volume_connector.update(args.volume_connector, patch) + _print_volume_connector_show(volume_connector, json=args.json) diff --git a/releasenotes/notes/add-volume-connector-cli-873090474d5e41b8.yaml b/releasenotes/notes/add-volume-connector-cli-873090474d5e41b8.yaml new file mode 100644 index 000000000..9ca2b4f7b --- /dev/null +++ b/releasenotes/notes/add-volume-connector-cli-873090474d5e41b8.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Adds these Ironic CLI commands for volume connector resources: + + * ``ironic volume-connector-create`` + * ``ironic volume-connector-list`` + * ``ironic volume-connector-show`` + * ``ironic volume-connector-update`` + * ``ironic volume-connector-delete``