Add CLI support for runbooks

Enable CRUD support, manual cleaning and servicing with runbooks
from the command line interface.

Demo Video: https://youtu.be/00PJS4SXFYQ

Change-Id: Iec672c505a245991db72afbb9b668220f845ca81
This commit is contained in:
cid 2024-07-11 18:48:14 +01:00 committed by Jay Faulkner
parent 85542f0caf
commit 1035b2b238
13 changed files with 1462 additions and 29 deletions

View File

@ -37,7 +37,7 @@ from ironicclient import exc
# http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa
# for full details. # for full details.
DEFAULT_VER = '1.9' DEFAULT_VER = '1.9'
LAST_KNOWN_API_VERSION = 88 LAST_KNOWN_API_VERSION = 92
LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION)
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)

View File

@ -214,7 +214,7 @@ def common_params_for_list(args, fields, field_labels):
def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None, def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None,
fields=None, detail=False): fields=None, detail=False, project=None, public=None):
"""Generate common filters for any list request. """Generate common filters for any list request.
:param marker: entity ID from which to start returning entities. :param marker: entity ID from which to start returning entities.
@ -237,6 +237,10 @@ def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None,
filters.append('sort_key=%s' % sort_key) filters.append('sort_key=%s' % sort_key)
if sort_dir is not None: if sort_dir is not None:
filters.append('sort_dir=%s' % sort_dir) filters.append('sort_dir=%s' % sort_dir)
if project is not None:
filters.append('project=%s' % project)
if public is not None:
filters.append('public=True')
if fields is not None: if fields is not None:
filters.append('fields=%s' % ','.join(fields)) filters.append('fields=%s' % ','.join(fields))
if detail: if detail:

View File

@ -80,6 +80,8 @@ class ProvisionStateBaremetalNode(command.Command):
baremetal_client = self.app.client_manager.baremetal baremetal_client = self.app.client_manager.baremetal
runbook = getattr(parsed_args, 'runbook', None)
clean_steps = getattr(parsed_args, 'clean_steps', None) clean_steps = getattr(parsed_args, 'clean_steps', None)
clean_steps = utils.handle_json_arg(clean_steps, 'clean steps') clean_steps = utils.handle_json_arg(clean_steps, 'clean steps')
@ -109,7 +111,8 @@ class ProvisionStateBaremetalNode(command.Command):
cleansteps=clean_steps, cleansteps=clean_steps,
deploysteps=deploy_steps, deploysteps=deploy_steps,
rescue_password=rescue_password, rescue_password=rescue_password,
servicesteps=service_steps) servicesteps=service_steps,
runbook=runbook)
class ProvisionStateWithWait(ProvisionStateBaremetalNode): class ProvisionStateWithWait(ProvisionStateBaremetalNode):
@ -289,18 +292,22 @@ class CleanBaremetalNode(ProvisionStateWithWait):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super(CleanBaremetalNode, self).get_parser(prog_name) parser = super(CleanBaremetalNode, self).get_parser(prog_name)
clean_group = parser.add_mutually_exclusive_group(required=True)
parser.add_argument( clean_group.add_argument(
'--clean-steps', '--clean-steps',
metavar='<clean-steps>', metavar='<clean-steps>',
required=True,
default=None,
help=_("The clean steps. May be the path to a YAML file " help=_("The clean steps. May be the path to a YAML file "
"containing the clean steps; OR '-', with the clean steps " "containing the clean steps; OR '-', with the clean steps "
"being read from standard input; OR a JSON string. The " "being read from standard input; OR a JSON string. The "
"value should be a list of clean-step dictionaries; each " "value should be a list of clean-step dictionaries; each "
"dictionary should have keys 'interface' and 'step', and " "dictionary should have keys 'interface' and 'step', and "
"optional key 'args'.")) "optional key 'args'."))
clean_group.add_argument(
'--runbook',
metavar='<runbook>',
help=_("The identifier of a predefined runbook to use for "
"cleaning."))
return parser return parser
@ -312,18 +319,22 @@ class ServiceBaremetalNode(ProvisionStateWithWait):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super(ServiceBaremetalNode, self).get_parser(prog_name) parser = super(ServiceBaremetalNode, self).get_parser(prog_name)
service_group = parser.add_mutually_exclusive_group(required=True)
parser.add_argument( service_group.add_argument(
'--service-steps', '--service-steps',
metavar='<service-steps>', metavar='<service-steps>',
required=True,
default=None,
help=_("The service steps. May be the path to a YAML file " help=_("The service steps. May be the path to a YAML file "
"containing the service steps; OR '-', with the service " "containing the service steps; OR '-', with the service "
" steps being read from standard input; OR a JSON string. " " steps being read from standard input; OR a JSON string. "
"The value should be a list of service-step dictionaries; " "The value should be a list of service-step dictionaries; "
"each dictionary should have keys 'interface' and 'step', " "each dictionary should have keys 'interface' and 'step', "
"and optional key 'args'.")) "and optional key 'args'."))
service_group.add_argument(
'--runbook',
metavar='<runbook>',
help=_("The identifier of a predefined runbook to use for "
"servicing."))
return parser return parser

View File

