diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index ca1e61216..a936ddace 100644 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -156,6 +156,10 @@ class CreateBaremetalNode(show.ShowOne): metavar='', help='Network interface used for switching node to ' 'cleaning/provisioning networks.') + parser.add_argument( + '--resource-class', + metavar='', + help='Resource class for mapping nodes to Nova flavors') return parser @@ -166,7 +170,7 @@ class CreateBaremetalNode(show.ShowOne): field_list = ['chassis_uuid', 'driver', 'driver_info', 'properties', 'extra', 'uuid', 'name', - 'network_interface'] + 'network_interface', 'resource_class'] fields = dict((k, v) for (k, v) in vars(parsed_args).items() if k in field_list and not (v is None)) fields = utils.args_array_to_dict(fields, 'driver_info') @@ -326,6 +330,11 @@ class ListBaremetalNode(lister.Lister): choices=self.PROVISION_STATES, help="Limit list to nodes in . One of %s." % ( ", ".join(self.PROVISION_STATES))) + parser.add_argument( + '--resource-class', + dest='resource_class', + metavar='', + help="Limit list to nodes with resource class ") display_group = parser.add_mutually_exclusive_group(required=False) display_group.add_argument( '--long', @@ -365,6 +374,8 @@ class ListBaremetalNode(lister.Lister): params['maintenance'] = parsed_args.maintenance if parsed_args.provision_state: params['provision_state'] = parsed_args.provision_state + if parsed_args.resource_class: + params['resource_class'] = parsed_args.resource_class if parsed_args.long: params['detail'] = parsed_args.long columns = res_fields.NODE_DETAILED_RESOURCE.fields @@ -565,6 +576,11 @@ class SetBaremetalNode(command.Command): metavar='', help='Set the network interface for the node', ) + parser.add_argument( + '--resource-class', + metavar='', + help='Set the resource class for the node', + ) parser.add_argument( "--property", metavar="", @@ -619,6 +635,11 @@ class SetBaremetalNode(command.Command): "network_interface=%s" % parsed_args.network_interface] properties.extend(utils.args_array_to_patch( 'add', network_interface)) + if parsed_args.resource_class: + resource_class = [ + "resource_class=%s" % parsed_args.resource_class] + properties.extend(utils.args_array_to_patch( + 'add', resource_class)) if parsed_args.property: properties.extend(utils.args_array_to_patch( 'add', ['properties/' + x for x in parsed_args.property])) @@ -744,6 +765,12 @@ class UnsetBaremetalNode(command.Command): action='store_true', help="Unset the name of the node", ) + parser.add_argument( + "--resource-class", + dest='resource_class', + action='store_true', + help="Unset the resource class of the node", + ) parser.add_argument( '--property', metavar='', @@ -787,6 +814,9 @@ class UnsetBaremetalNode(command.Command): if parsed_args.name: properties.extend(utils.args_array_to_patch('remove', ['name'])) + if parsed_args.resource_class: + properties.extend(utils.args_array_to_patch('remove', + ['resource_class'])) if parsed_args.property: properties.extend(utils.args_array_to_patch('remove', ['properties/' + x diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index 1be4b92df..00ef95e52 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -167,6 +167,11 @@ class TestBaremetalCreate(TestBaremetal): [('network_interface', 'neutron')], {'network_interface': 'neutron'}) + def test_baremetal_create_with_resource_class(self): + self.check_with_options(['--resource-class', 'foo'], + [('resource_class', 'foo')], + {'resource_class': 'foo'}) + class TestBaremetalDelete(TestBaremetal): def setUp(self): @@ -317,6 +322,7 @@ class TestBaremetalList(TestBaremetal): 'Maintenance Reason', 'Power State', 'Properties', 'Provisioning State', 'Provision Updated At', 'Current RAID configuration', 'Reservation', + 'Resource Class', 'Target Power State', 'Target Provision State', 'Target RAID configuration', 'Updated At', 'Inspection Finished At', @@ -349,6 +355,7 @@ class TestBaremetalList(TestBaremetal): '', '', '', + '', baremetal_fakes.baremetal_uuid, baremetal_fakes.baremetal_name, '', @@ -439,6 +446,30 @@ class TestBaremetalList(TestBaremetal): self.check_parser, self.cmd, arglist, verifylist) + def test_baremetal_list_resource_class(self): + arglist = [ + '--resource-class', 'foo', + ] + verifylist = [ + ('resource_class', 'foo'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': None, + 'limit': None, + 'resource_class': 'foo' + } + + self.baremetal_mock.node.list.assert_called_with( + **kwargs + ) + def test_baremetal_list_fields(self): arglist = [ '--fields', 'uuid', 'name', @@ -827,6 +858,25 @@ class TestBaremetalSet(TestBaremetal): [{'path': '/network_interface', 'value': 'xxxxx', 'op': 'add'}] ) + def test_baremetal_set_resource_class(self): + arglist = [ + 'node_uuid', + '--resource-class', 'foo', + ] + verifylist = [ + ('node', 'node_uuid'), + ('resource_class', 'foo') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.update.assert_called_once_with( + 'node_uuid', + [{'path': '/resource_class', 'value': 'foo', 'op': 'add'}] + ) + def test_baremetal_set_extra(self): arglist = [ 'node_uuid', @@ -1124,6 +1174,25 @@ class TestBaremetalUnset(TestBaremetal): [{'path': '/name', 'op': 'remove'}] ) + def test_baremetal_unset_resource_class(self): + arglist = [ + 'node_uuid', + '--resource-class', + ] + verifylist = [ + ('node', 'node_uuid'), + ('resource_class', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.update.assert_called_once_with( + 'node_uuid', + [{'path': '/resource_class', 'op': 'remove'}] + ) + def test_baremetal_unset_extra(self): arglist = [ 'node_uuid', diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index ad69b43b2..5acb3866e 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -38,6 +38,7 @@ NODE1 = {'id': 123, 'driver_info': {'user': 'foo', 'password': 'bar'}, 'properties': {'num_cpu': 4}, 'name': 'fake-node-1', + 'resource_class': 'foo', 'extra': {}} NODE2 = {'id': 456, 'uuid': '66666666-7777-8888-9999-111111111111', @@ -47,6 +48,7 @@ NODE2 = {'id': 456, 'driver': 'fake too', 'driver_info': {'user': 'foo', 'password': 'bar'}, 'properties': {'num_cpu': 4}, + 'resource_class': 'bar', 'extra': {}} PORT = {'id': 456, 'uuid': '11111111-2222-3333-4444-555555555555', @@ -171,6 +173,13 @@ fake_responses = { {"nodes": [NODE1]}, ) }, + '/v1/nodes/?resource_class=foo': + { + 'GET': ( + {}, + {"nodes": [NODE1]}, + ) + }, '/v1/nodes/detail?instance_uuid=%s' % NODE2['instance_uuid']: { 'GET': ( @@ -548,6 +557,15 @@ class NodeManagerTest(testtools.TestCase): self.assertThat(nodes, HasLength(1)) self.assertEqual(NODE1['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_resource_class(self): + nodes = self.mgr.list(resource_class="foo") + expect = [ + ('GET', '/v1/nodes/?resource_class=foo', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(nodes, HasLength(1)) + self.assertEqual(NODE1['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_no_maintenance(self): nodes = self.mgr.list(maintenance=False) expect = [ diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index 907a2e8f4..1e717547c 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -52,6 +52,7 @@ class NodeShellTest(utils.BaseTestCase): 'provision_state', 'provision_updated_at', 'reservation', + 'resource_class', 'target_power_state', 'target_provision_state', 'updated_at', @@ -749,7 +750,7 @@ class NodeShellTest(utils.BaseTestCase): maintenance=None, marker=None, limit=None, sort_dir=None, sort_key=None, detail=False, fields=None, provision_state=None, driver=None, - json=False): + json=False, resource_class=None): args = mock.MagicMock() args.node = node args.associated = associated @@ -763,6 +764,7 @@ class NodeShellTest(utils.BaseTestCase): args.fields = fields args.driver = driver args.json = json + args.resource_class = resource_class return args @@ -818,6 +820,24 @@ class NodeShellTest(utils.BaseTestCase): client_mock.node.list.assert_called_once_with(driver='fake', detail=True) + def test_do_node_list_resource_class(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(resource_class='foo', + detail=False) + + n_shell.do_node_list(client_mock, args) + client_mock.node.list.assert_called_once_with(resource_class='foo', + detail=False) + + def test_do_node_list_detail_resource_class(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(resource_class='foo', + detail=True) + + n_shell.do_node_list(client_mock, args) + client_mock.node.list.assert_called_once_with(resource_class='foo', + detail=True) + def test_do_node_list_sort_key(self): client_mock = mock.MagicMock() args = self._get_client_mock_args(sort_key='created_at', diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index bf91c0877..b161c36f1 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -38,12 +38,12 @@ class NodeManager(base.CreateManager): resource_class = Node _creation_attributes = ['chassis_uuid', 'driver', 'driver_info', 'extra', 'uuid', 'properties', 'name', - 'network_interface'] + 'network_interface', 'resource_class'] _resource_name = 'nodes' def list(self, associated=None, maintenance=None, marker=None, limit=None, detail=False, sort_key=None, sort_dir=None, fields=None, - provision_state=None, driver=None): + provision_state=None, driver=None, resource_class=None): """Retrieve a list of nodes. :param associated: Optional. Either a Boolean or a string @@ -84,6 +84,9 @@ class NodeManager(base.CreateManager): :param driver: Optional. String value to get only nodes using that driver. + :param resource_class: Optional. String value to get only nodes + with the given resource class set. + :returns: A list of nodes. """ @@ -104,6 +107,8 @@ class NodeManager(base.CreateManager): filters.append('provision_state=%s' % provision_state) if driver is not None: filters.append('driver=%s' % driver) + if resource_class is not None: + filters.append('resource_class=%s' % resource_class) path = '' if detail: diff --git a/ironicclient/v1/node_shell.py b/ironicclient/v1/node_shell.py index 69fbba810..c3222e28f 100644 --- a/ironicclient/v1/node_shell.py +++ b/ironicclient/v1/node_shell.py @@ -117,6 +117,11 @@ def do_node_show(cc, args): default=[], help="One or more node fields. Only these fields will be fetched from " "the server. Can not be used when '--detail' is specified.") +@cliutils.arg( + '--resource-class', + dest='resource_class', + metavar='', + help="List nodes using specified resource class.") def do_node_list(cc, args): """List the nodes which are registered with the Ironic service.""" params = {} @@ -132,6 +137,9 @@ def do_node_list(cc, args): if args.driver is not None: params['driver'] = args.driver + if args.resource_class is not None: + params['resource_class'] = args.resource_class + if args.detail: fields = res_fields.NODE_DETAILED_RESOURCE.fields field_labels = res_fields.NODE_DETAILED_RESOURCE.labels @@ -207,11 +215,16 @@ def do_node_list(cc, args): metavar='', help='Network interface used for switching node to cleaning/provisioning ' 'networks.') +@cliutils.arg( + '--resource-class', + metavar='', + help='Resource class for classifying or grouping nodes. Used, for ' + 'example, to classify nodes in Nova\'s placement engine.') def do_node_create(cc, args): """Register a new node with the Ironic service.""" field_list = ['chassis_uuid', 'driver', 'driver_info', 'properties', 'extra', 'uuid', 'name', - 'network_interface'] + 'network_interface', 'resource_class'] fields = dict((k, v) for (k, v) in vars(args).items() if k in field_list and not (v is None)) fields = utils.args_array_to_dict(fields, 'driver_info') diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index a137b075e..2cd88aebf 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -59,6 +59,7 @@ class Resource(object): 'provision_updated_at': 'Provision Updated At', 'raid_config': 'Current RAID configuration', 'reservation': 'Reservation', + 'resource_class': 'Resource Class', 'target_power_state': 'Target Power State', 'target_provision_state': 'Target Provision State', 'target_raid_config': 'Target RAID configuration', @@ -146,6 +147,7 @@ NODE_DETAILED_RESOURCE = Resource( 'provision_updated_at', 'raid_config', 'reservation', + 'resource_class', 'target_power_state', 'target_provision_state', 'target_raid_config', diff --git a/releasenotes/notes/add-node-resource-class-6040d1d6c734522c.yaml b/releasenotes/notes/add-node-resource-class-6040d1d6c734522c.yaml new file mode 100644 index 000000000..ead301f0a --- /dev/null +++ b/releasenotes/notes/add-node-resource-class-6040d1d6c734522c.yaml @@ -0,0 +1,4 @@ +--- +features: + - Adds support for the new ``node.resource_class`` field, + which was introduced in API version 1.21.