summaryrefslogtreecommitdiff
path: root/ironicclient
diff options
context:
space:
mode:
authorDmitry Tantsur <divius.inside@gmail.com>2019-02-12 16:24:05 +0100
committerDmitry Tantsur <divius.inside@gmail.com>2019-02-16 15:51:38 +0100
commite0708a16efa48d872a9f088af856809f523f03dc (patch)
treeb456e4c82c8402ccaa5a68e4dd268ed82ab07430 /ironicclient
parente8a6d447f803c115ed57064e6fada3e9d6f30794 (diff)
downloadpython-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.py21
-rw-r--r--ironicclient/osc/v1/baremetal_allocation.py269
-rw-r--r--ironicclient/tests/functional/osc/v1/base.py53
-rw-r--r--ironicclient/tests/functional/osc/v1/test_baremetal_allocation.py160
-rw-r--r--ironicclient/tests/unit/osc/v1/fakes.py10
-rw-r--r--ironicclient/tests/unit/osc/v1/test_baremetal_allocation.py471
-rw-r--r--ironicclient/tests/unit/osc/v1/test_baremetal_node.py1
-rw-r--r--ironicclient/tests/unit/v1/test_allocation.py330
-rw-r--r--ironicclient/tests/unit/v1/test_node_shell.py3
-rw-r--r--ironicclient/v1/allocation.py141
-rw-r--r--ironicclient/v1/client.py2
-rw-r--r--ironicclient/v1/node.py23
-rw-r--r--ironicclient/v1/resource_fields.py34
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',
+ ],
+)