@ -0,0 +1,403 @@
# 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
_RUNBOOK_STEPS_HELP = _(
"The runbook steps. May be the path to a YAML file containing the "
"runbook steps; OR '-', with the runbook steps being read from standard "
"input; OR a JSON string. The value should be a list of runbook step "
"dictionaries; each dictionary should have keys 'interface', 'step', "
"'args' and 'order'.")
class CreateBaremetalRunbook(command.ShowOne):
"""Create a new runbook"""
log = logging.getLogger(__name__ + ".CreateBaremetalRunbook")
def get_parser(self, prog_name):
parser = super(CreateBaremetalRunbook, self).get_parser(
prog_name)
parser.add_argument(
'--name',
metavar='<name>',
required=True,
help=_('Unique name for this runbook. Must be a valid '
'trait name')
)
parser.add_argument(
'--uuid',
dest='uuid',
metavar='<uuid>',
help=_('UUID of the runbook.'))
parser.add_argument(
'--public',
metavar='<public>',
help=_('Whether the runbook will be private or public.')
)
parser.add_argument(
'--owner',
metavar='<owner>',
help=_('Owner of the runbook.')
)
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=_RUNBOOK_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, 'runbook steps')
field_list = ['name', 'uuid', 'owner', 'public', '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')
runbook = baremetal_client.runbook.create(steps=steps,
**fields)
data = dict([(f, getattr(runbook, f, '')) for f in
res_fields.RUNBOOK_DETAILED_RESOURCE.fields])
return self.dict2columns(data)
class ShowBaremetalRunbook(command.ShowOne):
"""Show baremetal runbook details."""
log = logging.getLogger(__name__ + ".ShowBaremetalRunbook")
def get_parser(self, prog_name):
parser = super(ShowBaremetalRunbook, self).get_parser(prog_name)
parser.add_argument(
"runbook",
metavar="<runbook>",
help=_("Name or UUID of the runbook.")
)
parser.add_argument(
'--fields',
nargs='+',
dest='fields',
metavar='<field>',
action='append',
choices=res_fields.RUNBOOK_DETAILED_RESOURCE.fields,
default=[],
help=_("One or more runbook 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
runbook = baremetal_client.runbook.get(
parsed_args.runbook, fields=fields)._info
runbook.pop("links", None)
return zip(*sorted(runbook.items()))
class SetBaremetalRunbook(command.Command):
"""Set baremetal runbook properties."""
log = logging.getLogger(__name__ + ".SetBaremetalRunbook")
def get_parser(self, prog_name):
parser = super(SetBaremetalRunbook, self).get_parser(prog_name)
parser.add_argument(
'runbook',
metavar='<runbook>',
help=_("Name or UUID of the runbook")
)
parser.add_argument(
'--name',
metavar='<name>',
help=_('Set unique name of the runbook. Must be a valid '
'trait name.')
)
parser.add_argument(
'--public',
metavar='<public>',
help=_('Make a private runbook public.')
)
parser.add_argument(
'--owner',
metavar='<owner>',
help=_('Set owner of a runbook.')
)
parser.add_argument(
'--steps',
metavar="<steps>",
help=_RUNBOOK_STEPS_HELP
)
parser.add_argument(
"--extra",
metavar="<key=value>",
action='append',
help=_('Extra to set on this baremetal runbook '
'(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.owner:
owner = ["owner=%s" % parsed_args.owner]
properties.extend(utils.args_array_to_patch('add', owner))
if parsed_args.public:
public = ["public=%s" % parsed_args.public]
properties.extend(utils.args_array_to_patch('add', public))
if parsed_args.steps:
steps = utils.handle_json_arg(parsed_args.steps, 'runbook 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.runbook.update(parsed_args.runbook,
properties)
else:
self.log.warning("Please specify what to set.")
class UnsetBaremetalRunbook(command.Command):
"""Unset baremetal runbook properties."""
log = logging.getLogger(__name__ + ".UnsetBaremetalRunbook")
def get_parser(self, prog_name):
parser = super(UnsetBaremetalRunbook, self).get_parser(
prog_name)
parser.add_argument(
'runbook',
metavar='<runbook>',
help=_("Name or UUID of the runbook")
)
parser.add_argument(
'--name',
action='store_true',
help=_('Unset the name of the runbook.')
)
parser.add_argument(
'--public',
dest='public',
action='store_true',
help=_('Make a public runbook private.')
)
parser.add_argument(
'--owner',
dest='owner',
action='store_true',
help=_('Unset owner of a runbook.')
)
parser.add_argument(
"--step",
metavar="<key>",
action='append',
help=_('Step to unset on this baremetal runbook '
'(repeat option to unset multiple steps).'),
)
parser.add_argument(
"--extra",
metavar="<key>",
action='append',
help=_('Extra to unset on this baremetal runbook '
'(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 = []
for field in ['name', 'owner', 'public']:
if getattr(parsed_args, field):
properties.extend(utils.args_array_to_patch('remove', [field]))
if parsed_args.extra:
properties.extend(utils.args_array_to_patch('remove',
['extra/' + x for x in parsed_args.extra]))
if parsed_args.step:
properties.extend(utils.args_array_to_patch('remove',
['step/' + x for x in parsed_args.step]))
if properties:
baremetal_client.runbook.update(parsed_args.runbook,
properties)
else:
self.log.warning("Please specify what to unset.")
class DeleteBaremetalRunbook(command.Command):
"""Delete runbook(s)."""
log = logging.getLogger(__name__ + ".DeleteBaremetalRunbook")
def get_parser(self, prog_name):
parser = super(DeleteBaremetalRunbook, self).get_parser(
prog_name)
parser.add_argument(
"runbooks",
metavar="<runbook>",
nargs="+",
help=_("Name(s) or UUID(s) of the runbook(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 runbook in parsed_args.runbooks:
try:
baremetal_client.runbook.delete(runbook)
print(_('Deleted runbook %s') % runbook)
except exc.ClientException as e:
failures.append(_("Failed to delete runbook "
"%(runbook)s: %(error)s")
% {'runbook': runbook, 'error': e})
if failures:
raise exc.ClientException("\n".join(failures))
class ListBaremetalRunbook(command.Lister):
"""List baremetal runbooks."""
log = logging.getLogger(__name__ + ".ListBaremetalRunbook")
def get_parser(self, prog_name):
parser = super(ListBaremetalRunbook, self).get_parser(prog_name)
parser.add_argument(
'--limit',
metavar='<limit>',
type=int,
help=_('Maximum number of runbooks to return per request, '
'0 for no limit. Default is the maximum number used '
'by the Baremetal API Service.')
)
parser.add_argument(
'--marker',
metavar='<runbook>',
help=_('Runbook UUID (for example, of the last runbook '
'in the list from a previous request). Returns '
'the list of runbooks after this UUID.')
)
parser.add_argument(
'--sort',
metavar="<key>[:<direction>]",
help=_('Sort output by specified runbook 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 runbooks.")
)
display_group.add_argument(
'--fields',
nargs='+',
dest='fields',
metavar='<field>',
action='append',
default=[],
choices=res_fields.RUNBOOK_DETAILED_RESOURCE.fields,
help=_("One or more runbook 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.RUNBOOK_RESOURCE.fields
labels = res_fields.RUNBOOK_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.RUNBOOK_DETAILED_RESOURCE.fields
labels = res_fields.RUNBOOK_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.runbook.list(**params)
data = oscutils.sort_items(data, parsed_args.sort)
return (labels,
(oscutils.get_item_properties(s, columns) for s in data))

View File

@ -235,6 +235,26 @@ DEPLOY_TEMPLATE = {
'steps': baremetal_deploy_template_steps, 'steps': baremetal_deploy_template_steps,
'extra': baremetal_deploy_template_extra, 'extra': baremetal_deploy_template_extra,
} }
baremetal_runbook_uuid = 'ddd-tttttt-dddd'
baremetal_runbook_name = 'CUSTOM_AWESOME'
baremetal_runbook_owner = 'some_user'
baremetal_runbook_public = False
baremetal_runbook_steps = json.dumps([{
'interface': 'raid',
'step': 'create_configuration',
'args': {},
'order': 1
}])
baremetal_runbook_extra = {'key1': 'value1', 'key2': 'value2'}
RUNBOOK = {
'uuid': baremetal_runbook_uuid,
'name': baremetal_runbook_name,
'owner': baremetal_runbook_owner,
'public': baremetal_runbook_public,
'steps': baremetal_runbook_steps,
'extra': baremetal_runbook_extra,
}
NODE_HISTORY = [ NODE_HISTORY = [
{ {
'uuid': 'abcdef1', 'uuid': 'abcdef1',

View File

@ -59,7 +59,8 @@ class TestAbort(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'abort', cleansteps=None, configdrive=None, 'node_uuid', 'abort', cleansteps=None, configdrive=None,
deploysteps=None, rescue_password=None, servicesteps=None) deploysteps=None, rescue_password=None, servicesteps=None,
runbook=None)
class TestAdopt(TestBaremetal): class TestAdopt(TestBaremetal):
@ -83,7 +84,7 @@ class TestAdopt(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'adopt', 'node_uuid', 'adopt',
cleansteps=None, deploysteps=None, configdrive=None, cleansteps=None, deploysteps=None, configdrive=None,
rescue_password=None, servicesteps=None) rescue_password=None, servicesteps=None, runbook=None)
self.baremetal_mock.node.wait_for_provision_state.assert_not_called() self.baremetal_mock.node.wait_for_provision_state.assert_not_called()
def test_adopt_baremetal_provision_state_active_and_wait(self): def test_adopt_baremetal_provision_state_active_and_wait(self):
@ -103,7 +104,7 @@ class TestAdopt(TestBaremetal):
test_node.set_provision_state.assert_called_once_with( test_node.set_provision_state.assert_called_once_with(
'node_uuid', 'adopt', 'node_uuid', 'adopt',
cleansteps=None, deploysteps=None, configdrive=None, cleansteps=None, deploysteps=None, configdrive=None,
rescue_password=None, servicesteps=None) rescue_password=None, servicesteps=None, runbook=None)
test_node.wait_for_provision_state.assert_called_once_with( test_node.wait_for_provision_state.assert_called_once_with(
['node_uuid'], expected_state='active', ['node_uuid'], expected_state='active',
poll_interval=2, timeout=15) poll_interval=2, timeout=15)
@ -125,7 +126,7 @@ class TestAdopt(TestBaremetal):
test_node.set_provision_state.assert_called_once_with( test_node.set_provision_state.assert_called_once_with(
'node_uuid', 'adopt', 'node_uuid', 'adopt',
cleansteps=None, deploysteps=None, configdrive=None, cleansteps=None, deploysteps=None, configdrive=None,
rescue_password=None, servicesteps=None) rescue_password=None, servicesteps=None, runbook=None)
test_node.wait_for_provision_state.assert_called_once_with( test_node.wait_for_provision_state.assert_called_once_with(
['node_uuid'], expected_state='active', ['node_uuid'], expected_state='active',
poll_interval=2, timeout=0) poll_interval=2, timeout=0)
@ -174,7 +175,55 @@ class TestClean(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'clean', cleansteps=steps_dict, configdrive=None, 'node_uuid', 'clean', cleansteps=steps_dict, configdrive=None,
deploysteps=None, rescue_password=None, servicesteps=None) deploysteps=None, rescue_password=None, servicesteps=None,
runbook=None)
def test_clean_with_runbook(self):
runbook_name = 'runbook_name'
arglist = ['--runbook', runbook_name, 'node_uuid']
verifylist = [
('runbook', runbook_name),
('provision_state', 'clean'),
('nodes', ['node_uuid']),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'clean', cleansteps=None, configdrive=None,
deploysteps=None, rescue_password=None, servicesteps=None,
runbook=runbook_name)
def test_clean_with_runbook_and_steps(self):
runbook_name = 'runbook_name'
steps_dict = {
"clean_steps": [{
"interface": "raid",
"step": "create_configuration",
"args": {"create_nonroot_volumes": False}
}, {
"interface": "deploy",
"step": "erase_devices"
}]
}
steps_json = json.dumps(steps_dict)
arglist = ['--runbook', runbook_name, '--clean-steps', steps_json,
'node_uuid']
verifylist = [
('clean_steps', steps_json),
('runbook', runbook_name),
('provision_state', 'clean'),
('nodes', ['node_uuid']),
]
self.assertRaises(oscutils.ParserException,
self.check_parser,
self.cmd, arglist, verifylist)
class TestService(TestBaremetal): class TestService(TestBaremetal):
@ -210,7 +259,54 @@ class TestService(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'service', cleansteps=None, configdrive=None, 'node_uuid', 'service', cleansteps=None, configdrive=None,
deploysteps=None, rescue_password=None, servicesteps=steps_dict) deploysteps=None, rescue_password=None, servicesteps=steps_dict,
runbook=None)
def test_service_with_runbook(self):
runbook_name = 'runbook_name'
arglist = ['--runbook', runbook_name, 'node_uuid']
verifylist = [
('runbook', runbook_name),
('provision_state', 'service'),
('nodes', ['node_uuid']),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'service', cleansteps=None, configdrive=None,
deploysteps=None, rescue_password=None, servicesteps=None,
runbook=runbook_name)
def test_service_with_runbook_and_steps(self):
runbook_name = 'runbook_name'
steps_dict = {
"service_steps": [{
"interface": "raid",
"step": "create_configuration",
"args": {"create_nonroot_volumes": False}
}, {
"interface": "deploy",
"step": "erase_devices"
}]
}
steps_json = json.dumps(steps_dict)
arglist = ['--service-steps', steps_json, '--runbook', runbook_name,
'node_uuid']
verifylist = [
('service_steps', steps_json),
('runbook', runbook_name),
('provision_state', 'service'),
('nodes', ['node_uuid']),
]
self.assertRaises(oscutils.ParserException,
self.check_parser,
self.cmd, arglist, verifylist)
class TestInspect(TestBaremetal): class TestInspect(TestBaremetal):
@ -233,7 +329,8 @@ class TestInspect(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'inspect', cleansteps=None, configdrive=None, 'node_uuid', 'inspect', cleansteps=None, configdrive=None,
deploysteps=None, rescue_password=None, servicesteps=None) deploysteps=None, rescue_password=None, servicesteps=None,
runbook=None)
class TestManage(TestBaremetal): class TestManage(TestBaremetal):
@ -256,7 +353,8 @@ class TestManage(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'manage', cleansteps=None, configdrive=None, 'node_uuid', 'manage', cleansteps=None, configdrive=None,
deploysteps=None, rescue_password=None, servicesteps=None) deploysteps=None, rescue_password=None, servicesteps=None,
runbook=None)
class TestProvide(TestBaremetal): class TestProvide(TestBaremetal):
@ -279,7 +377,8 @@ class TestProvide(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'provide', cleansteps=None, configdrive=None, 'node_uuid', 'provide', cleansteps=None, configdrive=None,
deploysteps=None, rescue_password=None, servicesteps=None) deploysteps=None, rescue_password=None, servicesteps=None,
runbook=None)
class TestRebuild(TestBaremetal): class TestRebuild(TestBaremetal):
@ -302,7 +401,8 @@ class TestRebuild(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'rebuild', cleansteps=None, configdrive=None, 'node_uuid', 'rebuild', cleansteps=None, configdrive=None,
deploysteps=None, rescue_password=None, servicesteps=None) deploysteps=None, rescue_password=None, servicesteps=None,
runbook=None)
class TestUndeploy(TestBaremetal): class TestUndeploy(TestBaremetal):
@ -325,7 +425,8 @@ class TestUndeploy(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'deleted', cleansteps=None, configdrive=None, 'node_uuid', 'deleted', cleansteps=None, configdrive=None,
deploysteps=None, rescue_password=None, servicesteps=None) deploysteps=None, rescue_password=None, servicesteps=None,
runbook=None)
class TestBootdeviceSet(TestBaremetal): class TestBootdeviceSet(TestBaremetal):
@ -1912,7 +2013,7 @@ class TestDeployBaremetalProvisionState(TestBaremetal):
'node_uuid', 'active', 'node_uuid', 'active',
cleansteps=None, deploysteps=[{"interface": "deploy"}], cleansteps=None, deploysteps=[{"interface": "deploy"}],
configdrive='path/to/drive', rescue_password=None, configdrive='path/to/drive', rescue_password=None,
servicesteps=None) servicesteps=None, runbook=None)
def test_deploy_baremetal_provision_state_active_and_configdrive_dict( def test_deploy_baremetal_provision_state_active_and_configdrive_dict(
self): self):
@ -1931,7 +2032,7 @@ class TestDeployBaremetalProvisionState(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'active', 'node_uuid', 'active',
cleansteps=None, deploysteps=None, configdrive={'meta_data': {}}, cleansteps=None, deploysteps=None, configdrive={'meta_data': {}},
rescue_password=None, servicesteps=None) rescue_password=None, servicesteps=None, runbook=None)
def test_deploy_no_wait(self): def test_deploy_no_wait(self):
arglist = ['node_uuid'] arglist = ['node_uuid']
@ -1996,7 +2097,7 @@ class TestDeployBaremetalProvisionState(TestBaremetal):
test_node.set_provision_state.assert_has_calls([ test_node.set_provision_state.assert_has_calls([
mock.call(n, 'active', cleansteps=None, deploysteps=None, mock.call(n, 'active', cleansteps=None, deploysteps=None,
configdrive=None, rescue_password=None, configdrive=None, rescue_password=None,
servicesteps=None) servicesteps=None, runbook=None)
for n in ['node_uuid', 'node_name'] for n in ['node_uuid', 'node_name']
]) ])
test_node.wait_for_provision_state.assert_called_once_with( test_node.wait_for_provision_state.assert_called_once_with(
@ -2220,7 +2321,7 @@ class TestRescueBaremetalProvisionState(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'rescue', cleansteps=None, deploysteps=None, 'node_uuid', 'rescue', cleansteps=None, deploysteps=None,
configdrive=None, rescue_password='supersecret', configdrive=None, rescue_password='supersecret',
servicesteps=None) servicesteps=None, runbook=None)
def test_rescue_baremetal_provision_state_rescue_and_wait(self): def test_rescue_baremetal_provision_state_rescue_and_wait(self):
arglist = ['node_uuid', arglist = ['node_uuid',
@ -2412,7 +2513,7 @@ class TestRebuildBaremetalProvisionState(TestBaremetal):
'node_uuid', 'rebuild', 'node_uuid', 'rebuild',
cleansteps=None, deploysteps=[{"interface": "deploy"}], cleansteps=None, deploysteps=[{"interface": "deploy"}],
configdrive='path/to/drive', rescue_password=None, configdrive='path/to/drive', rescue_password=None,
servicesteps=None) servicesteps=None, runbook=None)
def test_rebuild_no_wait(self): def test_rebuild_no_wait(self):
arglist = ['node_uuid'] arglist = ['node_uuid']
@ -2428,7 +2529,7 @@ class TestRebuildBaremetalProvisionState(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'rebuild', 'node_uuid', 'rebuild',
cleansteps=None, deploysteps=None, configdrive=None, cleansteps=None, deploysteps=None, configdrive=None,
rescue_password=None, servicesteps=None) rescue_password=None, servicesteps=None, runbook=None)
self.baremetal_mock.node.wait_for_provision_state.assert_not_called() self.baremetal_mock.node.wait_for_provision_state.assert_not_called()
@ -2546,7 +2647,8 @@ class TestUnrescueBaremetalProvisionState(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'unrescue', cleansteps=None, deploysteps=None, 'node_uuid', 'unrescue', cleansteps=None, deploysteps=None,
configdrive=None, rescue_password=None, servicesteps=None) configdrive=None, rescue_password=None, servicesteps=None,
runbook=None)
def test_unrescue_baremetal_provision_state_active_and_wait(self): def test_unrescue_baremetal_provision_state_active_and_wait(self):
arglist = ['node_uuid', arglist = ['node_uuid',
@ -4661,7 +4763,8 @@ class TestUnholdBaremetalProvisionState(TestBaremetal):
self.baremetal_mock.node.set_provision_state.assert_called_once_with( self.baremetal_mock.node.set_provision_state.assert_called_once_with(
'node_uuid', 'unhold', cleansteps=None, deploysteps=None, 'node_uuid', 'unhold', cleansteps=None, deploysteps=None,
configdrive=None, rescue_password=None, servicesteps=None) configdrive=None, rescue_password=None, servicesteps=None,
runbook=None)
class TestListFirmwareComponents(TestBaremetal): class TestListFirmwareComponents(TestBaremetal):

View File

@ -0,0 +1,462 @@
# 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
from unittest import mock
from osc_lib.tests import utils as osctestutils
from ironicclient import exc
from ironicclient.osc.v1 import baremetal_runbook
from ironicclient.tests.unit.osc.v1 import fakes as baremetal_fakes
class TestBaremetalRunbook(baremetal_fakes.TestBaremetal):
def setUp(self):
super(TestBaremetalRunbook, self).setUp()
self.baremetal_mock = self.app.client_manager.baremetal
self.baremetal_mock.reset_mock()
class TestCreateBaremetalRunbook(TestBaremetalRunbook):
def setUp(self):
super(TestCreateBaremetalRunbook, self).setUp()
self.baremetal_mock.runbook.create.return_value = (
baremetal_fakes.FakeBaremetalResource(
None,
copy.deepcopy(baremetal_fakes.RUNBOOK),
loaded=True,
))
# Get the command object to test
self.cmd = baremetal_runbook.CreateBaremetalRunbook(
self.app, None)
def test_baremetal_runbook_create(self):
arglist = [
'--name', baremetal_fakes.baremetal_runbook_name,
'--steps', baremetal_fakes.baremetal_runbook_steps,
]
verifylist = [
('name', baremetal_fakes.baremetal_runbook_name),
('steps', baremetal_fakes.baremetal_runbook_steps),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Set expected values
args = {
'name': baremetal_fakes.baremetal_runbook_name,
'steps': json.loads(
baremetal_fakes.baremetal_runbook_steps),
}
self.baremetal_mock.runbook.create.assert_called_once_with(
**args)
def test_baremetal_runbook_create_uuid(self):
arglist = [
'--name', baremetal_fakes.baremetal_runbook_name,
'--steps', baremetal_fakes.baremetal_runbook_steps,
'--uuid', baremetal_fakes.baremetal_runbook_uuid,
]
verifylist = [
('name', baremetal_fakes.baremetal_runbook_name),
('steps', baremetal_fakes.baremetal_runbook_steps),
('uuid', baremetal_fakes.baremetal_runbook_uuid),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
# Set expected values
args = {
'name': baremetal_fakes.baremetal_runbook_name,
'steps': json.loads(
baremetal_fakes.baremetal_runbook_steps),
'uuid': baremetal_fakes.baremetal_runbook_uuid,
}
self.baremetal_mock.runbook.create.assert_called_once_with(
**args)
def test_baremetal_runbook_create_no_name(self):
arglist = [
'--steps', baremetal_fakes.baremetal_runbook_steps,
]
verifylist = [
('steps', baremetal_fakes.baremetal_runbook_steps),
]
self.assertRaises(osctestutils.ParserException,
self.check_parser,
self.cmd, arglist, verifylist)
self.assertFalse(self.baremetal_mock.runbook.create.called)
def test_baremetal_runbook_create_no_steps(self):
arglist = [
'--name', baremetal_fakes.baremetal_runbook_name,
]
verifylist = [
('name', baremetal_fakes.baremetal_runbook_name),
]
self.assertRaises(osctestutils.ParserException,
self.check_parser,
self.cmd, arglist, verifylist)
self.assertFalse(self.baremetal_mock.runbook.create.called)
class TestShowBaremetalRunbook(TestBaremetalRunbook):
def setUp(self):
super(TestShowBaremetalRunbook, self).setUp()
self.baremetal_mock.runbook.get.return_value = (
baremetal_fakes.FakeBaremetalResource(
None,
copy.deepcopy(baremetal_fakes.RUNBOOK),
loaded=True))
self.cmd = baremetal_runbook.ShowBaremetalRunbook(
self.app, None)
def test_baremetal_runbook_show(self):
arglist = [baremetal_fakes.baremetal_runbook_uuid]
verifylist = [('runbook',
baremetal_fakes.baremetal_runbook_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_runbook_uuid]
self.baremetal_mock.runbook.get.assert_called_with(
*args, fields=None)
collist = (
'extra',
'name',
'owner',
'public',
'steps',
'uuid')
self.assertEqual(collist, columns)
datalist = (
baremetal_fakes.baremetal_runbook_extra,
baremetal_fakes.baremetal_runbook_name,
baremetal_fakes.baremetal_runbook_owner,
baremetal_fakes.baremetal_runbook_public,
baremetal_fakes.baremetal_runbook_steps,
baremetal_fakes.baremetal_runbook_uuid)
self.assertEqual(datalist, tuple(data))
def test_baremetal_runbook_show_no_template(self):
arglist = []
verifylist = []
self.assertRaises(osctestutils.ParserException,
self.check_parser,
self.cmd, arglist, verifylist)
class TestBaremetalRunbookSet(TestBaremetalRunbook):
def setUp(self):
super(TestBaremetalRunbookSet, self).setUp()
self.baremetal_mock.runbook.update.return_value = (
baremetal_fakes.FakeBaremetalResource(
None,
copy.deepcopy(baremetal_fakes.RUNBOOK),
loaded=True))
self.cmd = baremetal_runbook.SetBaremetalRunbook(
self.app, None)
def test_baremetal_runbook_set_name(self):
new_name = 'foo'
arglist = [
baremetal_fakes.baremetal_runbook_uuid,
'--name', new_name]
verifylist = [
('runbook', baremetal_fakes.baremetal_runbook_uuid),
('name', new_name)]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.baremetal_mock.runbook.update.assert_called_once_with(
baremetal_fakes.baremetal_runbook_uuid,
[{'path': '/name', 'value': new_name, 'op': 'add'}])
def test_baremetal_runbook_set_steps(self):
arglist = [
baremetal_fakes.baremetal_runbook_uuid,
'--steps', baremetal_fakes.baremetal_runbook_steps]
verifylist = [
('runbook', baremetal_fakes.baremetal_runbook_uuid),
('steps', baremetal_fakes.baremetal_runbook_steps)]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
expected_steps = json.loads(
baremetal_fakes.baremetal_runbook_steps)
self.cmd.take_action(parsed_args)
self.baremetal_mock.runbook.update.assert_called_once_with(
baremetal_fakes.baremetal_runbook_uuid,
[{'path': '/steps', 'value': expected_steps, 'op': 'add'}])
def test_baremetal_runbook_set_no_options(self):
arglist = []
verifylist = []
self.assertRaises(osctestutils.ParserException,
self.check_parser,
self.cmd, arglist, verifylist)
class TestBaremetalRunbookUnset(TestBaremetalRunbook):
def setUp(self):
super(TestBaremetalRunbookUnset, self).setUp()
self.baremetal_mock.runbook.update.return_value = (
baremetal_fakes.FakeBaremetalResource(
None,
copy.deepcopy(baremetal_fakes.RUNBOOK),
loaded=True))
self.cmd = baremetal_runbook.UnsetBaremetalRunbook(
self.app, None)
def test_baremetal_runbook_unset_extra(self):
arglist = [
baremetal_fakes.baremetal_runbook_uuid, '--extra', 'key1']
verifylist = [('runbook',
baremetal_fakes.baremetal_runbook_uuid),
('extra', ['key1'])]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.baremetal_mock.runbook.update.assert_called_once_with(
baremetal_fakes.baremetal_runbook_uuid,
[{'path': '/extra/key1', 'op': 'remove'}])
def test_baremetal_runbook_unset_multiple_extras(self):
arglist = [
baremetal_fakes.baremetal_runbook_uuid,
'--extra', 'key1', '--extra', 'key2']
verifylist = [('runbook',
baremetal_fakes.baremetal_runbook_uuid),
('extra', ['key1', 'key2'])]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.baremetal_mock.runbook.update.assert_called_once_with(
baremetal_fakes.baremetal_runbook_uuid,
[{'path': '/extra/key1', 'op': 'remove'},
{'path': '/extra/key2', 'op': 'remove'}])
def test_baremetal_runbook_unset_no_options(self):
arglist = []
verifylist = []
self.assertRaises(osctestutils.ParserException,
self.check_parser,
self.cmd, arglist, verifylist)
def test_baremetal_runbook_unset_no_property(self):
uuid = baremetal_fakes.baremetal_runbook_uuid
arglist = [uuid]
verifylist = [('runbook', uuid)]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.assertFalse(self.baremetal_mock.runbook.update.called)
class TestBaremetalRunbookDelete(TestBaremetalRunbook):
def setUp(self):
super(TestBaremetalRunbookDelete, self).setUp()
self.cmd = baremetal_runbook.DeleteBaremetalRunbook(
self.app, None)
def test_baremetal_runbook_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.runbook.delete.assert_called_with(args)
def test_baremetal_runbook_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.runbook.delete.assert_has_calls(
[mock.call(x) for x in args])
self.assertEqual(
2, self.baremetal_mock.runbook.delete.call_count)
def test_baremetal_runbook_delete_multiple_with_fail(self):
arglist = ['zzz-zzzzzz-zzzz', 'badname']
verifylist = []
self.baremetal_mock.runbook.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.runbook.delete.assert_has_calls(
[mock.call(x) for x in args])
self.assertEqual(
2, self.baremetal_mock.runbook.delete.call_count)
def test_baremetal_runbook_delete_no_template(self):
arglist = []
verifylist = []
self.assertRaises(osctestutils.ParserException,
self.check_parser,
self.cmd, arglist, verifylist)
class TestBaremetalRunbookList(TestBaremetalRunbook):
def setUp(self):
super(TestBaremetalRunbookList, self).setUp()
self.baremetal_mock.runbook.list.return_value = [
baremetal_fakes.FakeBaremetalResource(
None,
copy.deepcopy(baremetal_fakes.RUNBOOK),
loaded=True)
]
self.cmd = baremetal_runbook.ListBaremetalRunbook(
self.app, None)
def test_baremetal_runbook_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.runbook.list.assert_called_with(**kwargs)
collist = (
"UUID",
"Name")
self.assertEqual(collist, columns)
datalist = ((
baremetal_fakes.baremetal_runbook_uuid,
baremetal_fakes.baremetal_runbook_name
), )
self.assertEqual(datalist, tuple(data))
def test_baremetal_runbook_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.runbook.list.assert_called_with(**kwargs)
collist = ('UUID',
'Name',
'Owner',
'Public',
'Steps',
'Extra',
'Created At',
'Updated At')
self.assertEqual(collist, columns)
datalist = ((
baremetal_fakes.baremetal_runbook_uuid,
baremetal_fakes.baremetal_runbook_name,
baremetal_fakes.baremetal_runbook_owner,
baremetal_fakes.baremetal_runbook_public,
baremetal_fakes.baremetal_runbook_steps,
baremetal_fakes.baremetal_runbook_extra,
'',
'',
), )
self.assertEqual(datalist, tuple(data))
def test_baremetal_runbook_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.runbook.list.assert_called_with(**kwargs)
def test_baremetal_runbook_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.runbook.list.assert_called_with(**kwargs)
def test_baremetal_runbook_list_invalid_fields(self):
arglist = ['--fields', 'uuid', 'invalid']
verifylist = [('fields', [['uuid', 'invalid']])]
self.assertRaises(osctestutils.ParserException,
self.check_parser,
self.cmd, arglist, verifylist)

View File

@ -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.runbook
RUNBOOK = {'uuid': '11111111-2222-3333-4444-555555555555',
'name': 'CUSTOM_RUNBOOK',
'steps': {},
'extra': {}}
RUNBOOK2 = {'uuid': '55555555-4444-3333-2222-111111111111',
'name': 'CUSTOM_RUNBOOK2',
'steps': {},
'extra': {}}
CREATE_RUNBOOK = copy.deepcopy(RUNBOOK)
del CREATE_RUNBOOK['uuid']
CREATE_RUNBOOK_WITH_UUID = copy.deepcopy(RUNBOOK)
UPDATED_RUNBOOK = copy.deepcopy(RUNBOOK)
NEW_NAME = 'CUSTOM_RUNBOOK3'
UPDATED_RUNBOOK['name'] = NEW_NAME
fake_responses = {
'/v1/runbooks':
{
'GET': (
{},
{"runbooks": [RUNBOOK]},
),
'POST': (
{},
CREATE_RUNBOOK,
),
},
'/v1/runbooks/?detail=True':
{
'GET': (
{},
{"runbooks": [RUNBOOK]},
),
},
'/v1/runbooks/?fields=uuid,name':
{
'GET': (
{},
{"runbooks": [RUNBOOK]},
),
},
'/v1/runbooks/%s' % RUNBOOK['uuid']:
{
'GET': (
{},
RUNBOOK,
),
'DELETE': (
{},
None,
),
'PATCH': (
{},
UPDATED_RUNBOOK,
),
},
'/v1/runbooks/%s?fields=uuid,name' % RUNBOOK['uuid']:
{
'GET': (
{},
RUNBOOK,
),
},
}
fake_responses_pagination = {
'/v1/runbooks':
{
'GET': (
{},
{"runbooks": [RUNBOOK],
"next": "http://127.0.0.1:6385/v1/runbooks/?limit=1"}
),
},
'/v1/runbooks/?limit=1':
{
'GET': (
{},
{"runbooks": [RUNBOOK2]}
),
},
'/v1/runbooks/?marker=%s' % RUNBOOK['uuid']:
{
'GET': (
{},
{"runbooks": [RUNBOOK2]}
),
},
}
fake_responses_sorting = {
'/v1/runbooks/?sort_key=updated_at':
{
'GET': (
{},
{"runbooks": [RUNBOOK2, RUNBOOK]}
),
},
'/v1/runbooks/?sort_dir=desc':
{
'GET': (
{},
{"runbooks": [RUNBOOK2, RUNBOOK]}
),
},
}
class RunbookManagerTest(testtools.TestCase):
def setUp(self):
super(RunbookManagerTest, self).setUp()
self.api = utils.FakeAPI(fake_responses)
self.mgr = ironicclient.v1.runbook.RunbookManager(
self.api)
def test_runbooks_list(self):
runbooks = self.mgr.list()
expect = [
('GET', '/v1/runbooks', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(runbooks))
def test_runbooks_list_detail(self):
runbooks = self.mgr.list(detail=True)
expect = [
('GET', '/v1/runbooks/?detail=True', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(runbooks))
def test_runbook_list_fields(self):
runbooks = self.mgr.list(fields=['uuid', 'name'])
expect = [
('GET', '/v1/runbooks/?fields=uuid,name', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(runbooks))
def test_runbook_list_detail_and_fields_fail(self):
self.assertRaises(exc.InvalidAttribute, self.mgr.list,
detail=True, fields=['uuid', 'name'])
def test_runbooks_list_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = ironicclient.v1.runbook.RunbookManager(
self.api)
runbooks = self.mgr.list(limit=1)
expect = [
('GET', '/v1/runbooks/?limit=1', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(runbooks, HasLength(1))
def test_runbooks_list_marker(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = ironicclient.v1.runbook.RunbookManager(
self.api)
runbooks = self.mgr.list(marker=RUNBOOK['uuid'])
expect = [
('GET',
'/v1/runbooks/?marker=%s' % RUNBOOK['uuid'], {},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(runbooks, HasLength(1))
def test_runbooks_list_pagination_no_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = ironicclient.v1.runbook.RunbookManager(
self.api)
runbooks = self.mgr.list(limit=0)
expect = [
('GET', '/v1/runbooks', {}, None),
('GET', '/v1/runbooks/?limit=1', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertThat(runbooks, HasLength(2))
def test_runbooks_list_sort_key(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = ironicclient.v1.runbook.RunbookManager(
self.api)
runbooks = self.mgr.list(sort_key='updated_at')
expect = [
('GET', '/v1/runbooks/?sort_key=updated_at', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(runbooks))
def test_runbooks_list_sort_dir(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = ironicclient.v1.runbook.RunbookManager(
self.api)
runbooks = self.mgr.list(sort_dir='desc')
expect = [
('GET', '/v1/runbooks/?sort_dir=desc', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(runbooks))
def test_runbooks_show(self):
runbook = self.mgr.get(RUNBOOK['uuid'])
expect = [
('GET', '/v1/runbooks/%s' % RUNBOOK['uuid'], {},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(RUNBOOK['uuid'], runbook.uuid)
self.assertEqual(RUNBOOK['name'], runbook.name)
self.assertEqual(RUNBOOK['steps'], runbook.steps)
self.assertEqual(RUNBOOK['extra'], runbook.extra)
def test_runbook_show_fields(self):
runbook = self.mgr.get(RUNBOOK['uuid'],
fields=['uuid', 'name'])
expect = [
('GET', '/v1/runbooks/%s?fields=uuid,name' %
RUNBOOK['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(RUNBOOK['uuid'], runbook.uuid)
self.assertEqual(RUNBOOK['name'], runbook.name)
def test_create(self):
runbook = self.mgr.create(**CREATE_RUNBOOK)
expect = [
('POST', '/v1/runbooks', {}, CREATE_RUNBOOK),
]
self.assertEqual(expect, self.api.calls)
self.assertTrue(runbook)
def test_create_with_uuid(self):
runbook = self.mgr.create(**CREATE_RUNBOOK_WITH_UUID)
expect = [
('POST', '/v1/runbooks', {},
CREATE_RUNBOOK_WITH_UUID),
]
self.assertEqual(expect, self.api.calls)
self.assertTrue(runbook)
def test_delete(self):
runbook = self.mgr.delete(
runbook_id=RUNBOOK['uuid'])
expect = [
('DELETE', '/v1/runbooks/%s' % RUNBOOK['uuid'], {},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertIsNone(runbook)
def test_update(self):
patch = {'op': 'replace',
'value': NEW_NAME,
'path': '/name'}
runbook = self.mgr.update(
runbook_id=RUNBOOK['uuid'], patch=patch)
expect = [
('PATCH', '/v1/runbooks/%s' % RUNBOOK['uuid'],
{}, patch),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(NEW_NAME, runbook.name)

View File

@ -27,6 +27,7 @@ from ironicclient.v1 import events
from ironicclient.v1 import node from ironicclient.v1 import node
from ironicclient.v1 import port from ironicclient.v1 import port
from ironicclient.v1 import portgroup from ironicclient.v1 import portgroup
from ironicclient.v1 import runbook
from ironicclient.v1 import volume_connector from ironicclient.v1 import volume_connector
from ironicclient.v1 import volume_target from ironicclient.v1 import volume_target
@ -98,6 +99,7 @@ class Client(object):
self.volume_target = volume_target.VolumeTargetManager( self.volume_target = volume_target.VolumeTargetManager(
self.http_client) self.http_client)
self.driver = driver.DriverManager(self.http_client) self.driver = driver.DriverManager(self.http_client)
self.runbook = runbook.RunbookManager(self.http_client)
self.portgroup = portgroup.PortgroupManager(self.http_client) self.portgroup = portgroup.PortgroupManager(self.http_client)
self.conductor = conductor.ConductorManager(self.http_client) self.conductor = conductor.ConductorManager(self.http_client)
self.events = events.EventManager(self.http_client) self.events = events.EventManager(self.http_client)

View File

@ -729,7 +729,7 @@ class NodeManager(base.CreateManager):
self, node_uuid, state, configdrive=None, cleansteps=None, self, node_uuid, state, configdrive=None, cleansteps=None,
rescue_password=None, os_ironic_api_version=None, rescue_password=None, os_ironic_api_version=None,
global_request_id=None, deploysteps=None, global_request_id=None, deploysteps=None,
servicesteps=None): servicesteps=None, runbook=None):
"""Set the provision state for the node. """Set the provision state for the node.
:param node_uuid: The UUID or name of the node. :param node_uuid: The UUID or name of the node.
@ -767,6 +767,8 @@ class NodeManager(base.CreateManager):
dictionaries; each dictonary should have keys 'interface', 'step', dictionaries; each dictonary should have keys 'interface', 'step',
and optional key 'args' when setting an 'active' nodes to and optional key 'args' when setting an 'active' nodes to
'service'. 'service'.
:param runbook: The identifier of a predefined runbook to use for
provisioning.
:raises: InvalidAttribute if there was an error with the clean steps or :raises: InvalidAttribute if there was an error with the clean steps or
deploy steps deploy steps
:returns: The status of the request :returns: The status of the request
@ -807,6 +809,9 @@ class NodeManager(base.CreateManager):
if servicesteps: if servicesteps:
body['service_steps'] = servicesteps body['service_steps'] = servicesteps
if runbook:
body['runbook'] = runbook
return self.update(path, body, http_method='PUT', return self.update(path, body, http_method='PUT',
os_ironic_api_version=os_ironic_api_version, os_ironic_api_version=os_ironic_api_version,
global_request_id=global_request_id) global_request_id=global_request_id)

View File

@ -158,6 +158,7 @@ class Resource(object):
'children': 'Child Nodes', 'children': 'Child Nodes',
'firmware_interface': 'Firmware Interface', 'firmware_interface': 'Firmware Interface',
'port_name': 'Port Name', 'port_name': 'Port Name',
'public': 'Public'
} }
def __init__(self, field_ids, sort_excluded=None, override_labels=None): def __init__(self, field_ids, sort_excluded=None, override_labels=None):
@ -606,6 +607,25 @@ DEPLOY_TEMPLATE_RESOURCE = Resource(
], ],
) )
# Runbooks
RUNBOOK_DETAILED_RESOURCE = Resource(
['uuid',
'name',
'owner',
'public',
'steps',
'extra',
'created_at',
'updated_at'
],
sort_excluded=['extra', 'steps']
)
RUNBOOK_RESOURCE = Resource(
['uuid',
'name',
],
)
NODE_HISTORY_RESOURCE = Resource( NODE_HISTORY_RESOURCE = Resource(
['uuid', ['uuid',

106
ironicclient/v1/runbook.py Normal file
View File

@ -0,0 +1,106 @@
# 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 Runbook(base.Resource):
def __repr__(self):
return "<Runbook %s>" % self._info
class RunbookManager(base.CreateManager):
resource_class = Runbook
_creation_attributes = ['extra', 'name', 'owner', 'public', 'steps',
'uuid']
_resource_name = 'runbooks'
def list(self, limit=None, marker=None, sort_key=None, sort_dir=None,
detail=False, fields=None, os_ironic_api_version=None,
global_request_id=None, project=None, public=None):
"""Retrieve a list of runbooks.
: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 runbooks to return.
2) limit == 0, return the entire list of runbooks.
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 runbooks.
: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.
:param os_ironic_api_version: String version (e.g. "1.35") to use for
the request. If not specified, the client's default is used.
:param global_request_id: String containing global request ID header
value (in form "req-<UUID>") to use for the request.
:returns: A list of runbooks.
"""
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, public=public,
project=project)
path = ''
if filters:
path += '?' + '&'.join(filters)
header_values = {"os_ironic_api_version": os_ironic_api_version,
"global_request_id": global_request_id}
if limit is None:
return self._list(self._path(path), "runbooks",
**header_values)
else:
return self._list_pagination(self._path(path), "runbooks",
limit=limit, **header_values)
def get(self, runbook_id, fields=None, os_ironic_api_version=None,
global_request_id=None):
return self._get(resource_id=runbook_id, fields=fields,
os_ironic_api_version=os_ironic_api_version,
global_request_id=global_request_id)
def delete(self, runbook_id, os_ironic_api_version=None,
global_request_id=None):
return self._delete(resource_id=runbook_id,
os_ironic_api_version=os_ironic_api_version,
global_request_id=global_request_id)
def update(self, runbook_id, patch, os_ironic_api_version=None,
global_request_id=None):
return self._update(resource_id=runbook_id, patch=patch,
os_ironic_api_version=os_ironic_api_version,
global_request_id=global_request_id)

View File

@ -132,6 +132,12 @@ openstack.baremetal.v1 =
baremetal_volume_target_unset = ironicclient.osc.v1.baremetal_volume_target:UnsetBaremetalVolumeTarget baremetal_volume_target_unset = ironicclient.osc.v1.baremetal_volume_target:UnsetBaremetalVolumeTarget
baremetal_conductor_list = ironicclient.osc.v1.baremetal_conductor:ListBaremetalConductor baremetal_conductor_list = ironicclient.osc.v1.baremetal_conductor:ListBaremetalConductor
baremetal_conductor_show = ironicclient.osc.v1.baremetal_conductor:ShowBaremetalConductor baremetal_conductor_show = ironicclient.osc.v1.baremetal_conductor:ShowBaremetalConductor
baremetal_runbook_create = ironicclient.osc.v1.baremetal_runbook:CreateBaremetalRunbook
baremetal_runbook_delete = ironicclient.osc.v1.baremetal_runbook:DeleteBaremetalRunbook
baremetal_runbook_list = ironicclient.osc.v1.baremetal_runbook:ListBaremetalRunbook
baremetal_runbook_set = ironicclient.osc.v1.baremetal_runbook:SetBaremetalRunbook
baremetal_runbook_unset = ironicclient.osc.v1.baremetal_runbook:UnsetBaremetalRunbook
baremetal_runbook_show = ironicclient.osc.v1.baremetal_runbook:ShowBaremetalRunbook
[extras] [extras]
cli = cli =