diff options
author | Dmitry Tantsur <divius.inside@gmail.com> | 2019-02-12 16:24:05 +0100 |
---|---|---|
committer | Dmitry Tantsur <divius.inside@gmail.com> | 2019-02-16 15:51:38 +0100 |
commit | e0708a16efa48d872a9f088af856809f523f03dc (patch) | |
tree | b456e4c82c8402ccaa5a68e4dd268ed82ab07430 /ironicclient | |
parent | e8a6d447f803c115ed57064e6fada3e9d6f30794 (diff) | |
download | python-ironicclient-e0708a16efa48d872a9f088af856809f523f03dc.tar.gz |
Allocation API: client API and CLI
Adds the Python API to create/list/view/delete allocations, as well
as the OpenStackClient commands.
Change-Id: Ib97ee888c4a7b6dfa38934f02372284aa4c781a0
Story: #2004341
Task: #28028
Diffstat (limited to 'ironicclient')
-rw-r--r-- | ironicclient/common/utils.py | 21 | ||||
-rw-r--r-- | ironicclient/osc/v1/baremetal_allocation.py | 269 | ||||
-rw-r--r-- | ironicclient/tests/functional/osc/v1/base.py | 53 | ||||
-rw-r--r-- | ironicclient/tests/functional/osc/v1/test_baremetal_allocation.py | 160 | ||||
-rw-r--r-- | ironicclient/tests/unit/osc/v1/fakes.py | 10 | ||||
-rw-r--r-- | ironicclient/tests/unit/osc/v1/test_baremetal_allocation.py | 471 | ||||
-rw-r--r-- | ironicclient/tests/unit/osc/v1/test_baremetal_node.py | 1 | ||||
-rw-r--r-- | ironicclient/tests/unit/v1/test_allocation.py | 330 | ||||
-rw-r--r-- | ironicclient/tests/unit/v1/test_node_shell.py | 3 | ||||
-rw-r--r-- | ironicclient/v1/allocation.py | 141 | ||||
-rw-r--r-- | ironicclient/v1/client.py | 2 | ||||
-rw-r--r-- | ironicclient/v1/node.py | 23 | ||||
-rw-r--r-- | ironicclient/v1/resource_fields.py | 34 |
13 files changed, 1499 insertions, 19 deletions
diff --git a/ironicclient/common/utils.py b/ironicclient/common/utils.py index ac6200a..c09ae84 100644 --- a/ironicclient/common/utils.py +++ b/ironicclient/common/utils.py @@ -24,6 +24,7 @@ import shutil import subprocess import sys import tempfile +import time from oslo_serialization import base64 from oslo_utils import strutils @@ -390,3 +391,23 @@ def handle_json_or_file_arg(json_arg): 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) diff --git a/ironicclient/osc/v1/baremetal_allocation.py b/ironicclient/osc/v1/baremetal_allocation.py new file mode 100644 index 0000000..31de0cf --- /dev/null +++ b/ironicclient/osc/v1/baremetal_allocation.py @@ -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="<key=value>", + 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='<time-out>', + 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="<id>", + help=_("UUID or name of the allocation")) + parser.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='<field>', + 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='<limit>', + 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='<allocation>', + help=_('Port group 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="<key>[:<direction>]", + 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='<node>', + help=_("Only list allocations of this node (name or UUID).")) + parser.add_argument( + '--resource-class', + metavar='<resource_class>', + help=_("Only list allocations with this resource class.")) + parser.add_argument( + '--state', + metavar='<state>', + 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='<field>', + 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="<allocation>", + 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 --git a/ironicclient/tests/functional/osc/v1/base.py b/ironicclient/tests/functional/osc/v1/base.py index 365a349..b2ce630 100644 --- a/ironicclient/tests/functional/osc/v1/base.py +++ b/ironicclient/tests/functional/osc/v1/base.py @@ -348,3 +348,56 @@ class TestCase(base.FunctionalTestBase): output = self.openstack('baremetal conductor list {0} {1}' .format(opts, params)) return json.loads(output) + + def allocation_create(self, resource_class='allocation-test', params=''): + opts = self.get_opts() + output = self.openstack('baremetal allocation create {0} ' + '--resource-class {1} {2}' + .format(opts, resource_class, params)) + allocation = json.loads(output) + self.addCleanup(self.allocation_delete, allocation['uuid'], True) + if not output: + self.fail('Baremetal allocation has not been created!') + + return allocation + + def allocation_list(self, fields=None, params=''): + """List baremetal allocations. + + :param List fields: List of fields to show + :param String params: Additional kwargs + :return: list of JSON allocation objects + """ + opts = self.get_opts(fields=fields) + output = self.openstack('baremetal allocation list {0} {1}' + .format(opts, params)) + return json.loads(output) + + def allocation_show(self, identifier, fields=None, params=''): + """Show specified baremetal allocation. + + :param String identifier: Name or UUID of the allocation + :param List fields: List of fields to show + :param List params: Additional kwargs + :return: JSON object of allocation + """ + opts = self.get_opts(fields) + output = self.openstack('baremetal allocation show {0} {1} {2}' + .format(opts, identifier, params)) + return json.loads(output) + + def allocation_delete(self, identifier, ignore_exceptions=False): + """Try to delete baremetal allocation by name or UUID. + + :param String identifier: Name or UUID of the allocation + :param Bool ignore_exceptions: Ignore exception (needed for cleanUp) + :return: raw values output + :raise: CommandFailed exception when command fails to delete + an allocation + """ + try: + return self.openstack('baremetal allocation delete {0}' + .format(identifier)) + except exceptions.CommandFailed: + if not ignore_exceptions: + raise diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_allocation.py b/ironicclient/tests/functional/osc/v1/test_baremetal_allocation.py new file mode 100644 index 0000000..d92ef7d --- /dev/null +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_allocation.py @@ -0,0 +1,160 @@ +# 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 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 BaremetalAllocationTests(base.TestCase): + """Functional tests for baremetal allocation commands.""" + + def test_create(self): + """Check baremetal allocation create command. + + Test steps: + 1) Create baremetal allocation in setUp. + 2) Check that allocation successfully created. + """ + allocation_info = self.allocation_create() + self.assertTrue(allocation_info['resource_class']) + self.assertEqual(allocation_info['state'], 'allocating') + + allocation_list = self.allocation_list() + self.assertIn(allocation_info['uuid'], + [x['UUID'] for x in allocation_list]) + + def test_create_name_uuid(self): + """Check baremetal allocation create command with name and UUID. + + Test steps: + 1) Create baremetal allocation with specified name and UUID. + 2) Check that allocation successfully created. + """ + uuid = data_utils.rand_uuid() + name = data_utils.rand_name('baremetal-allocation') + allocation_info = self.allocation_create( + params='--uuid {0} --name {1}'.format(uuid, name)) + self.assertEqual(allocation_info['uuid'], uuid) + self.assertEqual(allocation_info['name'], name) + self.assertTrue(allocation_info['resource_class']) + self.assertEqual(allocation_info['state'], 'allocating') + + allocation_list = self.allocation_list() + self.assertIn(uuid, [x['UUID'] for x in allocation_list]) + self.assertIn(name, [x['Name'] for x in allocation_list]) + + def test_create_traits(self): + """Check baremetal allocation create command with traits. + + Test steps: + 1) Create baremetal allocation with specified traits. + 2) Check that allocation successfully created. + """ + allocation_info = self.allocation_create( + params='--trait CUSTOM_1 --trait CUSTOM_2') + self.assertTrue(allocation_info['resource_class']) + self.assertEqual(allocation_info['state'], 'allocating') + self.assertIn('CUSTOM_1', allocation_info['traits']) + self.assertIn('CUSTOM_2', allocation_info['traits']) + + def test_create_candidate_nodes(self): + """Check baremetal allocation create command with candidate nodes. + + Test steps: + 1) Create two nodes. + 2) Create baremetal allocation with specified traits. + 3) Check that allocation successfully created. + """ + name = data_utils.rand_name('baremetal-allocation') + node1 = self.node_create(name=name) + node2 = self.node_create() + allocation_info = self.allocation_create( + params='--candidate-node {0} --candidate-node {1}' + .format(node1['name'], node2['uuid'])) + self.assertEqual(allocation_info['state'], 'allocating') + # NOTE(dtantsur): names are converted to uuids in the API + self.assertIn(node1['uuid'], allocation_info['candidate_nodes']) + self.assertIn(node2['uuid'], allocation_info['candidate_nodes']) + + @ddt.data('name', 'uuid') + def test_delete(self, key): + """Check baremetal allocation delete command with name/UUID argument. + + Test steps: + 1) Create baremetal allocation. + 2) Delete baremetal allocation by name/UUID. + 3) Check that allocation deleted successfully. + """ + name = data_utils.rand_name('baremetal-allocation') + allocation = self.allocation_create(params='--name {}'.format(name)) + output = self.allocation_delete(allocation[key]) + self.assertIn('Deleted allocation {0}'.format(allocation[key]), output) + + allocation_list = self.allocation_list() + self.assertNotIn(allocation['name'], + [x['Name'] for x in allocation_list]) + self.assertNotIn(allocation['uuid'], + [x['UUID'] for x in allocation_list]) + + @ddt.data('name', 'uuid') + def test_show(self, key): + """Check baremetal allocation show command with name and UUID. + + Test steps: + 1) Create baremetal allocation. + 2) Show baremetal allocation calling it with name and UUID arguments. + 3) Check name, uuid and resource_class in allocation show output. + """ + name = data_utils.rand_name('baremetal-allocation') + allocation = self.allocation_create(params='--name {}'.format(name)) + result = self.allocation_show(allocation[key], + ['name', 'uuid', 'resource_class']) + self.assertEqual(allocation['name'], result['name']) + self.assertEqual(allocation['uuid'], result['uuid']) + self.assertTrue(result['resource_class']) + self.assertNotIn('state', result) + + @ddt.data( + ('--uuid', '', 'expected one argument'), + ('--uuid', '!@#$^*&%^', 'Expected a UUID'), + ('--extra', '', 'expected one argument'), + ('--name', '', 'expected one argument'), + ('--name', 'not/a/name', 'invalid name'), + ('--resource-class', '', 'expected one argument'), + ('--resource-class', 'x' * 81, + 'Value should have a maximum character requirement of 80'), + ('--trait', '', 'expected one argument'), + ('--trait', 'foo', + 'A custom trait must start with the prefix CUSTOM_'), + ('--candidate-node', '', 'expected one argument'), + ('--candidate-node', 'banana?', 'Expected a logical name or UUID'), + ('--wait', 'meow', 'invalid int value')) + @ddt.unpack + def test_create_negative(self, argument, value, ex_text): + """Check errors on invalid input parameters.""" + base_cmd = 'baremetal allocation create' + if argument != '--resource-class': + base_cmd += ' --resource-class allocation-test' + command = self.construct_cmd(base_cmd, argument, value) + self.assertRaisesRegex(exceptions.CommandFailed, ex_text, + self.openstack, command) + + def test_create_no_resource_class(self): + """Check errors on missing resource class.""" + base_cmd = 'baremetal allocation create' + self.assertRaisesRegex(exceptions.CommandFailed, + '--resource-class is required', + self.openstack, base_cmd) diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index ebf5b49..1e93fc4 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -191,6 +191,16 @@ CONDUCTOR = { 'drivers': baremetal_drivers, } +baremetal_allocation_state = 'active' +baremetal_resource_class = 'baremetal' +ALLOCATION = { + 'resource_class': baremetal_resource_class, + 'uuid': baremetal_uuid, + 'name': baremetal_name, + 'state': baremetal_allocation_state, + 'node_uuid': baremetal_uuid, +} + class TestBaremetal(utils.TestCommand): diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_allocation.py b/ironicclient/tests/unit/osc/v1/test_baremetal_allocation.py new file mode 100644 index 0000000..7779cfe --- /dev/null +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_allocation.py @@ -0,0 +1,471 @@ +# 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 mock +from osc_lib.tests import utils as osctestutils + +from ironicclient.osc.v1 import baremetal_allocation +from ironicclient.tests.unit.osc.v1 import fakes as baremetal_fakes + + +class TestBaremetalAllocation(baremetal_fakes.TestBaremetal): + + def setUp(self): + super(TestBaremetalAllocation, self).setUp() + + self.baremetal_mock = self.app.client_manager.baremetal + self.baremetal_mock.reset_mock() + + +class TestCreateBaremetalAllocation(TestBaremetalAllocation): + + def setUp(self): + super(TestCreateBaremetalAllocation, self).setUp() + + self.baremetal_mock.allocation.create.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.ALLOCATION), + loaded=True, + )) + + self.baremetal_mock.allocation.wait.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.ALLOCATION), + loaded=True, + )) + + # Get the command object to test + self.cmd = baremetal_allocation.CreateBaremetalAllocation(self.app, + None) + + def test_baremetal_allocation_create(self): + arglist = [ + '--resource-class', baremetal_fakes.baremetal_resource_class, + ] + + verifylist = [ + ('resource_class', baremetal_fakes.baremetal_resource_class), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'resource_class': baremetal_fakes.baremetal_resource_class, + } + + self.baremetal_mock.allocation.create.assert_called_once_with(**args) + + def test_baremetal_allocation_create_wait(self): + arglist = [ + '--resource-class', baremetal_fakes.baremetal_resource_class, + '--wait', + ] + + verifylist = [ + ('resource_class', baremetal_fakes.baremetal_resource_class), + ('wait_timeout', 0), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'resource_class': baremetal_fakes.baremetal_resource_class, + } + + self.baremetal_mock.allocation.create.assert_called_once_with(**args) + self.baremetal_mock.allocation.wait.assert_called_once_with( + baremetal_fakes.ALLOCATION['uuid'], timeout=0) + + def test_baremetal_allocation_create_wait_with_timeout(self): + arglist = [ + '--resource-class', baremetal_fakes.baremetal_resource_class, + '--wait', '3600', + ] + + verifylist = [ + ('resource_class', baremetal_fakes.baremetal_resource_class), + ('wait_timeout', 3600), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'resource_class': baremetal_fakes.baremetal_resource_class, + } + + self.baremetal_mock.allocation.create.assert_called_once_with(**args) + self.baremetal_mock.allocation.wait.assert_called_once_with( + baremetal_fakes.ALLOCATION['uuid'], timeout=3600) + + def test_baremetal_allocation_create_name_extras(self): + arglist = [ + '--resource-class', baremetal_fakes.baremetal_resource_class, + '--uuid', baremetal_fakes.baremetal_uuid, + '--name', baremetal_fakes.baremetal_name, + '--extra', 'key1=value1', + '--extra', 'key2=value2' + ] + + verifylist = [ + ('resource_class', baremetal_fakes.baremetal_resource_class), + ('uuid', baremetal_fakes.baremetal_uuid), + ('name', baremetal_fakes.baremetal_name), + ('extra', ['key1=value1', 'key2=value2']) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'resource_class': baremetal_fakes.baremetal_resource_class, + 'uuid': baremetal_fakes.baremetal_uuid, + 'name': baremetal_fakes.baremetal_name, + 'extra': {'key1': 'value1', 'key2': 'value2'} + } + + self.baremetal_mock.allocation.create.assert_called_once_with(**args) + + def test_baremetal_allocation_create_nodes_and_traits(self): + arglist = [ + '--resource-class', baremetal_fakes.baremetal_resource_class, + '--candidate-node', 'node1', + '--trait', 'CUSTOM_1', + '--candidate-node', 'node2', + '--trait', 'CUSTOM_2', + ] + + verifylist = [ + ('resource_class', baremetal_fakes.baremetal_resource_class), + ('candidate_nodes', ['node1', 'node2']), + ('traits', ['CUSTOM_1', 'CUSTOM_2']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'resource_class': baremetal_fakes.baremetal_resource_class, + 'candidate_nodes': ['node1', 'node2'], + 'traits': ['CUSTOM_1', 'CUSTOM_2'], + } + + self.baremetal_mock.allocation.create.assert_called_once_with(**args) + + def test_baremetal_allocation_create_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestShowBaremetalAllocation(TestBaremetalAllocation): + + def setUp(self): + super(TestShowBaremetalAllocation, self).setUp() + + self.baremetal_mock.allocation.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.ALLOCATION), + loaded=True)) + + self.cmd = baremetal_allocation.ShowBaremetalAllocation(self.app, None) + + def test_baremetal_allocation_show(self): + arglist = [baremetal_fakes.baremetal_uuid] + verifylist = [('allocation', baremetal_fakes.baremetal_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.baremetal_mock.allocation.get.assert_called_once_with( + baremetal_fakes.baremetal_uuid, fields=None) + + collist = ('name', 'node_uuid', 'resource_class', 'state', 'uuid') + self.assertEqual(collist, columns) + + datalist = ( + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_resource_class, + baremetal_fakes.baremetal_allocation_state, + baremetal_fakes.baremetal_uuid, + ) + + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_allocation_show_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestBaremetalAllocationList(TestBaremetalAllocation): + def setUp(self): + super(TestBaremetalAllocationList, self).setUp() + + self.baremetal_mock.allocation.list.return_value = [ + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.ALLOCATION), + loaded=True) + ] + self.cmd = baremetal_allocation.ListBaremetalAllocation(self.app, None) + + def test_baremetal_allocation_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.allocation.list.assert_called_with(**kwargs) + + collist = ( + "UUID", + "Name", + "Resource Class", + "State", + "Node UUID") + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_resource_class, + baremetal_fakes.baremetal_allocation_state, + baremetal_fakes.baremetal_uuid),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_allocation_list_node(self): + arglist = ['--node', baremetal_fakes.baremetal_uuid] + verifylist = [('node', baremetal_fakes.baremetal_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'node': baremetal_fakes.baremetal_uuid, + 'marker': None, + 'limit': None} + self.baremetal_mock.allocation.list.assert_called_once_with(**kwargs) + + collist = ( + "UUID", + "Name", + "Resource Class", + "State", + "Node UUID") + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_resource_class, + baremetal_fakes.baremetal_allocation_state, + baremetal_fakes.baremetal_uuid),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_allocation_list_resource_class(self): + arglist = ['--resource-class', + baremetal_fakes.baremetal_resource_class] + verifylist = [('resource_class', + baremetal_fakes.baremetal_resource_class)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'resource_class': baremetal_fakes.baremetal_resource_class, + 'marker': None, + 'limit': None} + self.baremetal_mock.allocation.list.assert_called_once_with(**kwargs) + + collist = ( + "UUID", + "Name", + "Resource Class", + "State", + "Node UUID") + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_resource_class, + baremetal_fakes.baremetal_allocation_state, + baremetal_fakes.baremetal_uuid),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_allocation_list_state(self): + arglist = ['--state', baremetal_fakes.baremetal_allocation_state] + verifylist = [('state', baremetal_fakes.baremetal_allocation_state)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'state': baremetal_fakes.baremetal_allocation_state, + 'marker': None, + 'limit': None} + self.baremetal_mock.allocation.list.assert_called_once_with(**kwargs) + + collist = ( + "UUID", + "Name", + "Resource Class", + "State", + "Node UUID") + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_resource_class, + baremetal_fakes.baremetal_allocation_state, + baremetal_fakes.baremetal_uuid),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_allocation_list_long(self): + arglist = ['--long'] + verifylist = [('long', True)] + + 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.allocation.list.assert_called_once_with(**kwargs) + + collist = ('UUID', + 'Name', + 'State', + 'Node UUID', + 'Last Error', + 'Resource Class', + 'Traits', + 'Candidate Nodes', + 'Extra', + 'Created At', + 'Updated At') + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_allocation_state, + baremetal_fakes.baremetal_uuid, + '', + baremetal_fakes.baremetal_resource_class, + '', + '', + '', + '', + ''),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_allocation_list_fields(self): + arglist = ['--fields', 'uuid', 'node_uuid'] + verifylist = [('fields', [['uuid', 'node_uuid']])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None, + 'fields': ('uuid', 'node_uuid') + } + self.baremetal_mock.allocation.list.assert_called_once_with(**kwargs) + + def test_baremetal_allocation_list_fields_multiple(self): + arglist = ['--fields', 'uuid', 'node_uuid', '--fields', 'extra'] + verifylist = [('fields', [['uuid', 'node_uuid'], ['extra']])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None, + 'fields': ('uuid', 'node_uuid', 'extra') + } + self.baremetal_mock.allocation.list.assert_called_once_with(**kwargs) + + def test_baremetal_allocation_list_invalid_fields(self): + arglist = ['--fields', 'uuid', 'invalid'] + verifylist = [('fields', [['uuid', 'invalid']])] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestBaremetalAllocationDelete(TestBaremetalAllocation): + + def setUp(self): + super(TestBaremetalAllocationDelete, self).setUp() + + self.baremetal_mock.allocation.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.ALLOCATION), + loaded=True)) + + self.cmd = baremetal_allocation.DeleteBaremetalAllocation(self.app, + None) + + def test_baremetal_allocation_delete(self): + arglist = [baremetal_fakes.baremetal_uuid] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.baremetal_mock.allocation.delete.assert_called_once_with( + baremetal_fakes.baremetal_uuid) + + def test_baremetal_allocation_delete_multiple(self): + arglist = [baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.baremetal_mock.allocation.delete.assert_has_calls( + [mock.call(x) for x in arglist] + ) + self.assertEqual(2, self.baremetal_mock.allocation.delete.call_count) + + def test_baremetal_allocation_delete_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index c11854d..a8f6b38 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -610,6 +610,7 @@ class TestBaremetalList(TestBaremetal): # NOTE(dtantsur): please keep this list sorted for sanity reasons collist = [ + 'Allocation UUID', 'Automated Clean', 'BIOS Interface', 'Boot Interface', diff --git a/ironicclient/tests/unit/v1/test_allocation.py b/ironicclient/tests/unit/v1/test_allocation.py new file mode 100644 index 0000000..e1fa8b5 --- /dev/null +++ b/ironicclient/tests/unit/v1/test_allocation.py @@ -0,0 +1,330 @@ +# 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 mock +import testtools + +from ironicclient import exc +from ironicclient.tests.unit import utils +import ironicclient.v1.allocation + +ALLOCATION = {'uuid': '11111111-2222-3333-4444-555555555555', + 'name': 'Allocation-name', + 'state': 'active', + 'node_uuid': '66666666-7777-8888-9999-000000000000', + 'last_error': None, + 'resource_class': 'baremetal', + 'traits': [], + 'candidate_nodes': [], + 'extra': {}} + +ALLOCATION2 = {'uuid': '55555555-4444-3333-2222-111111111111', + 'name': 'Allocation2-name', + 'state': 'allocating', + 'node_uuid': None, + 'last_error': None, + 'resource_class': 'baremetal', + 'traits': [], + 'candidate_nodes': [], + 'extra': {}} + +CREATE_ALLOCATION = copy.deepcopy(ALLOCATION) +for field in ('state', 'node_uuid', 'last_error'): + del CREATE_ALLOCATION[field] + +fake_responses = { + '/v1/allocations': + { + 'GET': ( + {}, + {"allocations": [ALLOCATION, ALLOCATION2]}, + ), + 'POST': ( + {}, + CREATE_ALLOCATION, + ), + }, + '/v1/allocations/%s' % ALLOCATION['uuid']: + { + 'GET': ( + {}, + ALLOCATION, + ), + 'DELETE': ( + {}, + None, + ), + }, + '/v1/allocations/?node=%s' % ALLOCATION['node_uuid']: + { + 'GET': ( + {}, + {"allocations": [ALLOCATION]}, + ), + }, +} + +fake_responses_pagination = { + '/v1/allocations': + { + 'GET': ( + {}, + {"allocations": [ALLOCATION], + "next": "http://127.0.0.1:6385/v1/allocations/?limit=1"} + ), + }, + '/v1/allocations/?limit=1': + { + 'GET': ( + {}, + {"allocations": [ALLOCATION2]} + ), + }, + '/v1/allocations/?marker=%s' % ALLOCATION['uuid']: + { + 'GET': ( + {}, + {"allocations": [ALLOCATION2]} + ), + }, +} + +fake_responses_sorting = { + '/v1/allocations/?sort_key=updated_at': + { + 'GET': ( + {}, + {"allocations": [ALLOCATION2, ALLOCATION]} + ), + }, + '/v1/allocations/?sort_dir=desc': + { + 'GET': ( + {}, + {"allocations": [ALLOCATION2, ALLOCATION]} + ), + }, +} + + +class AllocationManagerTest(testtools.TestCase): + + def setUp(self): + super(AllocationManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = ironicclient.v1.allocation.AllocationManager(self.api) + + def test_allocations_list(self): + allocations = self.mgr.list() + expect = [ + ('GET', '/v1/allocations', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(allocations)) + + expected_resp = ({}, {"allocations": [ALLOCATION, ALLOCATION2]},) + self.assertEqual(expected_resp, + self.api.responses['/v1/allocations']['GET']) + + def test_allocations_list_by_node(self): + allocations = self.mgr.list(node=ALLOCATION['node_uuid']) + expect = [ + ('GET', '/v1/allocations/?node=%s' % ALLOCATION['node_uuid'], {}, + None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(allocations)) + + expected_resp = ({}, {"allocations": [ALLOCATION, ALLOCATION2]},) + self.assertEqual(expected_resp, + self.api.responses['/v1/allocations']['GET']) + + def test_allocations_show(self): + allocation = self.mgr.get(ALLOCATION['uuid']) + expect = [ + ('GET', '/v1/allocations/%s' % ALLOCATION['uuid'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(ALLOCATION['uuid'], allocation.uuid) + self.assertEqual(ALLOCATION['name'], allocation.name) + self.assertEqual(ALLOCATION['node_uuid'], allocation.node_uuid) + self.assertEqual(ALLOCATION['state'], allocation.state) + self.assertEqual(ALLOCATION['resource_class'], + allocation.resource_class) + + expected_resp = ({}, ALLOCATION,) + self.assertEqual( + expected_resp, + self.api.responses['/v1/allocations/%s' + % ALLOCATION['uuid']]['GET']) + + def test_create(self): + allocation = self.mgr.create(**CREATE_ALLOCATION) + expect = [ + ('POST', '/v1/allocations', {}, CREATE_ALLOCATION), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(allocation) + + self.assertIn( + ALLOCATION, + self.api.responses['/v1/allocations']['GET'][1]['allocations']) + + def test_delete(self): + allocation = self.mgr.delete(allocation_id=ALLOCATION['uuid']) + expect = [ + ('DELETE', '/v1/allocations/%s' % ALLOCATION['uuid'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(allocation) + + expected_resp = ({}, ALLOCATION,) + self.assertEqual( + expected_resp, + self.api.responses['/v1/allocations/%s' + % ALLOCATION['uuid']]['GET']) + + +class AllocationManagerPaginationTest(testtools.TestCase): + + def setUp(self): + super(AllocationManagerPaginationTest, self).setUp() + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.allocation.AllocationManager(self.api) + + def test_allocations_list_limit(self): + allocations = self.mgr.list(limit=1) + expect = [ + ('GET', '/v1/allocations/?limit=1', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(allocations)) + + expected_resp = ( + {}, {"next": "http://127.0.0.1:6385/v1/allocations/?limit=1", + "allocations": [ALLOCATION]},) + self.assertEqual(expected_resp, + self.api.responses['/v1/allocations']['GET']) + + def test_allocations_list_marker(self): + allocations = self.mgr.list(marker=ALLOCATION['uuid']) + expect = [ + ('GET', '/v1/allocations/?marker=%s' % ALLOCATION['uuid'], + {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(allocations)) + + expected_resp = ( + {}, {"next": "http://127.0.0.1:6385/v1/allocations/?limit=1", + "allocations": [ALLOCATION]},) + self.assertEqual(expected_resp, + self.api.responses['/v1/allocations']['GET']) + + def test_allocations_list_pagination_no_limit(self): + allocations = self.mgr.list(limit=0) + expect = [ + ('GET', '/v1/allocations', {}, None), + ('GET', '/v1/allocations/?limit=1', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(allocations)) + + expected_resp = ( + {}, {"next": "http://127.0.0.1:6385/v1/allocations/?limit=1", + "allocations": [ALLOCATION]},) + self.assertEqual(expected_resp, + self.api.responses['/v1/allocations']['GET']) + + +class AllocationManagerSortingTest(testtools.TestCase): + + def setUp(self): + super(AllocationManagerSortingTest, self).setUp() + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.allocation.AllocationManager(self.api) + + def test_allocations_list_sort_key(self): + allocations = self.mgr.list(sort_key='updated_at') + expect = [ + ('GET', '/v1/allocations/?sort_key=updated_at', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(allocations)) + + expected_resp = ({}, {"allocations": [ALLOCATION2, ALLOCATION]},) + self.assertEqual( + expected_resp, + self.api.responses['/v1/allocations/?sort_key=updated_at']['GET']) + + def test_allocations_list_sort_dir(self): + allocations = self.mgr.list(sort_dir='desc') + expect = [ + ('GET', '/v1/allocations/?sort_dir=desc', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(allocations)) + + expected_resp = ({}, {"allocations": [ALLOCATION2, ALLOCATION]},) + self.assertEqual( + expected_resp, + self.api.responses['/v1/allocations/?sort_dir=desc']['GET']) + + +@mock.patch('time.sleep', autospec=True) +@mock.patch('ironicclient.v1.allocation.AllocationManager.get', autospec=True) +class AllocationWaitTest(testtools.TestCase): + + def setUp(self): + super(AllocationWaitTest, self).setUp() + self.mgr = ironicclient.v1.allocation.AllocationManager(mock.Mock()) + + def _fake_allocation(self, state, error=None): + return mock.Mock(state=state, last_error=error) + + def test_success(self, mock_get, mock_sleep): + allocations = [ + self._fake_allocation('allocating'), + self._fake_allocation('allocating'), + self._fake_allocation('active'), + ] + mock_get.side_effect = allocations + + result = self.mgr.wait('alloc1') + self.assertIs(result, allocations[2]) + self.assertEqual(3, mock_get.call_count) + self.assertEqual(2, mock_sleep.call_count) + mock_get.assert_called_with(self.mgr, 'alloc1') + + def test_error(self, mock_get, mock_sleep): + allocations = [ + self._fake_allocation('allocating'), + self._fake_allocation('error'), + ] + mock_get.side_effect = allocations + + self.assertRaises(exc.StateTransitionFailed, + self.mgr.wait, 'alloc1') + + self.assertEqual(2, mock_get.call_count) + self.assertEqual(1, mock_sleep.call_count) + mock_get.assert_called_with(self.mgr, 'alloc1') + + def test_timeout(self, mock_get, mock_sleep): + mock_get.return_value = self._fake_allocation('allocating') + + self.assertRaises(exc.StateTransitionTimeout, + self.mgr.wait, 'alloc1', timeout=0.001) + + mock_get.assert_called_with(self.mgr, 'alloc1') diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index 4c05f8a..ef3fd98 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -33,7 +33,8 @@ class NodeShellTest(utils.BaseTestCase): with mock.patch.object(cliutils, 'print_dict', fake_print_dict): node = object() n_shell._print_node_show(node) - exp = ['automated_clean', + exp = ['allocation_uuid', + 'automated_clean', 'chassis_uuid', 'clean_step', 'created_at', diff --git a/ironicclient/v1/allocation.py b/ironicclient/v1/allocation.py new file mode 100644 index 0000000..255bf03 --- /dev/null +++ b/ironicclient/v1/allocation.py @@ -0,0 +1,141 @@ +# 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 logging + +from ironicclient.common import base +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient import exc + + +LOG = logging.getLogger(__name__) + + +class Allocation(base.Resource): + def __repr__(self): + return "<Allocation %s>" % self._info + + +class AllocationManager(base.CreateManager): + resource_class = Allocation + _resource_name = 'allocations' + _creation_attributes = ['extra', 'name', 'resource_class', 'uuid', + 'traits', 'candidate_nodes'] + + def list(self, resource_class=None, state=None, node=None, limit=None, + marker=None, sort_key=None, sort_dir=None, fields=None): + """Retrieve a list of allocations. + + :param resource_class: Optional, get allocations with this resource + class. + :param state: Optional, get allocations in this state. One of + ``allocating``, ``active`` or ``error``. + :param node: UUID or name of the node of the allocation. + :param marker: Optional, the UUID of an allocation, eg the last + allocation 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 allocations to return. + 2) limit == 0, return the entire list of allocations. + 3) limit == 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 fields: Optional, a list with a specified set of fields + of the resource to be returned. + + :returns: A list of allocations. + :raises: InvalidAttribute if a subset of fields is requested with + detail option set. + + """ + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir, + fields) + for name, value in [('resource_class', resource_class), + ('state', state), ('node', node)]: + if value is not None: + filters.append('%s=%s' % (name, value)) + + if filters: + path = '?' + '&'.join(filters) + else: + path = '' + + if limit is None: + return self._list(self._path(path), "allocations") + else: + return self._list_pagination(self._path(path), "allocations", + limit=limit) + + def get(self, allocation_id, fields=None): + """Get an allocation with the specified identifier. + + :param allocation_id: The UUID or name of an allocation. + :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: an :class:`Allocation` object. + + """ + return self._get(resource_id=allocation_id, fields=fields) + + def delete(self, allocation_id): + """Delete the Allocation. + + :param allocation_id: The UUID or name of an allocation. + """ + return self._delete(resource_id=allocation_id) + + def wait(self, allocation_id, timeout=0, poll_interval=1, + poll_delay_function=None): + """Wait for the Allocation to become active. + + :param timeout: timeout in seconds, no timeout if 0. + :param poll_interval: interval in seconds between polls. + :param poll_delay_function: function to use to wait between polls + (defaults to time.sleep). Should take one argument - delay time + in seconds. Any exceptions raised inside it will abort the wait. + :return: updated :class:`Allocation` object. + :raises: StateTransitionFailed if allocation reaches the error state. + :raises: StateTransitionTimeout on timeout. + """ + timeout_msg = _('Allocation %(allocation)s failed to become active ' + 'in %(timeout)s seconds') % { + 'allocation': allocation_id, + 'timeout': timeout} + for _count in utils.poll(timeout, poll_interval, poll_delay_function, + timeout_msg): + allocation = self.get(allocation_id) + if allocation.state == 'error': + raise exc.StateTransitionFailed( + _('Allocation %(allocation)s failed: %(error)s') % + {'allocation': allocation_id, + 'error': allocation.last_error}) + elif allocation.state == 'active': + return allocation + + LOG.debug('Still waiting for allocation %(allocation)s to become ' + 'active, the current state is %(actual)s', + {'allocation': allocation_id, + 'actual': allocation.state}) diff --git a/ironicclient/v1/client.py b/ironicclient/v1/client.py index dc54804..76f6bf0 100644 --- a/ironicclient/v1/client.py +++ b/ironicclient/v1/client.py @@ -20,6 +20,7 @@ from ironicclient.common import http from ironicclient.common.http import DEFAULT_VER from ironicclient.common.i18n import _ from ironicclient import exc +from ironicclient.v1 import allocation from ironicclient.v1 import chassis from ironicclient.v1 import conductor from ironicclient.v1 import driver @@ -101,6 +102,7 @@ class Client(object): self.portgroup = portgroup.PortgroupManager(self.http_client) self.conductor = conductor.ConductorManager(self.http_client) self.events = events.EventManager(self.http_client) + self.allocation = allocation.AllocationManager(self.http_client) @property def current_api_version(self): diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 129d310..8e5747b 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -14,7 +14,6 @@ import logging import os -import time from oslo_utils import strutils @@ -682,19 +681,16 @@ class NodeManager(base.CreateManager): :raises: StateTransitionFailed if node reached an error state :raises: StateTransitionTimeout on timeout """ - if not isinstance(timeout, (int, float)) or timeout < 0: - raise ValueError(_('Timeout must be a non-negative number')) - - threshold = time.time() + timeout expected_state = expected_state.lower() - 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')) + timeout_msg = _('Node %(node)s failed to reach state %(state)s in ' + '%(timeout)s seconds') % {'node': node_ident, + 'state': expected_state, + 'timeout': timeout} # TODO(dtantsur): use version negotiation to request API 1.8 and use # the "fields" argument to reduce amount of data sent. - while not timeout or time.time() < threshold: + for _count in utils.poll(timeout, poll_interval, poll_delay_function, + timeout_msg): node = self.get(node_ident) if node.provision_state == expected_state: LOG.debug('Node %(node)s reached provision state %(state)s', @@ -721,10 +717,3 @@ class NodeManager(base.CreateManager): '%(state)s, the current state is %(actual)s', {'node': node_ident, 'state': expected_state, 'actual': node.provision_state}) - poll_delay_function(poll_interval) - - raise exc.StateTransitionTimeout( - _('Node %(node)s failed to reach state %(state)s in ' - '%(timeout)s seconds') % {'node': node_ident, - 'state': expected_state, - 'timeout': timeout}) diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index a0d6f3e..9ed2ae2 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -33,12 +33,14 @@ class Resource(object): FIELDS = { 'address': 'Address', 'alive': 'Alive', + 'allocation_uuid': 'Allocation UUID', 'async': 'Async', 'automated_clean': 'Automated Clean', 'attach': 'Response is attachment', 'bios_name': 'BIOS setting name', 'bios_value': 'BIOS setting value', 'boot_index': 'Boot Index', + 'candidate_nodes': 'Candidate Nodes', 'chassis_uuid': 'Chassis UUID', 'clean_step': 'Clean Step', 'conductor': 'Conductor', @@ -101,6 +103,7 @@ class Resource(object): 'raid_config': 'Current RAID configuration', 'reservation': 'Reservation', 'resource_class': 'Resource Class', + 'state': 'State', 'target_power_state': 'Target Power State', 'target_provision_state': 'Target Provision State', 'target_raid_config': 'Target RAID configuration', @@ -210,7 +213,8 @@ CHASSIS_RESOURCE = Resource( # corresponding headings, so some items (like raid_config) may seem out of # order here. NODE_DETAILED_RESOURCE = Resource( - ['automated_clean', + ['allocation_uuid', + 'automated_clean', 'bios_interface', 'boot_interface', 'chassis_uuid', @@ -261,6 +265,7 @@ NODE_DETAILED_RESOURCE = Resource( 'vendor_interface', ], sort_excluded=[ + 'allocation_uuid', # The server cannot sort on "chassis_uuid" because it isn't a column in # the "nodes" database table. "chassis_id" is stored, but it is # internal to ironic. See bug #1443003 for more details. @@ -478,3 +483,30 @@ CONDUCTOR_RESOURCE = Resource( ], sort_excluded=['alive'] ) + +# Allocations +ALLOCATION_DETAILED_RESOURCE = Resource( + ['uuid', + 'name', + 'state', + 'node_uuid', + 'last_error', + 'resource_class', + 'traits', + 'candidate_nodes', + 'extra', + 'created_at', + 'updated_at', + ], + sort_excluded=[ + 'candidate_nodes', + 'traits', + ]) +ALLOCATION_RESOURCE = Resource( + ['uuid', + 'name', + 'resource_class', + 'state', + 'node_uuid', + ], +) |