Add support for node.resource_class

This adds support for the node.resource_class field, introduced in
API 1.21. It can be modified on an existing node or passed when
creating a node, or used as a filter when fetching a list of nodes.

Change-Id: Id494a8b735a3532db84d90ba21da173f7e33ed1d
Depends-On: I936f2e7b2f4d26e01354e826e5595ff021c3a55c
Partial-Bug: #1604916
This commit is contained in:
Jim Rollenhagen 2016-07-20 22:28:43 +00:00
parent b3c77f4d3b
commit 22888e9759
8 changed files with 166 additions and 5 deletions

View File

@ -156,6 +156,10 @@ class CreateBaremetalNode(show.ShowOne):
metavar='<network_interface>', metavar='<network_interface>',
help='Network interface used for switching node to ' help='Network interface used for switching node to '
'cleaning/provisioning networks.') 'cleaning/provisioning networks.')
parser.add_argument(
'--resource-class',
metavar='<resource_class>',
help='Resource class for mapping nodes to Nova flavors')
return parser return parser
@ -166,7 +170,7 @@ class CreateBaremetalNode(show.ShowOne):
field_list = ['chassis_uuid', 'driver', 'driver_info', field_list = ['chassis_uuid', 'driver', 'driver_info',
'properties', 'extra', 'uuid', 'name', 'properties', 'extra', 'uuid', 'name',
'network_interface'] 'network_interface', 'resource_class']
fields = dict((k, v) for (k, v) in vars(parsed_args).items() fields = dict((k, v) for (k, v) in vars(parsed_args).items()
if k in field_list and not (v is None)) if k in field_list and not (v is None))
fields = utils.args_array_to_dict(fields, 'driver_info') fields = utils.args_array_to_dict(fields, 'driver_info')
@ -326,6 +330,11 @@ class ListBaremetalNode(lister.Lister):
choices=self.PROVISION_STATES, choices=self.PROVISION_STATES,
help="Limit list to nodes in <provision state>. One of %s." % ( help="Limit list to nodes in <provision state>. One of %s." % (
", ".join(self.PROVISION_STATES))) ", ".join(self.PROVISION_STATES)))
parser.add_argument(
'--resource-class',
dest='resource_class',
metavar='<resource class>',
help="Limit list to nodes with resource class <resource class>")
display_group = parser.add_mutually_exclusive_group(required=False) display_group = parser.add_mutually_exclusive_group(required=False)
display_group.add_argument( display_group.add_argument(
'--long', '--long',
@ -365,6 +374,8 @@ class ListBaremetalNode(lister.Lister):
params['maintenance'] = parsed_args.maintenance params['maintenance'] = parsed_args.maintenance
if parsed_args.provision_state: if parsed_args.provision_state:
params['provision_state'] = 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: if parsed_args.long:
params['detail'] = parsed_args.long params['detail'] = parsed_args.long
columns = res_fields.NODE_DETAILED_RESOURCE.fields columns = res_fields.NODE_DETAILED_RESOURCE.fields
@ -565,6 +576,11 @@ class SetBaremetalNode(command.Command):
metavar='<network_interface>', metavar='<network_interface>',
help='Set the network interface for the node', help='Set the network interface for the node',
) )
parser.add_argument(
'--resource-class',
metavar='<resource_class>',
help='Set the resource class for the node',
)
parser.add_argument( parser.add_argument(
"--property", "--property",
metavar="<key=value>", metavar="<key=value>",
@ -619,6 +635,11 @@ class SetBaremetalNode(command.Command):
"network_interface=%s" % parsed_args.network_interface] "network_interface=%s" % parsed_args.network_interface]
properties.extend(utils.args_array_to_patch( properties.extend(utils.args_array_to_patch(
'add', network_interface)) '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: if parsed_args.property:
properties.extend(utils.args_array_to_patch( properties.extend(utils.args_array_to_patch(
'add', ['properties/' + x for x in parsed_args.property])) 'add', ['properties/' + x for x in parsed_args.property]))
@ -744,6 +765,12 @@ class UnsetBaremetalNode(command.Command):
action='store_true', action='store_true',
help="Unset the name of the node", 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( parser.add_argument(
'--property', '--property',
metavar='<key>', metavar='<key>',
@ -787,6 +814,9 @@ class UnsetBaremetalNode(command.Command):
if parsed_args.name: if parsed_args.name:
properties.extend(utils.args_array_to_patch('remove', properties.extend(utils.args_array_to_patch('remove',
['name'])) ['name']))
if parsed_args.resource_class:
properties.extend(utils.args_array_to_patch('remove',
['resource_class']))
if parsed_args.property: if parsed_args.property:
properties.extend(utils.args_array_to_patch('remove', properties.extend(utils.args_array_to_patch('remove',
['properties/' + x ['properties/' + x

View File

@ -167,6 +167,11 @@ class TestBaremetalCreate(TestBaremetal):
[('network_interface', 'neutron')], [('network_interface', 'neutron')],
{'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): class TestBaremetalDelete(TestBaremetal):
def setUp(self): def setUp(self):
@ -317,6 +322,7 @@ class TestBaremetalList(TestBaremetal):
'Maintenance Reason', 'Power State', 'Properties', 'Maintenance Reason', 'Power State', 'Properties',
'Provisioning State', 'Provision Updated At', 'Provisioning State', 'Provision Updated At',
'Current RAID configuration', 'Reservation', 'Current RAID configuration', 'Reservation',
'Resource Class',
'Target Power State', 'Target Provision State', 'Target Power State', 'Target Provision State',
'Target RAID configuration', 'Target RAID configuration',
'Updated At', 'Inspection Finished At', 'Updated At', 'Inspection Finished At',
@ -349,6 +355,7 @@ class TestBaremetalList(TestBaremetal):
'', '',
'', '',
'', '',
'',
baremetal_fakes.baremetal_uuid, baremetal_fakes.baremetal_uuid,
baremetal_fakes.baremetal_name, baremetal_fakes.baremetal_name,
'', '',
@ -439,6 +446,30 @@ class TestBaremetalList(TestBaremetal):
self.check_parser, self.check_parser,
self.cmd, arglist, verifylist) 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): def test_baremetal_list_fields(self):
arglist = [ arglist = [
'--fields', 'uuid', 'name', '--fields', 'uuid', 'name',
@ -827,6 +858,25 @@ class TestBaremetalSet(TestBaremetal):
[{'path': '/network_interface', 'value': 'xxxxx', 'op': 'add'}] [{'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): def test_baremetal_set_extra(self):
arglist = [ arglist = [
'node_uuid', 'node_uuid',
@ -1124,6 +1174,25 @@ class TestBaremetalUnset(TestBaremetal):
[{'path': '/name', 'op': 'remove'}] [{'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): def test_baremetal_unset_extra(self):
arglist = [ arglist = [
'node_uuid', 'node_uuid',

View File

@ -38,6 +38,7 @@ NODE1 = {'id': 123,
'driver_info': {'user': 'foo', 'password': 'bar'}, 'driver_info': {'user': 'foo', 'password': 'bar'},
'properties': {'num_cpu': 4}, 'properties': {'num_cpu': 4},
'name': 'fake-node-1', 'name': 'fake-node-1',
'resource_class': 'foo',
'extra': {}} 'extra': {}}
NODE2 = {'id': 456, NODE2 = {'id': 456,
'uuid': '66666666-7777-8888-9999-111111111111', 'uuid': '66666666-7777-8888-9999-111111111111',
@ -47,6 +48,7 @@ NODE2 = {'id': 456,
'driver': 'fake too', 'driver': 'fake too',
'driver_info': {'user': 'foo', 'password': 'bar'}, 'driver_info': {'user': 'foo', 'password': 'bar'},
'properties': {'num_cpu': 4}, 'properties': {'num_cpu': 4},
'resource_class': 'bar',
'extra': {}} 'extra': {}}
PORT = {'id': 456, PORT = {'id': 456,
'uuid': '11111111-2222-3333-4444-555555555555', 'uuid': '11111111-2222-3333-4444-555555555555',
@ -171,6 +173,13 @@ fake_responses = {
{"nodes": [NODE1]}, {"nodes": [NODE1]},
) )
}, },
'/v1/nodes/?resource_class=foo':
{
'GET': (
{},
{"nodes": [NODE1]},
)
},
'/v1/nodes/detail?instance_uuid=%s' % NODE2['instance_uuid']: '/v1/nodes/detail?instance_uuid=%s' % NODE2['instance_uuid']:
{ {
'GET': ( 'GET': (
@ -548,6 +557,15 @@ class NodeManagerTest(testtools.TestCase):
self.assertThat(nodes, HasLength(1)) self.assertThat(nodes, HasLength(1))
self.assertEqual(NODE1['uuid'], getattr(nodes[0], 'uuid')) 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): def test_node_list_no_maintenance(self):
nodes = self.mgr.list(maintenance=False) nodes = self.mgr.list(maintenance=False)
expect = [ expect = [

View File

@ -52,6 +52,7 @@ class NodeShellTest(utils.BaseTestCase):
'provision_state', 'provision_state',
'provision_updated_at', 'provision_updated_at',
'reservation', 'reservation',
'resource_class',
'target_power_state', 'target_power_state',
'target_provision_state', 'target_provision_state',
'updated_at', 'updated_at',
@ -749,7 +750,7 @@ class NodeShellTest(utils.BaseTestCase):
maintenance=None, marker=None, limit=None, maintenance=None, marker=None, limit=None,
sort_dir=None, sort_key=None, detail=False, sort_dir=None, sort_key=None, detail=False,
fields=None, provision_state=None, driver=None, fields=None, provision_state=None, driver=None,
json=False): json=False, resource_class=None):
args = mock.MagicMock() args = mock.MagicMock()
args.node = node args.node = node
args.associated = associated args.associated = associated
@ -763,6 +764,7 @@ class NodeShellTest(utils.BaseTestCase):
args.fields = fields args.fields = fields
args.driver = driver args.driver = driver
args.json = json args.json = json
args.resource_class = resource_class
return args return args
@ -818,6 +820,24 @@ class NodeShellTest(utils.BaseTestCase):
client_mock.node.list.assert_called_once_with(driver='fake', client_mock.node.list.assert_called_once_with(driver='fake',
detail=True) 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): def test_do_node_list_sort_key(self):
client_mock = mock.MagicMock() client_mock = mock.MagicMock()
args = self._get_client_mock_args(sort_key='created_at', args = self._get_client_mock_args(sort_key='created_at',

View File

@ -38,12 +38,12 @@ class NodeManager(base.CreateManager):
resource_class = Node resource_class = Node
_creation_attributes = ['chassis_uuid', 'driver', 'driver_info', _creation_attributes = ['chassis_uuid', 'driver', 'driver_info',
'extra', 'uuid', 'properties', 'name', 'extra', 'uuid', 'properties', 'name',
'network_interface'] 'network_interface', 'resource_class']
_resource_name = 'nodes' _resource_name = 'nodes'
def list(self, associated=None, maintenance=None, marker=None, limit=None, def list(self, associated=None, maintenance=None, marker=None, limit=None,
detail=False, sort_key=None, sort_dir=None, fields=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. """Retrieve a list of nodes.
:param associated: Optional. Either a Boolean or a string :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 :param driver: Optional. String value to get only nodes using that
driver. driver.
:param resource_class: Optional. String value to get only nodes
with the given resource class set.
:returns: A list of nodes. :returns: A list of nodes.
""" """
@ -104,6 +107,8 @@ class NodeManager(base.CreateManager):
filters.append('provision_state=%s' % provision_state) filters.append('provision_state=%s' % provision_state)
if driver is not None: if driver is not None:
filters.append('driver=%s' % driver) filters.append('driver=%s' % driver)
if resource_class is not None:
filters.append('resource_class=%s' % resource_class)
path = '' path = ''
if detail: if detail:

View File

@ -117,6 +117,11 @@ def do_node_show(cc, args):
default=[], default=[],
help="One or more node fields. Only these fields will be fetched from " help="One or more node fields. Only these fields will be fetched from "
"the server. Can not be used when '--detail' is specified.") "the server. Can not be used when '--detail' is specified.")
@cliutils.arg(
'--resource-class',
dest='resource_class',
metavar='<resource class>',
help="List nodes using specified resource class.")
def do_node_list(cc, args): def do_node_list(cc, args):
"""List the nodes which are registered with the Ironic service.""" """List the nodes which are registered with the Ironic service."""
params = {} params = {}
@ -132,6 +137,9 @@ def do_node_list(cc, args):
if args.driver is not None: if args.driver is not None:
params['driver'] = args.driver params['driver'] = args.driver
if args.resource_class is not None:
params['resource_class'] = args.resource_class
if args.detail: if args.detail:
fields = res_fields.NODE_DETAILED_RESOURCE.fields fields = res_fields.NODE_DETAILED_RESOURCE.fields
field_labels = res_fields.NODE_DETAILED_RESOURCE.labels field_labels = res_fields.NODE_DETAILED_RESOURCE.labels
@ -207,11 +215,16 @@ def do_node_list(cc, args):
metavar='<network_interface>', metavar='<network_interface>',
help='Network interface used for switching node to cleaning/provisioning ' help='Network interface used for switching node to cleaning/provisioning '
'networks.') 'networks.')
@cliutils.arg(
'--resource-class',
metavar='<resource_class>',
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): def do_node_create(cc, args):
"""Register a new node with the Ironic service.""" """Register a new node with the Ironic service."""
field_list = ['chassis_uuid', 'driver', 'driver_info', field_list = ['chassis_uuid', 'driver', 'driver_info',
'properties', 'extra', 'uuid', 'name', 'properties', 'extra', 'uuid', 'name',
'network_interface'] 'network_interface', 'resource_class']
fields = dict((k, v) for (k, v) in vars(args).items() fields = dict((k, v) for (k, v) in vars(args).items()
if k in field_list and not (v is None)) if k in field_list and not (v is None))
fields = utils.args_array_to_dict(fields, 'driver_info') fields = utils.args_array_to_dict(fields, 'driver_info')

View File

@ -59,6 +59,7 @@ class Resource(object):
'provision_updated_at': 'Provision Updated At', 'provision_updated_at': 'Provision Updated At',
'raid_config': 'Current RAID configuration', 'raid_config': 'Current RAID configuration',
'reservation': 'Reservation', 'reservation': 'Reservation',
'resource_class': 'Resource Class',
'target_power_state': 'Target Power State', 'target_power_state': 'Target Power State',
'target_provision_state': 'Target Provision State', 'target_provision_state': 'Target Provision State',
'target_raid_config': 'Target RAID configuration', 'target_raid_config': 'Target RAID configuration',
@ -146,6 +147,7 @@ NODE_DETAILED_RESOURCE = Resource(
'provision_updated_at', 'provision_updated_at',
'raid_config', 'raid_config',
'reservation', 'reservation',
'resource_class',
'target_power_state', 'target_power_state',
'target_provision_state', 'target_provision_state',
'target_raid_config', 'target_raid_config',

View File

@ -0,0 +1,4 @@
---
features:
- Adds support for the new ``node.resource_class`` field,
which was introduced in API version 1.21.