Add support for parent node

- Updates the API client version to 1.83
- Adds support to get/set/update a parent_node value on a node.
- Adds support to ask the API for a list of nodes which related.
- Adds support to filter query list by parent_node as well.

Change-Id: Iea24e96f0360c6e5ac61cb57ab0c6f5d47c57f2b
This commit is contained in:
Julia Kreger 2023-07-07 08:04:58 -07:00
parent 0e9a476610
commit b57429ffdb
9 changed files with 250 additions and 12 deletions

View File

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

View File

@ -527,18 +527,21 @@ class CreateBaremetalNode(command.ShowOne):
'--shard',
metavar='<shard>',
help=_("Shard for the node."))
parser.add_argument(
'--parent-node',
metavar='<parent_node>',
help=_('Parent node for the node being created.'))
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 = ['automated_clean', 'chassis_uuid', 'driver',
'driver_info', 'properties', 'extra', 'uuid', 'name',
'conductor_group', 'owner', 'description', 'lessee',
'shard', 'resource_class'
'shard', 'resource_class', 'parent_node',
] + ['%s_interface' % iface
for iface in SUPPORTED_INTERFACES]
fields = dict((k, v) for (k, v) in vars(parsed_args).items()
@ -788,6 +791,18 @@ class ListBaremetalNode(command.Lister):
help=_("One or more node fields. Only these fields will be "
"fetched from the server. Can not be used when '--long' "
"is specified."))
children_group = parser.add_mutually_exclusive_group(required=False)
children_group.add_argument(
'--include-children',
action='store_true',
help=_("Include children in the node list."),
)
children_group.add_argument(
'--parent-node',
dest='parent_node',
metavar="<parent_node>",
help=_('List only nodes associated with a parent node.'),
)
return parser
def take_action(self, parsed_args):
@ -815,9 +830,11 @@ class ListBaremetalNode(command.Lister):
params[field] = getattr(parsed_args, field)
for field in ['provision_state', 'driver', 'resource_class',
'chassis', 'conductor', 'owner', 'lessee',
'description_contains', 'shards']:
'description_contains', 'shards', 'parent_node']:
if getattr(parsed_args, field):
params[field] = getattr(parsed_args, field)
if parsed_args.include_children:
params['include_children'] = True
if parsed_args.long:
params['detail'] = parsed_args.long
columns = res_fields.NODE_DETAILED_RESOURCE.fields
@ -1429,6 +1446,11 @@ class SetBaremetalNode(command.Command):
metavar='<shard>',
help=_('Set the shard for the node'),
)
parser.add_argument(
"--parent-node",
metavar='<parent_node>',
help=_('Set the parent node for the node'),
)
return parser
@ -1459,7 +1481,7 @@ class SetBaremetalNode(command.Command):
'chassis_uuid', 'driver', 'resource_class',
'conductor_group', 'protected', 'protected_reason',
'retired', 'retired_reason', 'owner', 'lessee',
'description', 'shard']:
'description', 'shard', 'parent_node']:
value = getattr(parsed_args, field)
if value:
properties.extend(utils.args_array_to_patch(
@ -1780,6 +1802,11 @@ class UnsetBaremetalNode(command.Command):
action="store_true",
help=_('Unset the shard field of the node'),
)
parser.add_argument(
"--parent-node",
action="store_true",
help=_('Unset the parent node field of the node'),
)
return parser
@ -1805,7 +1832,7 @@ class UnsetBaremetalNode(command.Command):
'storage_interface', 'vendor_interface',
'protected', 'protected_reason', 'retired',
'retired_reason', 'owner', 'lessee', 'description',
'shard', ]:
'shard', 'parent_node']:
if getattr(parsed_args, field):
properties.extend(utils.args_array_to_patch('remove', [field]))
@ -2300,3 +2327,31 @@ class NodeInventorySave(command.Command):
json.dump(inventory, fp)
else:
json.dump(inventory, sys.stdout)
class NodeChildrenList(command.ShowOne):
"""Get a list of nodes assocated as children."""
log = logging.getLogger(__name__ + ".NodeChildrenList")
def get_parser(self, prog_name):
parser = super(NodeChildrenList, self).get_parser(prog_name)
parser.add_argument(
'node',
metavar='<node>',
help=_("Name or UUID of the node.")
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
baremetal_client = self.app.client_manager.baremetal
labels = res_fields.CHILDREN_RESOURCE.labels
data = baremetal_client.node.list_children_of_node(
parsed_args.node)
return (labels, [[node] for node in data])

View File

@ -149,6 +149,8 @@ PORTGROUP = {'uuid': baremetal_portgroup_uuid,
VIFS = {'vifs': [{'id': 'aaa-aa'}]}
TRAITS = ['CUSTOM_FOO', 'CUSTOM_BAR']
CHILDREN = ['53da080f-6de7-4a3e-bcb6-b7889b380ad0',
'48467e9b-3cd1-45b5-a57e-169e01370169']
BIOS_SETTINGS = [{'name': 'bios_name_1', 'value': 'bios_value_1', 'links': []},
{'name': 'bios_name_2', 'value': 'bios_value_2', 'links': []}]

View File

@ -539,7 +539,8 @@ class TestBaremetalCreate(TestBaremetal):
def check_with_options(self, addl_arglist, addl_verifylist, addl_kwargs):
arglist = copy.copy(self.arglist) + addl_arglist
verifylist = copy.copy(self.verifylist) + addl_verifylist
print(verifylist)
print(arglist)
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
@ -736,6 +737,11 @@ class TestBaremetalCreate(TestBaremetal):
[('shard', 'myshard')],
{'shard': 'myshard'})
def test_baremetal_create_with_parent_node(self):
self.check_with_options(['--parent-node', 'nodex'],
[('parent_node', 'nodex')],
{'parent_node': 'nodex'})
class TestBaremetalDelete(TestBaremetal):
def setUp(self):
@ -916,6 +922,7 @@ class TestBaremetalList(TestBaremetal):
'Network Configuration',
'Network Interface',
'Owner',
'Parent Node',
'Power Interface',
'Power State',
'Properties',
@ -1498,6 +1505,52 @@ class TestBaremetalList(TestBaremetal):
self.assertRaises(oscutils.ParserException, self.check_parser,
self.cmd, arglist, verifylist)
def test_baremetal_list_by_parent_node(self):
parent_node = 'node1'
arglist = [
'--parent-node', parent_node,
]
verifylist = [
('parent_node', parent_node),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
print(parsed_args)
# DisplayCommandBase.take_action() returns two tuples
self.cmd.take_action(parsed_args)
kwargs = {
'marker': None,
'limit': None,
'parent_node': parent_node,
}
self.baremetal_mock.node.list.assert_called_with(
**kwargs
)
def test_baremetal_list_include_children(self):
arglist = [
'--include-children',
]
verifylist = [
('include_children', True),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
self.cmd.take_action(parsed_args)
kwargs = {
'marker': None,
'limit': None,
'include_children': True,
}
self.baremetal_mock.node.list.assert_called_with(
**kwargs
)
class TestBaremetalMaintenanceSet(TestBaremetal):
def setUp(self):
@ -4448,3 +4501,27 @@ class TestNodeInventorySave(TestBaremetal):
'boot': {'current_boot_mode': 'uefi'}}
inventory = json.loads(buf.getvalue())
self.assertEqual(expected_data, inventory['inventory'])
class TestNodeChildrenList(TestBaremetal):
def setUp(self):
super(TestNodeChildrenList, self).setUp()
self.baremetal_mock.node.list_children_of_node.return_value = (
baremetal_fakes.CHILDREN)
# Get the command object to test
self.cmd = baremetal_node.NodeChildrenList(self.app, None)
def test_child_node_list(self):
arglist = ['node_uuid']
verifylist = [('node', 'node_uuid')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.baremetal_mock.node.list_children_of_node \
.assert_called_once_with('node_uuid')
self.assertEqual(('Child Nodes',), columns)
self.assertEqual([[node] for node in baremetal_fakes.CHILDREN], data)

View File

@ -39,7 +39,8 @@ NODE1 = {'uuid': '66666666-7777-8888-9999-000000000000',
'network_data': {},
'resource_class': 'foo',
'extra': {},
'conductor_group': 'in-the-closet-to-the-left'}
'conductor_group': 'in-the-closet-to-the-left',
'parent_node': None}
NODE2 = {'uuid': '66666666-7777-8888-9999-111111111111',
'instance_uuid': '66666666-7777-8888-9999-222222222222',
'chassis_uuid': 'aaaaaaaa-1111-bbbb-2222-cccccccccccc',
@ -53,7 +54,8 @@ NODE2 = {'uuid': '66666666-7777-8888-9999-111111111111',
'owner': '33333333-2222-1111-0000-111111111111',
'retired': True,
'lessee': '77777777-8888-5555-2222-999999999999',
'shard': 'myshard'}
'shard': 'myshard',
'parent_node': NODE1['uuid']}
PORT = {'uuid': '11111111-2222-3333-4444-555555555555',
'node_uuid': '66666666-7777-8888-9999-000000000000',
'address': 'AA:AA:AA:AA:AA:AA',
@ -282,6 +284,27 @@ fake_responses = {
{"nodes": [NODE2]}
)
},
'/v1/nodes/?include_children=True':
{
'GET': (
{},
{"nodes": [NODE1, NODE2]}
)
},
'/v1/nodes/?parent_node=%s' % NODE1['uuid']:
{
'GET': (
{},
{"nodes": [NODE2]}
)
},
'/v1/nodes/%s/children' % NODE1['uuid']:
{
'GET': (
{},
{"children": [NODE2['uuid']]}
)
},
'/v1/nodes/detail?instance_uuid=%s' % NODE2['instance_uuid']:
{
'GET': (
@ -1016,6 +1039,35 @@ class NodeManagerTest(testtools.TestCase):
self.assertThat(nodes, HasLength(1))
self.assertEqual(NODE2['uuid'], getattr(nodes[0], 'uuid'))
def test_node_list_include_chidlren(self):
nodes = self.mgr.list(include_children=True)
expect = [
('GET', '/v1/nodes/?include_children=True', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(nodes, HasLength(2))
self.assertEqual(NODE1['uuid'], getattr(nodes[0], 'uuid'))
self.assertEqual(NODE2['uuid'], getattr(nodes[1], 'uuid'))
def test_node_list_nodes_by_parent_node(self):
nodes = self.mgr.list(parent_node=NODE1['uuid'])
expect = [
('GET', '/v1/nodes/?parent_node=%s' % NODE1['uuid'],
{}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(nodes, HasLength(1))
self.assertEqual(NODE2['uuid'], getattr(nodes[0], 'uuid'))
def test_node_list_children_of_node(self):
children = self.mgr.list_children_of_node(NODE1['uuid'])
expect = [
('GET', '/v1/nodes/%s/children' % NODE1['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(children))
self.assertEqual(NODE2['uuid'], children[0])
def test_node_list_detail(self):
nodes = self.mgr.list(detail=True)
expect = [

View File

@ -53,7 +53,8 @@ class NodeManager(base.CreateManager):
'raid_interface', 'rescue_interface',
'storage_interface', 'vendor_interface',
'resource_class', 'conductor_group',
'automated_clean', 'network_data']
'automated_clean', 'network_data',
'parent_node']
_resource_name = 'nodes'
def list(self, associated=None, maintenance=None, marker=None,
@ -62,7 +63,8 @@ class NodeManager(base.CreateManager):
resource_class=None, chassis=None, fault=None,
os_ironic_api_version=None, conductor_group=None,
conductor=None, owner=None, retired=None, lessee=None,
shards=None, sharded=None, global_request_id=None):
shards=None, sharded=None, parent_node=None,
include_children=None, global_request_id=None):
"""Retrieve a list of nodes.
:param associated: Optional. Either a Boolean or a string
@ -134,7 +136,13 @@ class NodeManager(base.CreateManager):
:param sharded: Optional. Boolean value, when true get only nodes
with a non-null node.shard value, when false get only
nodes with a null node.shard value. None is a noop.
with a non-null node.shard value.
:param parent_node: Optional. String value used to retreive child
nodes with the supplied parent node.
:param include_children: Optional. Boolean Value, only True is valid.
Tells the ironic API to enumerate all child
nodes which are normally hidden from the
node list.
:returns: A list of nodes.
"""
@ -175,6 +183,11 @@ class NodeManager(base.CreateManager):
filters.append('sharded=%s' % sharded)
if shards is not None:
filters.append('shard=%s' % ','.join(shards))
if parent_node is not None:
filters.append('parent_node=%s' % parent_node)
if include_children:
# NOTE(TheJulia): Only valid if True.
filters.append('include_children=True')
path = ''
if detail:
@ -382,6 +395,29 @@ class NodeManager(base.CreateManager):
self._path(path), response_key="targets", limit=limit,
obj_class=volume_target.VolumeTarget, **header_values)
def list_children_of_node(
self, node_id,
os_ironic_api_version=None,
global_request_id=None):
"""Get a list of child nodes for the supplied node_id.
:param node_id: The name or UUID of a node.
:param os_ironic_api_version: String version (e.g. "1.35") to use for
the request. If not specified, the client's default is used.
:param global_request_id: String containing global request ID header
value (in form "req-<UUID>") to use for the request.
:returns: A list of UUIDs representing child nodes for the supplied
node_id..
"""
path = "%s/children" % node_id
header_values = {"os_ironic_api_version": os_ironic_api_version,
"global_request_id": global_request_id}
return self._list_primitives(self._path(path), "children",
**header_values)
def get(self, node_id, fields=None, os_ironic_api_version=None,
global_request_id=None):
return self._get(resource_id=node_id, fields=fields,

View File

@ -152,6 +152,8 @@ class Resource(object):
'id': 'ID',
'connector_id': 'Connector ID',
'is_smartnic': 'Is Smart NIC port',
'parent_node': 'Parent Node',
'children': 'Child Nodes',
}
def __init__(self, field_ids, sort_excluded=None, override_labels=None):
@ -268,6 +270,7 @@ NODE_DETAILED_RESOURCE = Resource(
'network_data',
'network_interface',
'owner',
'parent_node',
'power_interface',
'power_state',
'properties',
@ -392,6 +395,10 @@ TRAIT_RESOURCE = Resource(
['traits'],
)
CHILDREN_RESOURCE = Resource(
['children'],
)
BIOS_RESOURCE = Resource(
['name', 'value'],
override_labels={'name': 'BIOS setting name',

View File

@ -0,0 +1,8 @@
---
features:
- |
Increments the maximum API version to ``1.83`` which allows the Node field
``parent_node`` to become visible.
- |
Adds client support to allow users to get, set, and update the
``parent_node`` field on a bare metal node.

View File

@ -64,6 +64,7 @@ openstack.baremetal.v1 =
baremetal_node_boot_device_show = ironicclient.osc.v1.baremetal_node:BootdeviceShowBaremetalNode
baremetal_node_boot_mode_set = ironicclient.osc.v1.baremetal_node:BootmodeSetBaremetalNode
baremetal_node_clean = ironicclient.osc.v1.baremetal_node:CleanBaremetalNode
baremetal_node_children_list = ironicclient.osc.v1.baremetal_node:NodeChildrenList
baremetal_node_console_disable = ironicclient.osc.v1.baremetal_node:ConsoleDisableBaremetalNode
baremetal_node_console_enable = ironicclient.osc.v1.baremetal_node:ConsoleEnableBaremetalNode
baremetal_node_console_show = ironicclient.osc.v1.baremetal_node:ConsoleShowBaremetalNode