diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 262861923..8f3b29da0 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -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__) diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 1935d407d..096fcec52 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -527,18 +527,21 @@ class CreateBaremetalNode(command.ShowOne): '--shard', metavar='', help=_("Shard for the node.")) + parser.add_argument( + '--parent-node', + metavar='', + 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="", + 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='', help=_('Set the shard for the node'), ) + parser.add_argument( + "--parent-node", + metavar='', + 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='', + 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]) diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index 2e15420a3..dcff9fb30 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -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': []}] diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index 807f28dca..c0fc83a19 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -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) diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index ec086a97f..8356dfa88 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -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 = [ diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 6a5287b60..9bc2fe836 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -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-") 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, diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index 1c9aebe58..a12992db7 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -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', diff --git a/releasenotes/notes/add-parent-node-support-450b111533c82440.yaml b/releasenotes/notes/add-parent-node-support-450b111533c82440.yaml new file mode 100644 index 000000000..3ce92c1ab --- /dev/null +++ b/releasenotes/notes/add-parent-node-support-450b111533c82440.yaml @@ -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. diff --git a/setup.cfg b/setup.cfg index e86eec08b..c95280364 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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