diff options
author | Hironori Shiina <shiina.hironori@jp.fujitsu.com> | 2017-07-14 13:58:48 +0900 |
---|---|---|
committer | Hironori Shiina <shiina.hironori@jp.fujitsu.com> | 2017-07-14 13:58:48 +0900 |
commit | 1a5a04102ee3494555db39e12ff27ef6a4b79f0b (patch) | |
tree | d774ea120616318c52409407de446b1eb3fa4f20 | |
parent | 911b8cc0bb3fade170cee082d04cba3e07b8d58b (diff) | |
download | python-ironicclient-1a5a04102ee3494555db39e12ff27ef6a4b79f0b.tar.gz |
Add Ironic CLI commands for volume target
This patch adds the following commands for volume target.
- ironic volume-target-list
- ironic volume-target-show
- ironic volume-target-create
- ironic volume-target-update
- ironic volume-target-delete
Co-Authored-By: Satoru Moriya <satoru.moriya.br@hitachi.com>
Co-Authored-By: Stephane Miller <stephane@alum.mit.edu>
Change-Id: I156e634fbfb9b782fdcbc51cb8c167a38ffe2bfa
Partial-Bug: 1526231
-rw-r--r-- | ironicclient/tests/unit/v1/test_volume_target_shell.py | 288 | ||||
-rw-r--r-- | ironicclient/v1/shell.py | 2 | ||||
-rw-r--r-- | ironicclient/v1/volume_target_shell.py | 216 | ||||
-rw-r--r-- | releasenotes/notes/add-volume-target-cli-e062303f4b3b40ef.yaml | 10 |
4 files changed, 516 insertions, 0 deletions
diff --git a/ironicclient/tests/unit/v1/test_volume_target_shell.py b/ironicclient/tests/unit/v1/test_volume_target_shell.py new file mode 100644 index 0000000..15e5c4b --- /dev/null +++ b/ironicclient/tests/unit/v1/test_volume_target_shell.py @@ -0,0 +1,288 @@ +# Copyright 2017 Hitachi, Ltd. +# +# 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_target_shell as vt_shell + + +class Volume_TargetShellTest(utils.BaseTestCase): + + def test_volume_target_show(self): + actual = {} + fake_print_dict = lambda data, *args, **kwargs: actual.update(data) + with mock.patch.object(cliutils, 'print_dict', fake_print_dict): + volume_target = object() + vt_shell._print_volume_target_show(volume_target) + exp = ['created_at', 'extra', 'node_uuid', 'volume_type', + 'updated_at', 'uuid', 'properties', 'boot_index', + 'volume_id'] + act = actual.keys() + self.assertEqual(sorted(exp), sorted(act)) + + def test_do_volume_target_show(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = 'volume_target_uuid' + args.fields = None + args.json = False + + vt_shell.do_volume_target_show(client_mock, args) + client_mock.volume_target.get.assert_called_once_with( + 'volume_target_uuid', fields=None) + + def test_do_volume_target_show_space_uuid(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = ' ' + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_show, + client_mock, args) + + def test_do_volume_target_show_empty_uuid(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = '' + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_show, + client_mock, args) + + def test_do_volume_target_show_fields(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = 'volume_target_uuid' + args.fields = [['uuid', 'boot_index']] + args.json = False + vt_shell.do_volume_target_show(client_mock, args) + client_mock.volume_target.get.assert_called_once_with( + 'volume_target_uuid', fields=['uuid', 'boot_index']) + + def test_do_volume_target_show_invalid_fields(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = 'volume_target_uuid' + args.fields = [['foo', 'bar']] + args.json = False + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_show, + client_mock, args) + + def test_do_volume_target_update(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = 'volume_target_uuid' + args.op = 'add' + args.attributes = [['arg1=val1', 'arg2=val2']] + args.json = False + + vt_shell.do_volume_target_update(client_mock, args) + patch = commonutils.args_array_to_patch(args.op, args.attributes[0]) + client_mock.volume_target.update.assert_called_once_with( + 'volume_target_uuid', patch) + + def test_do_volume_target_update_wrong_op(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = 'volume_target_uuid' + args.op = 'foo' + args.attributes = [['arg1=val1', 'arg2=val2']] + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_update, + client_mock, args) + self.assertFalse(client_mock.volume_target.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_target_list(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args() + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with(detail=False) + + def test_do_volume_target_list_detail(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(detail=True) + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with(detail=True) + + def test_do_volume_target_list_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='uuid', detail=False) + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with( + sort_key='uuid', detail=False) + + def test_do_volume_target_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, + vt_shell.do_volume_target_list, + client_mock, args) + self.assertFalse(client_mock.volume_target.list.called) + + def test_do_volume_target_list_detail_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='uuid', detail=True) + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with( + sort_key='uuid', detail=True) + + def test_do_volume_target_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, + vt_shell.do_volume_target_list, + client_mock, args) + self.assertFalse(client_mock.volume_target.list.called) + + def test_do_volume_target_list_fields(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(fields=[['uuid', 'boot_index']]) + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with( + fields=['uuid', 'boot_index'], detail=False) + + def test_do_volume_target_list_invalid_fields(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(fields=[['foo', 'bar']]) + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_list, + client_mock, args) + + def test_do_volume_target_list_sort_dir(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_dir='desc', detail=False) + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with( + sort_dir='desc', detail=False) + + def test_do_volume_target_list_detail_sort_dir(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_dir='asc', detail=True) + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with( + sort_dir='asc', detail=True) + + def test_do_volume_target_wrong_sort_dir(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_dir='abc', detail=False) + + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_list, + client_mock, args) + self.assertFalse(client_mock.volume_target.list.called) + + def test_do_volume_target_create(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.json = False + vt_shell.do_volume_target_create(client_mock, args) + client_mock.volume_target.create.assert_called_once_with() + + def test_do_volume_target_create_with_uuid(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.uuid = uuidutils.generate_uuid() + args.json = False + + vt_shell.do_volume_target_create(client_mock, args) + client_mock.volume_target.create.assert_called_once_with( + uuid=args.uuid) + + def test_do_volume_target_create_valid_fields_values(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_type = 'volume_type' + args.properties = ["key1=val1", "key2=val2"] + args.boot_index = 100 + args.node_uuid = 'uuid' + args.volume_id = 'volume_id' + args.extra = ["key1=val1", "key2=val2"] + args.json = False + vt_shell.do_volume_target_create(client_mock, args) + client_mock.volume_target.create.assert_called_once_with( + volume_type='volume_type', + properties={'key1': 'val1', 'key2': 'val2'}, + boot_index=100, node_uuid='uuid', volume_id='volume_id', + extra={'key1': 'val1', 'key2': 'val2'}) + + def test_do_volume_target_create_invalid_extra_fields(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_type = 'volume_type' + args.properties = ["key1=val1", "key2=val2"] + args.boot_index = 100 + args.node_uuid = 'uuid' + args.volume_id = 'volume_id' + args.extra = ["foo"] + args.json = False + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_create, + client_mock, args) + + def test_do_volume_target_delete(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = ['volume_target_uuid'] + vt_shell.do_volume_target_delete(client_mock, args) + client_mock.volume_target.delete.assert_called_once_with( + 'volume_target_uuid') + + def test_do_volume_target_delete_multi(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = ['uuid1', 'uuid2'] + vt_shell.do_volume_target_delete(client_mock, args) + self.assertEqual([mock.call('uuid1'), mock.call('uuid2')], + client_mock.volume_target.delete.call_args_list) + + def test_do_volume_target_delete_multi_error(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = ['uuid1', 'uuid2'] + client_mock.volume_target.delete.side_effect = [ + exceptions.ClientException('error'), None] + self.assertRaises(exceptions.ClientException, + vt_shell.do_volume_target_delete, + client_mock, args) + self.assertEqual([mock.call('uuid1'), mock.call('uuid2')], + client_mock.volume_target.delete.call_args_list) diff --git a/ironicclient/v1/shell.py b/ironicclient/v1/shell.py index 4a9742c..a31422f 100644 --- a/ironicclient/v1/shell.py +++ b/ironicclient/v1/shell.py @@ -19,6 +19,7 @@ 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 +from ironicclient.v1 import volume_target_shell COMMAND_MODULES = [ chassis_shell, @@ -28,6 +29,7 @@ COMMAND_MODULES = [ driver_shell, create_resources_shell, volume_connector_shell, + volume_target_shell, ] diff --git a/ironicclient/v1/volume_target_shell.py b/ironicclient/v1/volume_target_shell.py new file mode 100644 index 0000000..4d145e6 --- /dev/null +++ b/ironicclient/v1/volume_target_shell.py @@ -0,0 +1,216 @@ +# Copyright 2017 Hitachi, Ltd. +# +# 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_target_show(volume_target, fields=None, json=False): + if fields is None: + fields = res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields + + data = dict([(f, getattr(volume_target, f, '')) for f in fields]) + cliutils.print_dict(data, wrap=72, json_flag=json) + + +@cliutils.arg( + 'volume_target', + metavar='<id>', + help=_("UUID of the volume target.")) +@cliutils.arg( + '--fields', + nargs='+', + dest='fields', + metavar='<field>', + action='append', + default=[], + help=_("One or more volume target fields. Only these fields will be " + "fetched from the server.")) +def do_volume_target_show(cc, args): + """Show detailed information about a volume target.""" + fields = args.fields[0] if args.fields else None + utils.check_for_invalid_fields( + fields, res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields) + utils.check_empty_arg(args.volume_target, '<id>') + volume_target = cc.volume_target.get(args.volume_target, fields=fields) + _print_volume_target_show(volume_target, fields=fields, json=args.json) + + +@cliutils.arg( + '--detail', + dest='detail', + action='store_true', + default=False, + help=_("Show detailed information about volume targets.")) +@cliutils.arg( + '-n', '--node', + metavar='<node>', + help=_('Only list volume targets of this node (name or UUID)')) +@cliutils.arg( + '--limit', + metavar='<limit>', + type=int, + help=_('Maximum number of volume targets to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Baremetal API Service.')) +@cliutils.arg( + '--marker', + metavar='<volume target>', + help=_('Volume target UUID (for example, of the last volume target in ' + 'the list from a previous request). Returns the list of volume ' + 'targets after this UUID.')) +@cliutils.arg( + '--sort-key', + metavar='<field>', + help=_('Volume target field that will be used for sorting.')) +@cliutils.arg( + '--sort-dir', + metavar='<direction>', + choices=['asc', 'desc'], + help=_('Sort direction: "asc" (the default) or "desc".')) +@cliutils.arg( + '--fields', + nargs='+', + dest='fields', + metavar='<field>', + action='append', + default=[], + help=_("One or more volume target fields. Only these fields will be " + "fetched from the server. Can not be used when '--detail' is " + "specified.")) +def do_volume_target_list(cc, args): + """List the volume targets.""" + params = {} + + if args.node is not None: + params['node'] = args.node + + if args.detail: + fields = res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields + field_labels = res_fields.VOLUME_TARGET_DETAILED_RESOURCE.labels + elif args.fields: + utils.check_for_invalid_fields( + args.fields[0], + res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields) + resource = res_fields.Resource(args.fields[0]) + fields = resource.fields + field_labels = resource.labels + else: + fields = res_fields.VOLUME_TARGET_RESOURCE.fields + field_labels = res_fields.VOLUME_TARGET_RESOURCE.labels + + sort_fields = res_fields.VOLUME_TARGET_DETAILED_RESOURCE.sort_fields + sort_field_labels = ( + res_fields.VOLUME_TARGET_DETAILED_RESOURCE.sort_labels) + + params.update(utils.common_params_for_list(args, + sort_fields, + sort_field_labels)) + + volume_target = cc.volume_target.list(**params) + cliutils.print_list(volume_target, fields, field_labels=field_labels, + sortby_index=None, json_flag=args.json) + + +@cliutils.arg( + '-e', '--extra', + metavar="<key=value>", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) +@cliutils.arg( + '-n', '--node', + dest='node_uuid', + metavar='<node>', + required=True, + help=_('UUID of the node that this volume target belongs to.')) +@cliutils.arg( + '-t', '--type', + metavar="<volume type>", + required=True, + help=_("Type of the volume target, e.g. 'iscsi', 'fibre_channel', 'rbd'.")) +@cliutils.arg( + '-p', '--properties', + metavar="<key=value>", + action='append', + help=_("Key/value property related to the type of this volume " + "target. Can be specified multiple times.")) +@cliutils.arg( + '-b', '--boot-index', + metavar="<boot index>", + required=True, + help=_("Boot index of the volume target.")) +@cliutils.arg( + '-i', '--volume_id', + metavar="<volume id>", + required=True, + help=_("ID of the volume associated with this target.")) +@cliutils.arg( + '-u', '--uuid', + metavar='<uuid>', + help=_("UUID of the volume target.")) +def do_volume_target_create(cc, args): + """Create a new volume target.""" + field_list = ['extra', 'volume_type', 'properties', + 'boot_index', 'node_uuid', 'volume_id', '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, 'properties') + fields = utils.args_array_to_dict(fields, 'extra') + volume_target = cc.volume_target.create(**fields) + + data = dict([(f, getattr(volume_target, f, '')) for f in field_list]) + cliutils.print_dict(data, wrap=72, json_flag=args.json) + + +@cliutils.arg('volume_target', metavar='<volume target>', nargs='+', + help=_("UUID of the volume target.")) +def do_volume_target_delete(cc, args): + """Delete a volume target.""" + failures = [] + for vt in args.volume_target: + try: + cc.volume_target.delete(vt) + print(_('Deleted volume target %s') % vt) + except exceptions.ClientException as e: + failures.append(_("Failed to delete volume target %(vt)s: " + "%(error)s") + % {'vt': vt, 'error': e}) + if failures: + raise exceptions.ClientException("\n".join(failures)) + + +@cliutils.arg('volume_target', metavar='<volume target>', + help=_("UUID of the volume target.")) +@cliutils.arg( + 'op', + metavar='<op>', + choices=['add', 'replace', 'remove'], + help=_("Operation: 'add', 'replace', or 'remove'.")) +@cliutils.arg( + 'attributes', + metavar='<path=value>', + nargs='+', + action='append', + default=[], + help=_("Attribute to add, replace, or remove. Can be specified multiple " + "times. For 'remove', only <path> is necessary.")) +def do_volume_target_update(cc, args): + """Update information about a volume target.""" + patch = utils.args_array_to_patch(args.op, args.attributes[0]) + volume_target = cc.volume_target.update(args.volume_target, patch) + _print_volume_target_show(volume_target, json=args.json) diff --git a/releasenotes/notes/add-volume-target-cli-e062303f4b3b40ef.yaml b/releasenotes/notes/add-volume-target-cli-e062303f4b3b40ef.yaml new file mode 100644 index 0000000..3ce571d --- /dev/null +++ b/releasenotes/notes/add-volume-target-cli-e062303f4b3b40ef.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Adds these Ironic CLI commands for volume target resources: + + * ``ironic volume-target-create`` + * ``ironic volume-target-list`` + * ``ironic volume-target-show`` + * ``ironic volume-target-update`` + * ``ironic volume-target-delete`` |