diff -Nru python-ironicclient-2.6.0/AUTHORS python-ironicclient-2.7.0/AUTHORS --- python-ironicclient-2.6.0/AUTHORS 2019-01-15 01:38:03.000000000 +0000 +++ python-ironicclient-2.7.0/AUTHORS 2019-03-08 00:31:10.000000000 +0000 @@ -18,6 +18,7 @@ Chuck Short Clark Boylan Clif Houck +Corey Bryant Dao Cong Tien Davanum Srinivas David Hu @@ -29,11 +30,14 @@ Dmitry Tantsur Doug Hellmann Eric Fried +Eric Fried Flavio Percoco Florian Fuchs Galyna Zholtkevych Ghe Rivero +Guang Yee Gábor Antal +Hamdy Khader Hangdong Zhang HaoZhi, Cui Haomeng, Wang @@ -90,6 +94,7 @@ Ramakrishnan G Rodion Promyshlennikov Ruby Loo +Ruby Loo Ruby Loo Ruby Loo Rui Chen @@ -105,6 +110,7 @@ Tang Chen Tang Chen Tao Li +TienDC Tuan Do Anh Ukesh Kumar Vasudevan Vadim Hmyrov @@ -122,6 +128,7 @@ Zenghui Shi Zhenguo Niu deepakmourya +dnuka ericxiett fpxie ghanshyam @@ -148,3 +155,4 @@ xiexs ya.wang zhengchuan hu +zhulingjie diff -Nru python-ironicclient-2.6.0/ChangeLog python-ironicclient-2.7.0/ChangeLog --- python-ironicclient-2.6.0/ChangeLog 2019-01-15 01:38:03.000000000 +0000 +++ python-ironicclient-2.7.0/ChangeLog 2019-03-08 00:31:10.000000000 +0000 @@ -1,6 +1,28 @@ CHANGES ======= +2.7.0 +----- + +* Update release notes +* Follow-up to the configdrive change +* Accept 'valid\_interfaces' in client setup +* pass endpoint interface to http client +* Support passing a dictionary for configdrive +* Deploy templates: client support +* [Trivial] Allocation API: fix incorrect parameter description +* [Follow Up] Add 'hostname' to port's local link connection +* Replace mock.has\_calls() with assert\_has\_calls +* Add 'hostname' to port's local link connection +* Add is-smartnic port attribute to port command +* Support node description +* Allocation API: client API and CLI +* add python 3.7 unit test job +* Add Events support +* Add node owner +* Support for conductors exposed from API +* Change openstack-dev to openstack-discuss + 2.6.0 ----- diff -Nru python-ironicclient-2.6.0/debian/changelog python-ironicclient-2.7.0/debian/changelog --- python-ironicclient-2.6.0/debian/changelog 2019-01-30 15:59:14.000000000 +0000 +++ python-ironicclient-2.7.0/debian/changelog 2019-03-11 18:28:35.000000000 +0000 @@ -1,11 +1,17 @@ -python-ironicclient (2.6.0-0ubuntu1~ubuntu19.04.1~ppa201901301059) disco; urgency=medium +python-ironicclient (2.7.0-0ubuntu1~ubuntu19.04.1~ppa201903111428) disco; urgency=medium + + * New upstream release for OpenStack Stein. + * No-change backport to disco + + -- Corey Bryant Mon, 11 Mar 2019 14:28:35 -0400 + +python-ironicclient (2.6.0-0ubuntu1) disco; urgency=medium * New upstream release for OpenStack Stein. * d/control: Align (Build-)Depends with upstream. * d/p/drop-sphinxcontrib-apidoc.patch: Dropped. No longer needed. - * No-change backport to disco - -- Corey Bryant Wed, 30 Jan 2019 10:59:14 -0500 + -- Corey Bryant Wed, 30 Jan 2019 10:15:28 -0500 python-ironicclient (2.5.0-0ubuntu1) cosmic; urgency=medium diff -Nru python-ironicclient-2.6.0/doc/source/cli/osc/v1/index.rst python-ironicclient-2.7.0/doc/source/cli/osc/v1/index.rst --- python-ironicclient-2.6.0/doc/source/cli/osc/v1/index.rst 2019-01-15 01:36:19.000000000 +0000 +++ python-ironicclient-2.7.0/doc/source/cli/osc/v1/index.rst 2019-03-08 00:29:10.000000000 +0000 @@ -4,6 +4,13 @@ List of released CLI commands available in openstack client. These commands can be referenced by doing ``openstack help baremetal``. +==================== +baremetal allocation +==================== + +.. autoprogram-cliff:: openstack.baremetal.v1 + :command: baremetal allocation * + ================= baremetal chassis ================= diff -Nru python-ironicclient-2.6.0/doc/source/contributor/contributing.rst python-ironicclient-2.7.0/doc/source/contributor/contributing.rst --- python-ironicclient-2.6.0/doc/source/contributor/contributing.rst 2019-01-15 01:36:21.000000000 +0000 +++ python-ironicclient-2.7.0/doc/source/contributor/contributing.rst 2019-03-08 00:29:10.000000000 +0000 @@ -37,7 +37,7 @@ https://storyboard.openstack.org/#!/project/959 Mailing list (prefix subjects with ``[ironic]`` for faster responses) - http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev + http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss Code Hosting https://git.openstack.org/cgit/openstack/python-ironicclient diff -Nru python-ironicclient-2.6.0/ironicclient/client.py python-ironicclient-2.7.0/ironicclient/client.py --- python-ironicclient-2.6.0/ironicclient/client.py 2019-01-15 01:36:19.000000000 +0000 +++ python-ironicclient-2.7.0/ironicclient/client.py 2019-03-08 00:29:13.000000000 +0000 @@ -100,14 +100,24 @@ session = session_loader.load_from_options(auth=auth_plugin, **session_opts) + # Make sure we also pass the endpoint interface to the HTTP client. + # NOTE(gyee/efried): 'interface' in ksa config is deprecated in favor of + # 'valid_interfaces'. So, since the caller may be deriving kwargs from + # conf, accept 'valid_interfaces' first. But keep support for 'interface', + # in case the caller is deriving kwargs from, say, an existing Adapter. + interface = kwargs.get('valid_interfaces', kwargs.get('interface')) + endpoint = kwargs.get('endpoint') if not endpoint: try: # endpoint will be used to get hostname # and port that will be used for API version caching. + # NOTE(gyee): KSA defaults interface to 'public' if it is + # empty or None so there's no need to set it to publicURL + # explicitly. endpoint = session.get_endpoint( service_type=kwargs.get('service_type') or 'baremetal', - interface=kwargs.get('interface') or 'publicURL', + interface=interface, region_name=kwargs.get('region_name') ) except Exception as e: @@ -120,7 +130,8 @@ 'max_retries': max_retries, 'retry_interval': retry_interval, 'session': session, - 'endpoint_override': endpoint + 'endpoint_override': endpoint, + 'interface': interface } return Client(api_version, **ironicclient_kwargs) diff -Nru python-ironicclient-2.6.0/ironicclient/common/http.py python-ironicclient-2.7.0/ironicclient/common/http.py --- python-ironicclient-2.6.0/ironicclient/common/http.py 2019-01-15 01:36:19.000000000 +0000 +++ python-ironicclient-2.7.0/ironicclient/common/http.py 2019-03-08 00:29:13.000000000 +0000 @@ -43,7 +43,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 48 +LAST_KNOWN_API_VERSION = 56 LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) LOG = logging.getLogger(__name__) diff -Nru python-ironicclient-2.6.0/ironicclient/common/utils.py python-ironicclient-2.7.0/ironicclient/common/utils.py --- python-ironicclient-2.6.0/ironicclient/common/utils.py 2019-01-15 01:36:19.000000000 +0000 +++ python-ironicclient-2.7.0/ironicclient/common/utils.py 2019-03-08 00:29:10.000000000 +0000 @@ -24,6 +24,7 @@ import subprocess import sys import tempfile +import time from oslo_serialization import base64 from oslo_utils import strutils @@ -390,3 +391,39 @@ raise exc.InvalidAttribute(err) return json_arg + + +def poll(timeout, poll_interval, poll_delay_function, timeout_message): + if not isinstance(timeout, (int, float)) or timeout < 0: + raise ValueError(_('Timeout must be a non-negative number')) + + threshold = time.time() + timeout + poll_delay_function = (time.sleep if poll_delay_function is None + else poll_delay_function) + if not callable(poll_delay_function): + raise TypeError(_('poll_delay_function must be callable')) + + count = 0 + while not timeout or time.time() < threshold: + yield count + + poll_delay_function(poll_interval) + 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 -Nru python-ironicclient-2.6.0/ironicclient/osc/v1/baremetal_allocation.py python-ironicclient-2.7.0/ironicclient/osc/v1/baremetal_allocation.py --- python-ironicclient-2.6.0/ironicclient/osc/v1/baremetal_allocation.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ironicclient-2.7.0/ironicclient/osc/v1/baremetal_allocation.py 2019-03-08 00:29:10.000000000 +0000 @@ -0,0 +1,269 @@ +# 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 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 + + +class CreateBaremetalAllocation(command.ShowOne): + """Create a new baremetal allocation.""" + + log = logging.getLogger(__name__ + ".CreateBaremetalAllocation") + + def get_parser(self, prog_name): + parser = super(CreateBaremetalAllocation, self).get_parser(prog_name) + + parser.add_argument( + '--resource-class', + dest='resource_class', + required=True, + help=_('Resource class to request.')) + parser.add_argument( + '--trait', + action='append', + dest='traits', + help=_('A trait to request. Can be specified multiple times.')) + parser.add_argument( + '--candidate-node', + action='append', + dest='candidate_nodes', + help=_('A candidate node for this allocation. Can be specified ' + 'multiple times. If at least one is specified, only the ' + 'provided candidate nodes are considered for the ' + 'allocation.')) + parser.add_argument( + '--name', + dest='name', + help=_('Unique name of the allocation.')) + parser.add_argument( + '--uuid', + dest='uuid', + help=_('UUID of the allocation.')) + parser.add_argument( + '--extra', + metavar="", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) + parser.add_argument( + '--wait', + type=int, + dest='wait_timeout', + default=None, + metavar='', + const=0, + nargs='?', + help=_("Wait for the new allocation to become active. An error " + "is returned if allocation fails and --wait is used. " + "Optionally takes a timeout value (in seconds). The " + "default value is 0, meaning it will wait indefinitely.")) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + baremetal_client = self.app.client_manager.baremetal + + field_list = ['name', 'uuid', 'extra', 'resource_class', 'traits', + 'candidate_nodes'] + 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') + allocation = baremetal_client.allocation.create(**fields) + if parsed_args.wait_timeout is not None: + allocation = baremetal_client.allocation.wait( + allocation.uuid, timeout=parsed_args.wait_timeout) + + data = dict([(f, getattr(allocation, f, '')) for f in + res_fields.ALLOCATION_DETAILED_RESOURCE.fields]) + + return self.dict2columns(data) + + +class ShowBaremetalAllocation(command.ShowOne): + """Show baremetal allocation details.""" + + log = logging.getLogger(__name__ + ".ShowBaremetalAllocation") + + def get_parser(self, prog_name): + parser = super(ShowBaremetalAllocation, self).get_parser(prog_name) + parser.add_argument( + "allocation", + metavar="", + help=_("UUID or name of the allocation")) + parser.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + choices=res_fields.ALLOCATION_DETAILED_RESOURCE.fields, + default=[], + help=_("One or more allocation 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 + + allocation = baremetal_client.allocation.get( + parsed_args.allocation, fields=fields)._info + + allocation.pop("links", None) + return zip(*sorted(allocation.items())) + + +class ListBaremetalAllocation(command.Lister): + """List baremetal allocations.""" + + log = logging.getLogger(__name__ + ".ListBaremetalAllocation") + + def get_parser(self, prog_name): + parser = super(ListBaremetalAllocation, self).get_parser(prog_name) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Maximum number of allocations to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Baremetal API Service.')) + parser.add_argument( + '--marker', + metavar='', + help=_('Allocation UUID (for example, of the last allocation in ' + 'the list from a previous request). Returns the list of ' + 'allocations after this UUID.')) + parser.add_argument( + '--sort', + metavar="[:]", + help=_('Sort output by specified allocation fields and directions ' + '(asc or desc) (default: asc). Multiple fields and ' + 'directions can be specified, separated by comma.')) + parser.add_argument( + '--node', + metavar='', + help=_("Only list allocations of this node (name or UUID).")) + parser.add_argument( + '--resource-class', + metavar='', + help=_("Only list allocations with this resource class.")) + parser.add_argument( + '--state', + metavar='', + help=_("Only list allocations in this state.")) + + # NOTE(dtantsur): the allocation API does not expose the 'detail' flag, + # but some fields are inconvenient to display in a table, so we emulate + # it on the client side. + display_group = parser.add_mutually_exclusive_group(required=False) + display_group.add_argument( + '--long', + default=False, + help=_("Show detailed information about the allocations."), + action='store_true') + display_group.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + choices=res_fields.ALLOCATION_DETAILED_RESOURCE.fields, + help=_("One or more allocation 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 + + 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 + for field in ('node', 'resource_class', 'state'): + value = getattr(parsed_args, field) + if value is not None: + params[field] = value + + if parsed_args.long: + columns = res_fields.ALLOCATION_DETAILED_RESOURCE.fields + labels = res_fields.ALLOCATION_DETAILED_RESOURCE.labels + elif parsed_args.fields: + fields = itertools.chain.from_iterable(parsed_args.fields) + resource = res_fields.Resource(list(fields)) + columns = resource.fields + labels = resource.labels + params['fields'] = columns + else: + columns = res_fields.ALLOCATION_RESOURCE.fields + labels = res_fields.ALLOCATION_RESOURCE.labels + + self.log.debug("params(%s)", params) + data = client.allocation.list(**params) + + data = oscutils.sort_items(data, parsed_args.sort) + + return (labels, + (oscutils.get_item_properties(s, columns) for s in data)) + + +class DeleteBaremetalAllocation(command.Command): + """Unregister baremetal allocation(s).""" + + log = logging.getLogger(__name__ + ".DeleteBaremetalAllocation") + + def get_parser(self, prog_name): + parser = super(DeleteBaremetalAllocation, self).get_parser(prog_name) + parser.add_argument( + "allocations", + metavar="", + nargs="+", + help=_("Allocations(s) to delete (name or UUID).")) + + 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 allocation in parsed_args.allocations: + try: + baremetal_client.allocation.delete(allocation) + print(_('Deleted allocation %s') % allocation) + except exc.ClientException as e: + failures.append(_("Failed to delete allocation " + "%(allocation)s: %(error)s") + % {'allocation': allocation, 'error': e}) + + if failures: + raise exc.ClientException("\n".join(failures)) diff -Nru python-ironicclient-2.6.0/ironicclient/osc/v1/baremetal_conductor.py python-ironicclient-2.7.0/ironicclient/osc/v1/baremetal_conductor.py --- python-ironicclient-2.6.0/ironicclient/osc/v1/baremetal_conductor.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ironicclient-2.7.0/ironicclient/osc/v1/baremetal_conductor.py 2019-03-08 00:29:10.000000000 +0000 @@ -0,0 +1,145 @@ +# +# Copyright 2015 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import itertools +import logging + +from osc_lib.command import command +from osc_lib import utils as oscutils + +from ironicclient.common.i18n import _ +from ironicclient import exc +from ironicclient.v1 import resource_fields as res_fields + + +class ListBaremetalConductor(command.Lister): + """List baremetal conductors""" + + log = logging.getLogger(__name__ + ".ListBaremetalNode") + + def get_parser(self, prog_name): + parser = super(ListBaremetalConductor, self).get_parser(prog_name) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Maximum number of conductors to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Baremetal API Service.') + ) + parser.add_argument( + '--marker', + metavar='', + help=_('Hostname of the conductor (for example, of the last ' + 'conductor in the list from a previous request). Returns ' + 'the list of conductors after this conductor.') + ) + parser.add_argument( + '--sort', + metavar="[:]", + help=_('Sort output by specified conductor 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(required=False) + display_group.add_argument( + '--long', + default=False, + help=_("Show detailed information about the conductors."), + action='store_true') + display_group.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + choices=res_fields.CONDUCTOR_DETAILED_RESOURCE.fields, + help=_("One or more conductor 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.CONDUCTOR_RESOURCE.fields + labels = res_fields.CONDUCTOR_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.long: + params['detail'] = parsed_args.long + columns = res_fields.CONDUCTOR_DETAILED_RESOURCE.fields + labels = res_fields.CONDUCTOR_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.conductor.list(**params) + + data = oscutils.sort_items(data, parsed_args.sort) + + return (labels, + (oscutils.get_item_properties(s, columns, formatters={ + 'Properties': oscutils.format_dict},) for s in data)) + + +class ShowBaremetalConductor(command.ShowOne): + """Show baremetal conductor details""" + + log = logging.getLogger(__name__ + ".ShowBaremetalConductor") + + def get_parser(self, prog_name): + parser = super(ShowBaremetalConductor, self).get_parser(prog_name) + parser.add_argument( + "conductor", + metavar="", + help=_("Hostname of the conductor")) + parser.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + choices=res_fields.CONDUCTOR_DETAILED_RESOURCE.fields, + default=[], + help=_("One or more conductor 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 + conductor = baremetal_client.conductor.get( + parsed_args.conductor, fields=fields)._info + conductor.pop("links", None) + + return self.dict2columns(conductor) diff -Nru python-ironicclient-2.6.0/ironicclient/osc/v1/baremetal_deploy_template.py python-ironicclient-2.7.0/ironicclient/osc/v1/baremetal_deploy_template.py --- python-ironicclient-2.6.0/ironicclient/osc/v1/baremetal_deploy_template.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ironicclient-2.7.0/ironicclient/osc/v1/baremetal_deploy_template.py 2019-03-08 00:29:10.000000000 +0000 @@ -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='', + help=_('Unique name for this deploy template. Must be a valid ' + 'trait name') + ) + parser.add_argument( + '--uuid', + dest='uuid', + metavar='', + help=_('UUID of the deploy template.')) + parser.add_argument( + '--extra', + metavar="", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) + parser.add_argument( + '--steps', + metavar="", + 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="