diff options
author | Mark Goddard <mark@stackhpc.com> | 2019-02-14 11:25:09 +0000 |
---|---|---|
committer | Mark Goddard <mark@stackhpc.com> | 2019-03-01 14:24:28 +0000 |
commit | cc3725342820862724a86e27d63190d276231141 (patch) | |
tree | 243b0c66883e93b85f7eb66db84c419c48ea37d9 | |
parent | fdba8ed994bcbf1b3fc82803ccec49faf505b081 (diff) | |
download | python-ironicclient-cc3725342820862724a86e27d63190d276231141.tar.gz |
Deploy templates: client support
Adds OSC support for the deploy templates API.
Change-Id: I0f2f37e840449ee41f747e2a43ed6f53c927094e
Depends-On: https://review.openstack.org/631845
Story: 1722275
Task: 28678
-rw-r--r-- | ironicclient/common/http.py | 2 | ||||
-rw-r--r-- | ironicclient/common/utils.py | 16 | ||||
-rw-r--r-- | ironicclient/osc/v1/baremetal_deploy_template.py | 345 | ||||
-rwxr-xr-x | ironicclient/osc/v1/baremetal_node.py | 10 | ||||
-rw-r--r-- | ironicclient/osc/v1/baremetal_port.py | 2 | ||||
-rw-r--r-- | ironicclient/tests/functional/osc/v1/base.py | 57 | ||||
-rw-r--r-- | ironicclient/tests/functional/osc/v1/test_baremetal_deploy_template_basic.py | 177 | ||||
-rw-r--r-- | ironicclient/tests/unit/osc/v1/fakes.py | 18 | ||||
-rw-r--r-- | ironicclient/tests/unit/osc/v1/test_baremetal_deploy_template.py | 450 | ||||
-rw-r--r-- | ironicclient/tests/unit/v1/test_deploy_template.py | 291 | ||||
-rw-r--r-- | ironicclient/v1/client.py | 3 | ||||
-rw-r--r-- | ironicclient/v1/deploy_template.py | 86 | ||||
-rw-r--r-- | ironicclient/v1/resource_fields.py | 19 | ||||
-rw-r--r-- | releasenotes/notes/deploy-templates-df354ce825b00430.yaml | 11 | ||||
-rw-r--r-- | setup.cfg | 6 |
15 files changed, 1484 insertions, 9 deletions
diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 57652bb..f930e21 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -43,7 +43,7 @@ from ironicclient import exc # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 54 +LAST_KNOWN_API_VERSION = 55 LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) LOG = logging.getLogger(__name__) diff --git a/ironicclient/common/utils.py b/ironicclient/common/utils.py index c09ae84..fac5278 100644 --- a/ironicclient/common/utils.py +++ b/ironicclient/common/utils.py @@ -411,3 +411,19 @@ def poll(timeout, poll_interval, poll_delay_function, timeout_message): count += 1 raise exc.StateTransitionTimeout(timeout_message) + + +def handle_json_arg(json_arg, info_desc): + """Read a JSON argument from stdin, file or string. + + :param json_arg: May be a file name containing the JSON, a JSON string, or + '-' indicating that the argument should be read from standard input. + :param info_desc: A string description of the desired information + :returns: A list or dictionary parsed from JSON. + :raises: InvalidAttribute if the argument cannot be parsed. + """ + if json_arg == '-': + json_arg = get_from_stdin(info_desc) + if json_arg: + json_arg = handle_json_or_file_arg(json_arg) + return json_arg diff --git a/ironicclient/osc/v1/baremetal_deploy_template.py b/ironicclient/osc/v1/baremetal_deploy_template.py new file mode 100644 index 0000000..3a1a8c3 --- /dev/null +++ b/ironicclient/osc/v1/baremetal_deploy_template.py @@ -0,0 +1,345 @@ +# 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 itertools +import json +import logging + +from osc_lib.command import command +from osc_lib 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 + + +_DEPLOY_STEPS_HELP = _( + "The deploy steps in JSON format. May be the path to a file containing " + "the deploy steps; OR '-', with the deploy steps being read from standard " + "input; OR a string. The value should be a list of deploy-step " + "dictionaries; each dictionary should have keys 'interface', 'step', " + "'args' and 'priority'.") + + +class CreateBaremetalDeployTemplate(command.ShowOne): + """Create a new deploy template""" + + log = logging.getLogger(__name__ + ".CreateBaremetalDeployTemplate") + + def get_parser(self, prog_name): + parser = super(CreateBaremetalDeployTemplate, self).get_parser( + prog_name) + + parser.add_argument( + 'name', + metavar='<name>', + help=_('Unique name for this deploy template. Must be a valid ' + 'trait name') + ) + parser.add_argument( + '--uuid', + dest='uuid', + metavar='<uuid>', + help=_('UUID of the deploy template.')) + parser.add_argument( + '--extra', + metavar="<key=value>", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) + parser.add_argument( + '--steps', + metavar="<steps>", + required=True, + help=_DEPLOY_STEPS_HELP + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + baremetal_client = self.app.client_manager.baremetal + + steps = utils.handle_json_arg(parsed_args.steps, 'deploy steps') + + field_list = ['name', 'uuid', 'extra'] + fields = dict((k, v) for (k, v) in vars(parsed_args).items() + if k in field_list and v is not None) + fields = utils.args_array_to_dict(fields, 'extra') + template = baremetal_client.deploy_template.create(steps=steps, + **fields) + + data = dict([(f, getattr(template, f, '')) for f in + res_fields.DEPLOY_TEMPLATE_DETAILED_RESOURCE.fields]) + + return self.dict2columns(data) + + +class ShowBaremetalDeployTemplate(command.ShowOne): + """Show baremetal deploy template details.""" + + log = logging.getLogger(__name__ + ".ShowBaremetalDeployTemplate") + + def get_parser(self, prog_name): + parser = super(ShowBaremetalDeployTemplate, self).get_parser(prog_name) + parser.add_argument( + "template", + metavar="<template>", + help=_("Name or UUID of the deploy template.") + ) + parser.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='<field>', + action='append', + choices=res_fields.DEPLOY_TEMPLATE_DETAILED_RESOURCE.fields, + default=[], + help=_("One or more deploy template fields. Only these fields " + "will be fetched from the server.") + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + fields = list(itertools.chain.from_iterable(parsed_args.fields)) + fields = fields if fields else None + + template = baremetal_client.deploy_template.get( + parsed_args.template, fields=fields)._info + + template.pop("links", None) + return zip(*sorted(template.items())) + + +class SetBaremetalDeployTemplate(command.Command): + """Set baremetal deploy template properties.""" + + log = logging.getLogger(__name__ + ".SetBaremetalDeployTemplate") + + def get_parser(self, prog_name): + parser = super(SetBaremetalDeployTemplate, self).get_parser(prog_name) + + parser.add_argument( + 'template', + metavar='<template>', + help=_("Name or UUID of the deploy template") + ) + parser.add_argument( + '--name', + metavar='<name>', + help=_('Set unique name of the deploy template. Must be a valid ' + 'trait name.') + ) + parser.add_argument( + '--steps', + metavar="<steps>", + help=_DEPLOY_STEPS_HELP + ) + parser.add_argument( + "--extra", + metavar="<key=value>", + action='append', + help=_('Extra to set on this baremetal deploy template ' + '(repeat option to set multiple extras).'), + ) + 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.name: + name = ["name=%s" % parsed_args.name] + properties.extend(utils.args_array_to_patch('add', name)) + if parsed_args.steps: + steps = utils.handle_json_arg(parsed_args.steps, 'deploy steps') + steps = ["steps=%s" % json.dumps(steps)] + properties.extend(utils.args_array_to_patch('add', steps)) + if parsed_args.extra: + properties.extend(utils.args_array_to_patch( + 'add', ['extra/' + x for x in parsed_args.extra])) + + if properties: + baremetal_client.deploy_template.update(parsed_args.template, + properties) + else: + self.log.warning("Please specify what to set.") + + +class UnsetBaremetalDeployTemplate(command.Command): + """Unset baremetal deploy template properties.""" + log = logging.getLogger(__name__ + ".UnsetBaremetalDeployTemplate") + + def get_parser(self, prog_name): + parser = super(UnsetBaremetalDeployTemplate, self).get_parser( + prog_name) + + parser.add_argument( + 'template', + metavar='<template>', + help=_("Name or UUID of the deploy template") + ) + parser.add_argument( + "--extra", + metavar="<key>", + action='append', + help=_('Extra to unset on this baremetal deploy template ' + '(repeat option to unset multiple extras).'), + ) + + 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.extra: + properties.extend(utils.args_array_to_patch('remove', + ['extra/' + x for x in parsed_args.extra])) + + if properties: + baremetal_client.deploy_template.update(parsed_args.template, + properties) + else: + self.log.warning("Please specify what to unset.") + + +class DeleteBaremetalDeployTemplate(command.Command): + """Delete deploy template(s).""" + + log = logging.getLogger(__name__ + ".DeleteBaremetalDeployTemplate") + + def get_parser(self, prog_name): + parser = super(DeleteBaremetalDeployTemplate, self).get_parser( + prog_name) + parser.add_argument( + "templates", + metavar="<template>", + nargs="+", + help=_("Name(s) or UUID(s) of the deploy template(s) to delete.") + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + + failures = [] + for template in parsed_args.templates: + try: + baremetal_client.deploy_template.delete(template) + print(_('Deleted deploy template %s') % template) + except exc.ClientException as e: + failures.append(_("Failed to delete deploy template " + "%(template)s: %(error)s") + % {'template': template, 'error': e}) + + if failures: + raise exc.ClientException("\n".join(failures)) + + +class ListBaremetalDeployTemplate(command.Lister): + """List baremetal deploy templates.""" + + log = logging.getLogger(__name__ + ".ListBaremetalDeployTemplate") + + def get_parser(self, prog_name): + parser = super(ListBaremetalDeployTemplate, self).get_parser(prog_name) + parser.add_argument( + '--limit', + metavar='<limit>', + type=int, + help=_('Maximum number of deploy templates to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Baremetal API Service.') + ) + parser.add_argument( + '--marker', + metavar='<template>', + help=_('DeployTemplate UUID (for example, of the last deploy ' + 'template in the list from a previous request). Returns ' + 'the list of deploy templates after this UUID.') + ) + parser.add_argument( + '--sort', + metavar="<key>[:<direction>]", + help=_('Sort output by specified deploy template fields and ' + 'directions (asc or desc) (default: asc). Multiple fields ' + 'and directions can be specified, separated by comma.') + ) + display_group = parser.add_mutually_exclusive_group() + display_group.add_argument( + '--long', + dest='detail', + action='store_true', + default=False, + help=_("Show detailed information about deploy templates.") + ) + display_group.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='<field>', + action='append', + default=[], + choices=res_fields.DEPLOY_TEMPLATE_DETAILED_RESOURCE.fields, + help=_("One or more deploy template fields. Only these fields " + "will be fetched from the server. Can not be used when " + "'--long' is specified.") + ) + 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.DEPLOY_TEMPLATE_RESOURCE.fields + labels = res_fields.DEPLOY_TEMPLATE_RESOURCE.labels + + 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.detail: + params['detail'] = parsed_args.detail + columns = res_fields.DEPLOY_TEMPLATE_DETAILED_RESOURCE.fields + labels = res_fields.DEPLOY_TEMPLATE_DETAILED_RESOURCE.labels + + elif parsed_args.fields: + params['detail'] = False + fields = itertools.chain.from_iterable(parsed_args.fields) + resource = res_fields.Resource(list(fields)) + columns = resource.fields + labels = resource.labels + params['fields'] = columns + + self.log.debug("params(%s)", params) + data = client.deploy_template.list(**params) + + data = oscutils.sort_items(data, parsed_args.sort) + + return (labels, + (oscutils.get_item_properties(s, columns) for s in data)) diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index fd1e2db..718e12a 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -66,10 +66,7 @@ class ProvisionStateBaremetalNode(command.Command): baremetal_client = self.app.client_manager.baremetal clean_steps = getattr(parsed_args, 'clean_steps', None) - if clean_steps == '-': - clean_steps = utils.get_from_stdin('clean steps') - if clean_steps: - clean_steps = utils.handle_json_or_file_arg(clean_steps) + clean_steps = utils.handle_json_arg(clean_steps, 'clean steps') config_drive = getattr(parsed_args, 'config_drive', None) rescue_password = getattr(parsed_args, 'rescue_password', None) @@ -1207,9 +1204,8 @@ class SetBaremetalNode(command.Command): # also being modified. if parsed_args.target_raid_config: raid_config = parsed_args.target_raid_config - if raid_config == '-': - raid_config = utils.get_from_stdin('target_raid_config') - raid_config = utils.handle_json_or_file_arg(raid_config) + raid_config = utils.handle_json_arg(raid_config, + 'target_raid_config') baremetal_client.node.set_target_raid_config(parsed_args.node, raid_config) diff --git a/ironicclient/osc/v1/baremetal_port.py b/ironicclient/osc/v1/baremetal_port.py index bc0468f..ceb6702 100644 --- a/ironicclient/osc/v1/baremetal_port.py +++ b/ironicclient/osc/v1/baremetal_port.py @@ -443,7 +443,7 @@ class ListBaremetalPort(command.Lister): type=int, help=_('Maximum number of ports to return per request, ' '0 for no limit. Default is the maximum number used ' - 'by the Ironic API Service.') + 'by the Baremetal API Service.') ) parser.add_argument( '--marker', diff --git a/ironicclient/tests/functional/osc/v1/base.py b/ironicclient/tests/functional/osc/v1/base.py index b2ce630..77308df 100644 --- a/ironicclient/tests/functional/osc/v1/base.py +++ b/ironicclient/tests/functional/osc/v1/base.py @@ -401,3 +401,60 @@ class TestCase(base.FunctionalTestBase): except exceptions.CommandFailed: if not ignore_exceptions: raise + + def deploy_template_create(self, name, params=''): + """Create baremetal deploy template and add cleanup. + + :param String name: deploy template name + :param String params: additional parameters + :return: JSON object of created deploy template + """ + opts = self.get_opts() + template = self.openstack('baremetal deploy template create {0} {1} ' + '{2}'.format(opts, name, params)) + template = json.loads(template) + if not template: + self.fail('Baremetal deploy template has not been created!') + self.addCleanup(self.deploy_template_delete, template['uuid'], True) + return template + + def deploy_template_list(self, fields=None, params=''): + """List baremetal deploy templates. + + :param List fields: List of fields to show + :param String params: Additional kwargs + :return: list of JSON deploy template objects + """ + opts = self.get_opts(fields=fields) + output = self.openstack('baremetal deploy template list {0} {1}' + .format(opts, params)) + return json.loads(output) + + def deploy_template_show(self, identifier, fields=None, params=''): + """Show specified baremetal deploy template. + + :param String identifier: Name or UUID of the deploy template + :param List fields: List of fields to show + :param List params: Additional kwargs + :return: JSON object of deploy template + """ + opts = self.get_opts(fields) + output = self.openstack('baremetal deploy template show {0} {1} {2}' + .format(opts, identifier, params)) + return json.loads(output) + + def deploy_template_delete(self, identifier, ignore_exceptions=False): + """Try to delete baremetal deploy template by UUID. + + :param String identifier: Name or UUID of the deploy template + :param Bool ignore_exceptions: Ignore exception (needed for cleanUp) + :return: raw values output + :raise: CommandFailed exception when command fails to delete a deploy + template + """ + try: + return self.openstack('baremetal deploy template delete {0}' + .format(identifier)) + except exceptions.CommandFailed: + if not ignore_exceptions: + raise diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_deploy_template_basic.py b/ironicclient/tests/functional/osc/v1/test_baremetal_deploy_template_basic.py new file mode 100644 index 0000000..1fa2932 --- /dev/null +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_deploy_template_basic.py @@ -0,0 +1,177 @@ +# 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 json + +import ddt +from tempest.lib.common.utils import data_utils +from tempest.lib import exceptions + +from ironicclient.tests.functional.osc.v1 import base + + +@ddt.ddt +class BaremetalDeployTemplateTests(base.TestCase): + """Functional tests for baremetal deploy template commands.""" + + @staticmethod + def _get_random_trait(): + return data_utils.rand_name('CUSTOM', '').replace('-', '_') + + def setUp(self): + super(BaremetalDeployTemplateTests, self).setUp() + self.steps = json.dumps([{ + 'interface': 'bios', + 'step': 'apply_configuration', + 'args': {}, + 'priority': 10, + }]) + name = self._get_random_trait() + self.template = self.deploy_template_create( + name, params="--steps '%s'" % self.steps) + + def tearDown(self): + if self.template is not None: + self.deploy_template_delete(self.template['uuid']) + super(BaremetalDeployTemplateTests, self).tearDown() + + def test_list(self): + """Check baremetal deploy template list command. + + Test steps: + 1) Create baremetal deploy template in setUp. + 2) List baremetal deploy templates. + 3) Check deploy template name and UUID in deploy templates list. + """ + template_list = self.deploy_template_list() + self.assertIn(self.template['name'], + [template['Name'] + for template in template_list]) + self.assertIn(self.template['uuid'], + [template['UUID'] + for template in template_list]) + + def test_list_long(self): + """Check baremetal deploy template list --long command + + Test steps: + 1) Create baremetal deploy template in setUp. + 2) List baremetal deploy templates with detail=True. + 3) Check deploy template fields in output. + """ + template_list = self.deploy_template_list(params='--long') + template = [template for template in template_list + if template['Name'] == self.template['name']][0] + self.assertEqual(self.template['extra'], template['Extra']) + self.assertEqual(self.template['name'], template['Name']) + self.assertEqual(self.template['steps'], template['Steps']) + self.assertEqual(self.template['uuid'], template['UUID']) + + def test_show(self): + """Check baremetal deploy template show command with UUID. + + Test steps: + 1) Create baremetal deploy template in setUp. + 2) Show baremetal deploy template calling it by UUID. + 3) Check deploy template fields in output. + """ + template = self.deploy_template_show(self.template['uuid']) + self.assertEqual(self.template['extra'], template['extra']) + self.assertEqual(self.template['name'], template['name']) + self.assertEqual(self.template['steps'], template['steps']) + self.assertEqual(self.template['uuid'], template['uuid']) + + def test_delete(self): + """Check baremetal deploy template delete command. + + Test steps: + 1) Create baremetal deploy template in setUp. + 2) Delete baremetal deploy template by UUID. + 3) Check that deploy template deleted successfully and not in list. + """ + output = self.deploy_template_delete(self.template['uuid']) + self.assertIn('Deleted deploy template {0}'.format( + self.template['uuid']), output) + template_list = self.deploy_template_list() + self.assertNotIn(self.template['name'], + [template['Name'] for template in template_list]) + self.assertNotIn(self.template['uuid'], + [template['UUID'] for template in template_list]) + self.template = None + + def test_set_steps(self): + """Check baremetal deploy template set command for steps. + + Test steps: + 1) Create baremetal deploy template in setUp. + 2) Set steps for deploy template. + 3) Check that baremetal deploy template steps were set. + """ + steps = [{ + 'interface': 'bios', + 'step': 'apply_configuration', + 'args': {}, + 'priority': 20, + }] + self.openstack("baremetal deploy template set --steps '{0}' {1}" + .format(json.dumps(steps), self.template['uuid'])) + + show_prop = self.deploy_template_show(self.template['uuid'], + fields=['steps']) + self.assertEqual(steps, show_prop['steps']) + + def test_set_unset(self): + """Check baremetal deploy template set and unset commands. + + Test steps: + 1) Create baremetal deploy template in setUp. + 2) Set extra data for deploy template. + 3) Check that baremetal deploy template extra data was set. + 4) Unset extra data for deploy template. + 5) Check that baremetal deploy template extra data was unset. + """ + extra_key = 'ext' + extra_value = 'testdata' + self.openstack( + 'baremetal deploy template set --extra {0}={1} {2}' + .format(extra_key, extra_value, self.template['uuid'])) + + show_prop = self.deploy_template_show(self.template['uuid'], + fields=['extra']) + self.assertEqual(extra_value, show_prop['extra'][extra_key]) + + self.openstack('baremetal deploy template unset --extra {0} {1}' + .format(extra_key, self.template['uuid'])) + + show_prop = self.deploy_template_show(self.template['uuid'], + fields=['extra']) + self.assertNotIn(extra_key, show_prop['extra']) + + @ddt.data( + ('--uuid', '', 'expected one argument'), + ('--uuid', '!@#$^*&%^', 'Expected a UUID'), + ('', '', 'too few arguments'), + ('', 'not/a/name', 'Deploy template name must be a valid trait'), + ('', 'foo', 'Deploy template name must be a valid trait'), + ('--steps', '', 'expected one argument'), + ('--steps', '[]', 'No deploy steps specified')) + @ddt.unpack + def test_create_negative(self, argument, value, ex_text): + """Check errors on invalid input parameters.""" + base_cmd = 'baremetal deploy template create' + if argument != '': + base_cmd += ' %s' % self._get_random_trait() + if argument != '--steps': + base_cmd += " --steps '%s'" % self.steps + command = self.construct_cmd(base_cmd, argument, value) + self.assertRaisesRegex(exceptions.CommandFailed, ex_text, + self.openstack, command) diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index 1e93fc4..419b709 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -14,6 +14,8 @@ # under the License. # +import json + import mock from osc_lib.tests import utils @@ -201,6 +203,22 @@ ALLOCATION = { 'node_uuid': baremetal_uuid, } +baremetal_deploy_template_uuid = 'ddd-tttttt-dddd' +baremetal_deploy_template_name = 'DeployTemplate-name' +baremetal_deploy_template_steps = json.dumps([{ + 'interface': 'raid', + 'step': 'create_configuration', + 'args': {}, + 'priority': 10 +}]) +baremetal_deploy_template_extra = {'key1': 'value1', 'key2': 'value2'} +DEPLOY_TEMPLATE = { + 'uuid': baremetal_deploy_template_uuid, + 'name': baremetal_deploy_template_name, + 'steps': baremetal_deploy_template_steps, + 'extra': baremetal_deploy_template_extra, +} + class TestBaremetal(utils.TestCommand): diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_deploy_template.py b/ironicclient/tests/unit/osc/v1/test_baremetal_deploy_template.py new file mode 100644 index 0000000..799ce1d --- /dev/null +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_deploy_template.py @@ -0,0 +1,450 @@ +# 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 +import json + +import mock +from osc_lib.tests import utils as osctestutils + +from ironicclient import exc +from ironicclient.osc.v1 import baremetal_deploy_template +from ironicclient.tests.unit.osc.v1 import fakes as baremetal_fakes + + +class TestBaremetalDeployTemplate(baremetal_fakes.TestBaremetal): + + def setUp(self): + super(TestBaremetalDeployTemplate, self).setUp() + + self.baremetal_mock = self.app.client_manager.baremetal + self.baremetal_mock.reset_mock() + + +class TestCreateBaremetalDeployTemplate(TestBaremetalDeployTemplate): + def setUp(self): + super(TestCreateBaremetalDeployTemplate, self).setUp() + + self.baremetal_mock.deploy_template.create.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.DEPLOY_TEMPLATE), + loaded=True, + )) + + # Get the command object to test + self.cmd = baremetal_deploy_template.CreateBaremetalDeployTemplate( + self.app, None) + + def test_baremetal_deploy_template_create(self): + arglist = [ + baremetal_fakes.baremetal_deploy_template_name, + '--steps', baremetal_fakes.baremetal_deploy_template_steps, + ] + + verifylist = [ + ('name', baremetal_fakes.baremetal_deploy_template_name), + ('steps', baremetal_fakes.baremetal_deploy_template_steps), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Set expected values + args = { + 'name': baremetal_fakes.baremetal_deploy_template_name, + 'steps': json.loads( + baremetal_fakes.baremetal_deploy_template_steps), + } + + self.baremetal_mock.deploy_template.create.assert_called_once_with( + **args) + + def test_baremetal_deploy_template_create_uuid(self): + arglist = [ + baremetal_fakes.baremetal_deploy_template_name, + '--steps', baremetal_fakes.baremetal_deploy_template_steps, + '--uuid', baremetal_fakes.baremetal_deploy_template_uuid, + ] + + verifylist = [ + ('name', baremetal_fakes.baremetal_deploy_template_name), + ('steps', baremetal_fakes.baremetal_deploy_template_steps), + ('uuid', baremetal_fakes.baremetal_deploy_template_uuid), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Set expected values + args = { + 'name': baremetal_fakes.baremetal_deploy_template_name, + 'steps': json.loads( + baremetal_fakes.baremetal_deploy_template_steps), + 'uuid': baremetal_fakes.baremetal_deploy_template_uuid, + } + + self.baremetal_mock.deploy_template.create.assert_called_once_with( + **args) + + def test_baremetal_deploy_template_create_no_name(self): + arglist = [ + '--steps', baremetal_fakes.baremetal_deploy_template_steps, + ] + + verifylist = [ + ('steps', baremetal_fakes.baremetal_deploy_template_steps), + ] + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + self.assertFalse(self.baremetal_mock.deploy_template.create.called) + + def test_baremetal_deploy_template_create_no_steps(self): + arglist = [ + '--name', baremetal_fakes.baremetal_deploy_template_name, + ] + + verifylist = [ + ('name', baremetal_fakes.baremetal_deploy_template_name), + ] + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + self.assertFalse(self.baremetal_mock.deploy_template.create.called) + + +class TestShowBaremetalDeployTemplate(TestBaremetalDeployTemplate): + def setUp(self): + super(TestShowBaremetalDeployTemplate, self).setUp() + + self.baremetal_mock.deploy_template.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.DEPLOY_TEMPLATE), + loaded=True)) + + self.cmd = baremetal_deploy_template.ShowBaremetalDeployTemplate( + self.app, None) + + def test_baremetal_deploy_template_show(self): + arglist = [baremetal_fakes.baremetal_deploy_template_uuid] + verifylist = [('template', + baremetal_fakes.baremetal_deploy_template_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + args = [baremetal_fakes.baremetal_deploy_template_uuid] + self.baremetal_mock.deploy_template.get.assert_called_with( + *args, fields=None) + + collist = ( + 'extra', + 'name', + 'steps', + 'uuid') + self.assertEqual(collist, columns) + + datalist = ( + baremetal_fakes.baremetal_deploy_template_extra, + baremetal_fakes.baremetal_deploy_template_name, + baremetal_fakes.baremetal_deploy_template_steps, + baremetal_fakes.baremetal_deploy_template_uuid) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_deploy_template_show_no_template(self): + arglist = [] + verifylist = [] + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestBaremetalDeployTemplateSet(TestBaremetalDeployTemplate): + def setUp(self): + super(TestBaremetalDeployTemplateSet, self).setUp() + + self.baremetal_mock.deploy_template.update.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.DEPLOY_TEMPLATE), + loaded=True)) + + self.cmd = baremetal_deploy_template.SetBaremetalDeployTemplate( + self.app, None) + + def test_baremetal_deploy_template_set_name(self): + new_name = 'foo' + arglist = [ + baremetal_fakes.baremetal_deploy_template_uuid, + '--name', new_name] + verifylist = [ + ('template', baremetal_fakes.baremetal_deploy_template_uuid), + ('name', new_name)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.deploy_template.update.assert_called_once_with( + baremetal_fakes.baremetal_deploy_template_uuid, + [{'path': '/name', 'value': new_name, 'op': 'add'}]) + + def test_baremetal_deploy_template_set_steps(self): + arglist = [ + baremetal_fakes.baremetal_deploy_template_uuid, + '--steps', baremetal_fakes.baremetal_deploy_template_steps] + verifylist = [ + ('template', baremetal_fakes.baremetal_deploy_template_uuid), + ('steps', baremetal_fakes.baremetal_deploy_template_steps)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + expected_steps = json.loads( + baremetal_fakes.baremetal_deploy_template_steps) + self.cmd.take_action(parsed_args) + self.baremetal_mock.deploy_template.update.assert_called_once_with( + baremetal_fakes.baremetal_deploy_template_uuid, + [{'path': '/steps', 'value': expected_steps, 'op': 'add'}]) + + def test_baremetal_deploy_template_set_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestBaremetalDeployTemplateUnset(TestBaremetalDeployTemplate): + def setUp(self): + super(TestBaremetalDeployTemplateUnset, self).setUp() + + self.baremetal_mock.deploy_template.update.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.DEPLOY_TEMPLATE), + loaded=True)) + + self.cmd = baremetal_deploy_template.UnsetBaremetalDeployTemplate( + self.app, None) + + def test_baremetal_deploy_template_unset_extra(self): + arglist = [ + baremetal_fakes.baremetal_deploy_template_uuid, '--extra', 'key1'] + verifylist = [('template', + baremetal_fakes.baremetal_deploy_template_uuid), + ('extra', ['key1'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.deploy_template.update.assert_called_once_with( + baremetal_fakes.baremetal_deploy_template_uuid, + [{'path': '/extra/key1', 'op': 'remove'}]) + + def test_baremetal_deploy_template_unset_multiple_extras(self): + arglist = [ + baremetal_fakes.baremetal_deploy_template_uuid, + '--extra', 'key1', '--extra', 'key2'] + verifylist = [('template', + baremetal_fakes.baremetal_deploy_template_uuid), + ('extra', ['key1', 'key2'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.deploy_template.update.assert_called_once_with( + baremetal_fakes.baremetal_deploy_template_uuid, + [{'path': '/extra/key1', 'op': 'remove'}, + {'path': '/extra/key2', 'op': 'remove'}]) + + def test_baremetal_deploy_template_unset_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_deploy_template_unset_no_property(self): + uuid = baremetal_fakes.baremetal_deploy_template_uuid + arglist = [uuid] + verifylist = [('template', uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.assertFalse(self.baremetal_mock.deploy_template.update.called) + + +class TestBaremetalDeployTemplateDelete(TestBaremetalDeployTemplate): + def setUp(self): + super(TestBaremetalDeployTemplateDelete, self).setUp() + + self.cmd = baremetal_deploy_template.DeleteBaremetalDeployTemplate( + self.app, None) + + def test_baremetal_deploy_template_delete(self): + arglist = ['zzz-zzzzzz-zzzz'] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + args = 'zzz-zzzzzz-zzzz' + self.baremetal_mock.deploy_template.delete.assert_called_with(args) + + def test_baremetal_deploy_template_delete_multiple(self): + arglist = ['zzz-zzzzzz-zzzz', 'fakename'] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + args = ['zzz-zzzzzz-zzzz', 'fakename'] + self.baremetal_mock.deploy_template.delete.has_calls( + [mock.call(x) for x in args]) + self.assertEqual( + 2, self.baremetal_mock.deploy_template.delete.call_count) + + def test_baremetal_deploy_template_delete_multiple_with_fail(self): + arglist = ['zzz-zzzzzz-zzzz', 'badname'] + verifylist = [] + + self.baremetal_mock.deploy_template.delete.side_effect = [ + '', exc.ClientException] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exc.ClientException, + self.cmd.take_action, + parsed_args) + + args = ['zzz-zzzzzz-zzzz', 'badname'] + self.baremetal_mock.deploy_template.delete.has_calls( + [mock.call(x) for x in args]) + self.assertEqual( + 2, self.baremetal_mock.deploy_template.delete.call_count) + + def test_baremetal_deploy_template_delete_no_template(self): + arglist = [] + verifylist = [] + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestBaremetalDeployTemplateList(TestBaremetalDeployTemplate): + def setUp(self): + super(TestBaremetalDeployTemplateList, self).setUp() + + self.baremetal_mock.deploy_template.list.return_value = [ + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.DEPLOY_TEMPLATE), + loaded=True) + ] + + self.cmd = baremetal_deploy_template.ListBaremetalDeployTemplate( + self.app, None) + + def test_baremetal_deploy_template_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None} + self.baremetal_mock.deploy_template.list.assert_called_with(**kwargs) + + collist = ( + "UUID", + "Name") + self.assertEqual(collist, columns) + + datalist = (( + baremetal_fakes.baremetal_deploy_template_uuid, + baremetal_fakes.baremetal_deploy_template_name + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_deploy_template_list_long(self): + arglist = ['--long'] + verifylist = [('detail', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'detail': True, + 'marker': None, + 'limit': None, + } + self.baremetal_mock.deploy_template.list.assert_called_with(**kwargs) + + collist = ('UUID', 'Name', 'Steps', 'Extra', 'Created At', + 'Updated At') + self.assertEqual(collist, columns) + + datalist = (( + baremetal_fakes.baremetal_deploy_template_uuid, + baremetal_fakes.baremetal_deploy_template_name, + baremetal_fakes.baremetal_deploy_template_steps, + baremetal_fakes.baremetal_deploy_template_extra, + '', + '', + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_deploy_template_list_fields(self): + arglist = ['--fields', 'uuid', 'steps'] + verifylist = [('fields', [['uuid', 'steps']])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None, + 'detail': False, + 'fields': ('uuid', 'steps') + } + self.baremetal_mock.deploy_template.list.assert_called_with(**kwargs) + + def test_baremetal_deploy_template_list_fields_multiple(self): + arglist = ['--fields', 'uuid', 'name', '--fields', 'steps'] + verifylist = [('fields', [['uuid', 'name'], ['steps']])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None, + 'detail': False, + 'fields': ('uuid', 'name', 'steps') + } + self.baremetal_mock.deploy_template.list.assert_called_with(**kwargs) + + def test_baremetal_deploy_template_list_invalid_fields(self): + arglist = ['--fields', 'uuid', 'invalid'] + verifylist = [('fields', [['uuid', 'invalid']])] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) diff --git a/ironicclient/tests/unit/v1/test_deploy_template.py b/ironicclient/tests/unit/v1/test_deploy_template.py new file mode 100644 index 0000000..0999bb3 --- /dev/null +++ b/ironicclient/tests/unit/v1/test_deploy_template.py @@ -0,0 +1,291 @@ +# 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 copy + +import testtools +from testtools.matchers import HasLength + +from ironicclient import exc +from ironicclient.tests.unit import utils +import ironicclient.v1.deploy_template + +DEPLOY_TEMPLATE = {'uuid': '11111111-2222-3333-4444-555555555555', + 'name': 'fake-template', + 'steps': {}, + 'extra': {}} + +DEPLOY_TEMPLATE2 = {'uuid': '55555555-4444-3333-2222-111111111111', + 'name': 'fake-template2', + 'steps': {}, + 'extra': {}} + +CREATE_DEPLOY_TEMPLATE = copy.deepcopy(DEPLOY_TEMPLATE) +del CREATE_DEPLOY_TEMPLATE['uuid'] + +CREATE_DEPLOY_TEMPLATE_WITH_UUID = copy.deepcopy(DEPLOY_TEMPLATE) + +UPDATED_DEPLOY_TEMPLATE = copy.deepcopy(DEPLOY_TEMPLATE) +NEW_NAME = 'fake-template3' +UPDATED_DEPLOY_TEMPLATE['name'] = NEW_NAME + +fake_responses = { + '/v1/deploy_templates': + { + 'GET': ( + {}, + {"deploy_templates": [DEPLOY_TEMPLATE]}, + ), + 'POST': ( + {}, + CREATE_DEPLOY_TEMPLATE, + ), + }, + '/v1/deploy_templates/?detail=True': + { + 'GET': ( + {}, + {"deploy_templates": [DEPLOY_TEMPLATE]}, + ), + }, + '/v1/deploy_templates/?fields=uuid,name': + { + 'GET': ( + {}, + {"deploy_templates": [DEPLOY_TEMPLATE]}, + ), + }, + '/v1/deploy_templates/%s' % DEPLOY_TEMPLATE['uuid']: + { + 'GET': ( + {}, + DEPLOY_TEMPLATE, + ), + 'DELETE': ( + {}, + None, + ), + 'PATCH': ( + {}, + UPDATED_DEPLOY_TEMPLATE, + ), + }, + '/v1/deploy_templates/%s?fields=uuid,name' % DEPLOY_TEMPLATE['uuid']: + { + 'GET': ( + {}, + DEPLOY_TEMPLATE, + ), + }, +} + +fake_responses_pagination = { + '/v1/deploy_templates': + { + 'GET': ( + {}, + {"deploy_templates": [DEPLOY_TEMPLATE], + "next": "http://127.0.0.1:6385/v1/deploy_templates/?limit=1"} + ), + }, + '/v1/deploy_templates/?limit=1': + { + 'GET': ( + {}, + {"deploy_templates": [DEPLOY_TEMPLATE2]} + ), + }, + '/v1/deploy_templates/?marker=%s' % DEPLOY_TEMPLATE['uuid']: + { + 'GET': ( + {}, + {"deploy_templates": [DEPLOY_TEMPLATE2]} + ), + }, +} + +fake_responses_sorting = { + '/v1/deploy_templates/?sort_key=updated_at': + { + 'GET': ( + {}, + {"deploy_templates": [DEPLOY_TEMPLATE2, DEPLOY_TEMPLATE]} + ), + }, + '/v1/deploy_templates/?sort_dir=desc': + { + 'GET': ( + {}, + {"deploy_templates": [DEPLOY_TEMPLATE2, DEPLOY_TEMPLATE]} + ), + }, +} + + +class DeployTemplateManagerTest(testtools.TestCase): + + def setUp(self): + super(DeployTemplateManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = ironicclient.v1.deploy_template.DeployTemplateManager( + self.api) + + def test_deploy_templates_list(self): + deploy_templates = self.mgr.list() + expect = [ + ('GET', '/v1/deploy_templates', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(deploy_templates)) + + def test_deploy_templates_list_detail(self): + deploy_templates = self.mgr.list(detail=True) + expect = [ + ('GET', '/v1/deploy_templates/?detail=True', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(deploy_templates)) + + def test_deploy_template_list_fields(self): + deploy_templates = self.mgr.list(fields=['uuid', 'name']) + expect = [ + ('GET', '/v1/deploy_templates/?fields=uuid,name', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(deploy_templates)) + + def test_deploy_template_list_detail_and_fields_fail(self): + self.assertRaises(exc.InvalidAttribute, self.mgr.list, + detail=True, fields=['uuid', 'name']) + + def test_deploy_templates_list_limit(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.deploy_template.DeployTemplateManager( + self.api) + deploy_templates = self.mgr.list(limit=1) + expect = [ + ('GET', '/v1/deploy_templates/?limit=1', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(deploy_templates, HasLength(1)) + + def test_deploy_templates_list_marker(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.deploy_template.DeployTemplateManager( + self.api) + deploy_templates = self.mgr.list(marker=DEPLOY_TEMPLATE['uuid']) + expect = [ + ('GET', + '/v1/deploy_templates/?marker=%s' % DEPLOY_TEMPLATE['uuid'], {}, + None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(deploy_templates, HasLength(1)) + + def test_deploy_templates_list_pagination_no_limit(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.deploy_template.DeployTemplateManager( + self.api) + deploy_templates = self.mgr.list(limit=0) + expect = [ + ('GET', '/v1/deploy_templates', {}, None), + ('GET', '/v1/deploy_templates/?limit=1', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(deploy_templates, HasLength(2)) + + def test_deploy_templates_list_sort_key(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.deploy_template.DeployTemplateManager( + self.api) + deploy_templates = self.mgr.list(sort_key='updated_at') + expect = [ + ('GET', '/v1/deploy_templates/?sort_key=updated_at', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(deploy_templates)) + + def test_deploy_templates_list_sort_dir(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.deploy_template.DeployTemplateManager( + self.api) + deploy_templates = self.mgr.list(sort_dir='desc') + expect = [ + ('GET', '/v1/deploy_templates/?sort_dir=desc', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(deploy_templates)) + + def test_deploy_templates_show(self): + deploy_template = self.mgr.get(DEPLOY_TEMPLATE['uuid']) + expect = [ + ('GET', '/v1/deploy_templates/%s' % DEPLOY_TEMPLATE['uuid'], {}, + None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(DEPLOY_TEMPLATE['uuid'], deploy_template.uuid) + self.assertEqual(DEPLOY_TEMPLATE['name'], deploy_template.name) + self.assertEqual(DEPLOY_TEMPLATE['steps'], deploy_template.steps) + self.assertEqual(DEPLOY_TEMPLATE['extra'], deploy_template.extra) + + def test_deploy_template_show_fields(self): + deploy_template = self.mgr.get(DEPLOY_TEMPLATE['uuid'], + fields=['uuid', 'name']) + expect = [ + ('GET', '/v1/deploy_templates/%s?fields=uuid,name' % + DEPLOY_TEMPLATE['uuid'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(DEPLOY_TEMPLATE['uuid'], deploy_template.uuid) + self.assertEqual(DEPLOY_TEMPLATE['name'], deploy_template.name) + + def test_create(self): + deploy_template = self.mgr.create(**CREATE_DEPLOY_TEMPLATE) + expect = [ + ('POST', '/v1/deploy_templates', {}, CREATE_DEPLOY_TEMPLATE), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(deploy_template) + + def test_create_with_uuid(self): + deploy_template = self.mgr.create(**CREATE_DEPLOY_TEMPLATE_WITH_UUID) + expect = [ + ('POST', '/v1/deploy_templates', {}, + CREATE_DEPLOY_TEMPLATE_WITH_UUID), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(deploy_template) + + def test_delete(self): + deploy_template = self.mgr.delete( + template_id=DEPLOY_TEMPLATE['uuid']) + expect = [ + ('DELETE', '/v1/deploy_templates/%s' % DEPLOY_TEMPLATE['uuid'], {}, + None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(deploy_template) + + def test_update(self): + patch = {'op': 'replace', + 'value': NEW_NAME, + 'path': '/name'} + deploy_template = self.mgr.update( + template_id=DEPLOY_TEMPLATE['uuid'], patch=patch) + expect = [ + ('PATCH', '/v1/deploy_templates/%s' % DEPLOY_TEMPLATE['uuid'], + {}, patch), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NEW_NAME, deploy_template.name) diff --git a/ironicclient/v1/client.py b/ironicclient/v1/client.py index 76f6bf0..6f38964 100644 --- a/ironicclient/v1/client.py +++ b/ironicclient/v1/client.py @@ -23,6 +23,7 @@ from ironicclient import exc from ironicclient.v1 import allocation from ironicclient.v1 import chassis from ironicclient.v1 import conductor +from ironicclient.v1 import deploy_template from ironicclient.v1 import driver from ironicclient.v1 import events from ironicclient.v1 import node @@ -103,6 +104,8 @@ class Client(object): self.conductor = conductor.ConductorManager(self.http_client) self.events = events.EventManager(self.http_client) self.allocation = allocation.AllocationManager(self.http_client) + self.deploy_template = deploy_template.DeployTemplateManager( + self.http_client) @property def current_api_version(self): diff --git a/ironicclient/v1/deploy_template.py b/ironicclient/v1/deploy_template.py new file mode 100644 index 0000000..4ae377d --- /dev/null +++ b/ironicclient/v1/deploy_template.py @@ -0,0 +1,86 @@ +# 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 import base +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient import exc + + +class DeployTemplate(base.Resource): + def __repr__(self): + return "<DeployTemplate %s>" % self._info + + +class DeployTemplateManager(base.CreateManager): + resource_class = DeployTemplate + _creation_attributes = ['extra', 'name', 'steps', 'uuid'] + _resource_name = 'deploy_templates' + + def list(self, limit=None, marker=None, sort_key=None, sort_dir=None, + detail=False, fields=None): + """Retrieve a list of deploy templates. + + :param marker: Optional, the UUID of a deploy template, eg the last + template from a previous result set. Return the next + result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of deploy templates to return. + 2) limit == 0, return the entire list of deploy templates. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Ironic API + (see Ironic's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about deploy templates. + + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. Can not be used + when 'detail' is set. + + :returns: A list of deploy templates. + + """ + if limit is not None: + limit = int(limit) + + if detail and fields: + raise exc.InvalidAttribute(_("Can't fetch a subset of fields " + "with 'detail' set")) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir, + fields, detail=detail) + path = '' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), "deploy_templates") + else: + return self._list_pagination(self._path(path), "deploy_templates", + limit=limit) + + def get(self, template_id, fields=None): + return self._get(resource_id=template_id, fields=fields) + + def delete(self, template_id): + return self._delete(resource_id=template_id) + + def update(self, template_id, patch): + return self._update(resource_id=template_id, patch=patch) diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index f9a18f5..38b6cc6 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -104,6 +104,7 @@ class Resource(object): 'reservation': 'Reservation', 'resource_class': 'Resource Class', 'state': 'State', + 'steps': 'Steps', 'target_power_state': 'Target Power State', 'target_provision_state': 'Target Provision State', 'target_raid_config': 'Target RAID configuration', @@ -513,3 +514,21 @@ ALLOCATION_RESOURCE = Resource( 'node_uuid', ], ) + +# Deploy templates +DEPLOY_TEMPLATE_DETAILED_RESOURCE = Resource( + ['uuid', + 'name', + 'steps', + 'extra', + 'created_at', + 'updated_at', + ], + sort_excluded=['extra', 'steps'] +) + +DEPLOY_TEMPLATE_RESOURCE = Resource( + ['uuid', + 'name', + ], +) diff --git a/releasenotes/notes/deploy-templates-df354ce825b00430.yaml b/releasenotes/notes/deploy-templates-df354ce825b00430.yaml new file mode 100644 index 0000000..0b6ada1 --- /dev/null +++ b/releasenotes/notes/deploy-templates-df354ce825b00430.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Adds Python API and CLI for the deploy templates API introduced in API + version 1.55. The following new commands are available: + + * ``openstack baremetal deploy template create`` + * ``openstack baremetal deploy template delete`` + * ``openstack baremetal deploy template list`` + * ``openstack baremetal deploy template set`` + * ``openstack baremetal deploy template show`` @@ -40,6 +40,12 @@ openstack.baremetal.v1 = baremetal_chassis_show = ironicclient.osc.v1.baremetal_chassis:ShowBaremetalChassis baremetal_chassis_unset = ironicclient.osc.v1.baremetal_chassis:UnsetBaremetalChassis baremetal_create = ironicclient.osc.v1.baremetal_create:CreateBaremetal + baremetal_deploy_template_create = ironicclient.osc.v1.baremetal_deploy_template:CreateBaremetalDeployTemplate + baremetal_deploy_template_delete = ironicclient.osc.v1.baremetal_deploy_template:DeleteBaremetalDeployTemplate + baremetal_deploy_template_list = ironicclient.osc.v1.baremetal_deploy_template:ListBaremetalDeployTemplate + baremetal_deploy_template_set = ironicclient.osc.v1.baremetal_deploy_template:SetBaremetalDeployTemplate + baremetal_deploy_template_unset = ironicclient.osc.v1.baremetal_deploy_template:UnsetBaremetalDeployTemplate + baremetal_deploy_template_show = ironicclient.osc.v1.baremetal_deploy_template:ShowBaremetalDeployTemplate baremetal_driver_list = ironicclient.osc.v1.baremetal_driver:ListBaremetalDriver baremetal_driver_passthru_call = ironicclient.osc.v1.baremetal_driver:PassthruCallBaremetalDriver baremetal_driver_passthru_list = ironicclient.osc.v1.baremetal_driver:PassthruListBaremetalDriver |