From 4603ef678fc7e8eb438170a1cb54a7ffe7bbfb70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Jens=C3=A5s?= Date: Sun, 25 Mar 2018 16:56:00 +0200 Subject: [PATCH] Enrich integration with ironic networking features The mac key in nodes_json is deprecated, replaced with "ports" key. New ports key is list of dicts holding a richer data set matching the properties of ports in the Bare Metal service api. In addition to mac address the physical_network and local_link_connection can be defined for Bare Metal ports when registering nodes. * address: (mandatory) The physical address (mac address) of the port. * physical_network: (otional) Defaults to: ctlplane * local_link_connection: (optional) This data enables the possibility for automatic configuration of switches via neutron plugins. e.g ML2 vendor plugins. Defaults to: None Implements: enrich-ironic-networking-integration Change-Id: I74d4178dbb0cfe8c934ce15e3e7c9bb1c469de10 --- ...son-ironic-port-data-0905da3f7b13d149.yaml | 38 ++++++++ tripleo_common/actions/baremetal.py | 4 +- tripleo_common/actions/parameters.py | 6 +- tripleo_common/tests/utils/test_nodes.py | 86 +++++++++++++++---- tripleo_common/utils/nodes.py | 48 ++++++++--- 5 files changed, 145 insertions(+), 37 deletions(-) create mode 100644 releasenotes/notes/enrich-nodes-json-ironic-port-data-0905da3f7b13d149.yaml diff --git a/releasenotes/notes/enrich-nodes-json-ironic-port-data-0905da3f7b13d149.yaml b/releasenotes/notes/enrich-nodes-json-ironic-port-data-0905da3f7b13d149.yaml new file mode 100644 index 000000000..175b173c6 --- /dev/null +++ b/releasenotes/notes/enrich-nodes-json-ironic-port-data-0905da3f7b13d149.yaml @@ -0,0 +1,38 @@ +--- +features: + - | + Adds support to specify additional parameters for Bare Metal ports when + registering nodes. + + The ``mac`` key in nodes_json (instackenv.json) is replaced by the new + ``ports`` key. Each port-entry supports the following keys: ``address``, + ``physical_network`` and ``local_link_connection``. (The keys in ``ports`` + mirror a subset off the `Bare Metal service API `_ + .) + + Example specifying port mac address only:: + + "ports": [ + { + "address": "52:54:00:87:c8:2e" + } + ] + + Example specifying additional parameters:: + + "ports": [ + { + "address": "52:54:00:87:c8:2f", + "physical_network": "network", + "local_link_connection": { + "switch_info": "switch", + "port_id": "gi1/0/11", + "switch_id": "a6:18:66:33:cb:49" + } + } + ] +deprecations: + - | + The ``mac`` key in nodes_json is replaced by ``ports``. The ``ports`` key + expect a list of dictionaries specifying ``address`` (mac address), and + optional keys ``physical_network`` and ``local_link_connection``. diff --git a/tripleo_common/actions/baremetal.py b/tripleo_common/actions/baremetal.py index dc73355ec..4ed67b9b1 100644 --- a/tripleo_common/actions/baremetal.py +++ b/tripleo_common/actions/baremetal.py @@ -46,7 +46,7 @@ class RegisterOrUpdateNodes(base.TripleOAction): def __init__(self, nodes_json, remove=False, kernel_name=None, ramdisk_name=None, instance_boot_option='local'): super(RegisterOrUpdateNodes, self).__init__() - self.nodes_json = nodes_json + self.nodes_json = nodes.convert_nodes_json_mac_to_ports(nodes_json) self.remove = remove self.instance_boot_option = instance_boot_option self.kernel_name = kernel_name @@ -83,7 +83,7 @@ class ValidateNodes(base.TripleOAction): def __init__(self, nodes_json): super(ValidateNodes, self).__init__() - self.nodes_json = nodes_json + self.nodes_json = nodes.convert_nodes_json_mac_to_ports(nodes_json) def run(self, context): try: diff --git a/tripleo_common/actions/parameters.py b/tripleo_common/actions/parameters.py index 1f3ca3a3e..cc27fbb6f 100644 --- a/tripleo_common/actions/parameters.py +++ b/tripleo_common/actions/parameters.py @@ -345,7 +345,7 @@ class GenerateFencingParametersAction(base.TripleOAction): def __init__(self, nodes_json, os_auth, delay, ipmi_level, ipmi_cipher, ipmi_lanplus): super(GenerateFencingParametersAction, self).__init__() - self.nodes_json = nodes_json + self.nodes_json = nodes.convert_nodes_json_mac_to_ports(nodes_json) self.os_auth = os_auth self.delay = delay self.ipmi_level = ipmi_level @@ -362,10 +362,10 @@ class GenerateFencingParametersAction(base.TripleOAction): for node in self.nodes_json: node_data = {} params = {} - if "mac" in node: + if "ports" in node: # Not all Ironic drivers present a MAC address, so we only # capture it if it's present - mac_addr = node["mac"][0] + mac_addr = node['ports'][0]['address'] node_data["host_mac"] = mac_addr # If the MAC isn't in the hostmap, this node hasn't been diff --git a/tripleo_common/tests/utils/test_nodes.py b/tripleo_common/tests/utils/test_nodes.py index 53b2ee872..d2d6bc495 100644 --- a/tripleo_common/tests/utils/test_nodes.py +++ b/tripleo_common/tests/utils/test_nodes.py @@ -258,9 +258,9 @@ class NodesTest(base.TestCase): def _get_node(self): return {'cpu': '1', 'memory': '2048', 'disk': '30', 'arch': 'amd64', - 'mac': ['aaa'], 'pm_addr': 'foo.bar', 'pm_user': 'test', - 'pm_password': 'random', 'pm_type': 'ipmi', 'name': 'node1', - 'capabilities': 'num_nics:6'} + 'ports': [{'address': 'aaa'}], 'pm_addr': 'foo.bar', + 'pm_user': 'test', 'pm_password': 'random', 'pm_type': 'ipmi', + 'name': 'node1', 'capabilities': 'num_nics:6'} def test_register_all_nodes_ironic_no_hw_stats(self): node_list = [self._get_node()] @@ -287,7 +287,8 @@ class NodesTest(base.TestCase): resource_class='baremetal', properties=node_properties) port_call = mock.call(node_uuid=ironic.node.create.return_value.uuid, - address='aaa', physical_network='ctlplane') + address='aaa', physical_network='ctlplane', + local_link_connection=None) ironic.node.create.assert_has_calls([pxe_node, mock.ANY]) ironic.port.create.assert_has_calls([port_call]) @@ -311,7 +312,8 @@ class NodesTest(base.TestCase): resource_class='baremetal', properties=node_properties) port_call = mock.call(node_uuid=ironic.node.create.return_value.uuid, - address='aaa', physical_network='ctlplane') + address='aaa', physical_network='ctlplane', + local_link_connection=None) ironic.node.create.assert_has_calls([pxe_node, mock.ANY]) ironic.port.create.assert_has_calls([port_call]) @@ -341,7 +343,8 @@ class NodesTest(base.TestCase): resource_class='baremetal', properties=node_properties) port_call = mock.call(node_uuid=ironic.node.create.return_value.uuid, - address='aaa', physical_network='ctlplane') + address='aaa', physical_network='ctlplane', + local_link_connection=None) ironic.node.create.assert_has_calls([pxe_node, mock.ANY]) ironic.port.create.assert_has_calls([port_call]) @@ -365,7 +368,8 @@ class NodesTest(base.TestCase): resource_class='baremetal', uuid="abcdef") port_call = mock.call(node_uuid=ironic.node.create.return_value.uuid, - address='aaa', physical_network='ctlplane') + address='aaa', physical_network='ctlplane', + local_link_connection=None) ironic.node.create.assert_has_calls([pxe_node, mock.ANY]) ironic.port.create.assert_has_calls([port_call]) @@ -390,7 +394,8 @@ class NodesTest(base.TestCase): resource_class='baremetal', properties=node_properties) port_call = mock.call(node_uuid=ironic.node.create.return_value.uuid, - address='aaa', physical_network='ctlplane') + address='aaa', physical_network='ctlplane', + local_link_connection=None) ironic.node.create.assert_has_calls([pxe_node, mock.ANY]) ironic.port.create.assert_has_calls([port_call]) @@ -425,7 +430,8 @@ class NodesTest(base.TestCase): resource_class='baremetal', **interfaces) port_call = mock.call(node_uuid=ironic.node.create.return_value.uuid, - address='aaa', physical_network='ctlplane') + address='aaa', local_link_connection=None, + physical_network='ctlplane') ironic.node.create.assert_has_calls([pxe_node, mock.ANY]) ironic.port.create.assert_has_calls([port_call]) @@ -589,7 +595,7 @@ class NodesTest(base.TestCase): def test_register_node_update(self): node = self._get_node() - node['mac'][0] = node['mac'][0].upper() + node['ports'][0]['address'] = node['ports'][0]['address'].upper() ironic = mock.MagicMock() node_map = {'mac': {'aaa': 1}} @@ -781,6 +787,38 @@ class NodesTest(base.TestCase): 'redfish_username': 'test', 'redfish_system_id': '/redfish/v1/Systems/1'}) + def test_register_ironic_node_with_physical_network(self): + node = self._get_node() + node['ports'] = [{'physical_network': 'subnet1', 'address': 'aaa'}] + ironic = mock.MagicMock() + nodes.register_ironic_node(node, client=ironic) + port_call = mock.call(node_uuid=ironic.node.create.return_value.uuid, + address='aaa', physical_network='subnet1', + local_link_connection=None) + ironic.port.create.assert_has_calls([port_call]) + + def test_register_ironic_node_with_local_link_connection(self): + node = self._get_node() + node['ports'] = [ + { + 'local_link_connection': { + "switch_info": "switch", + "port_id": "port1", + "switch_id": "bbb" + }, + 'physical_network': 'subnet1', + 'address': 'aaa' + } + ] + ironic = mock.MagicMock() + nodes.register_ironic_node(node, client=ironic) + port_call = mock.call(node_uuid=ironic.node.create.return_value.uuid, + address='aaa', physical_network='subnet1', + local_link_connection={"switch_info": "switch", + "port_id": "port1", + "switch_id": "bbb"}) + ironic.port.create.assert_has_calls([port_call]) + def test_clean_up_extra_nodes_ironic(self): node = collections.namedtuple('node', ['uuid']) client = mock.MagicMock() @@ -859,8 +897,10 @@ VALID_NODE_JSON = [ 'pm_password': 'p@$$w0rd', 'pm_port': 1234, 'ipmi_priv_level': 'USER', - 'mac': ['aa:bb:cc:dd:ee:ff', - '11:22:33:44:55:66'], + 'ports': [ + {'address': 'aa:bb:cc:dd:ee:ff'}, + {'address': '11:22:33:44:55:66'} + ], 'name': 'foobar1', 'capabilities': {'foo': 'bar'}, 'kernel_id': 'kernel1', @@ -871,8 +911,10 @@ VALID_NODE_JSON = [ 'pm_password': 'p@$$w0rd', 'pm_port': 1234, 'ipmi_priv_level': 'USER', - 'mac': ['dd:ee:ff:aa:bb:cc', - '44:55:66:11:22:33'], + 'ports': [ + {'address': 'dd:ee:ff:aa:bb:cc'}, + {'address': '44:55:66:11:22:33'} + ], 'name': 'foobar2', 'capabilities': {'foo': 'bar'}, 'kernel_id': 'kernel1', @@ -881,7 +923,9 @@ VALID_NODE_JSON = [ 'pm_addr': '1.2.3.4', 'pm_user': 'root', 'pm_password': 'p@$$w0rd', - 'mac': ['22:22:22:22:22:22'], + 'ports': [ + {'address': '22:22:22:22:22:22'} + ], 'capabilities': 'foo:bar,foo1:bar1', 'cpu': 2, 'memory': 1024, @@ -932,7 +976,9 @@ class TestValidateNodes(base.TestCase): 'pm_addr': '1.1.1.1', 'pm_user': 'root', 'pm_password': 'p@$$w0rd', - 'mac': ['42']}, + 'ports': [ + {'address': '42'}] + }, ] self.assertRaisesRegex(exception.InvalidNode, 'MAC address 42 is invalid', @@ -944,12 +990,16 @@ class TestValidateNodes(base.TestCase): 'pm_addr': '1.1.1.1', 'pm_user': 'root', 'pm_password': 'p@$$w0rd', - 'mac': ['11:22:33:44:55:66']}, + 'ports': [ + {'address': '11:22:33:44:55:66'} + ]}, {'pm_type': 'ipmi', 'pm_addr': '1.2.1.1', 'pm_user': 'user', 'pm_password': 'p@$$w0rd', - 'mac': ['11:22:33:44:55:66']}, + 'ports': [ + {'address': '11:22:33:44:55:66'} + ]}, ] self.assertRaisesRegex(exception.InvalidNode, 'MAC 11:22:33:44:55:66 is not unique', diff --git a/tripleo_common/utils/nodes.py b/tripleo_common/utils/nodes.py index 76d4d056c..4f08eb2d7 100644 --- a/tripleo_common/utils/nodes.py +++ b/tripleo_common/utils/nodes.py @@ -34,6 +34,20 @@ _KNOWN_INTERFACE_FIELDS = [ CTLPLANE_NETWORK = 'ctlplane' +def convert_nodes_json_mac_to_ports(nodes_json): + for node in nodes_json: + if node.get('mac'): + LOG.warning('Key mac is deprecated, please use ports.') + for address in node['mac']: + try: + node['ports'].append({'address': address}) + except KeyError: + node['ports'] = [{'address': address}] + del node['mac'] + + return nodes_json + + class DriverInfo(object): """Class encapsulating field conversion logic.""" DEFAULTS = {} @@ -232,9 +246,9 @@ class SshDriverInfo(DriverInfo): def validate(self, node): super(SshDriverInfo, self).validate(node) - if not node.get('mac'): + if not node.get('ports')[0]['address']: raise exception.InvalidNode( - 'Nodes with SSH drivers require at least one MAC') + 'Nodes with SSH drivers require at least one PORT') class iBootDriverInfo(PrefixedDriverInfo): @@ -356,9 +370,14 @@ def register_ironic_node(node, client): LOG.debug('Registering node %s with ironic.', node_id) ironic_node = client.node.create(**create_map) - for mac in node.get("mac", []): - client.port.create(address=mac, physical_network=CTLPLANE_NETWORK, - node_uuid=ironic_node.uuid) + for port in node.get('ports', []): + LOG.debug('Creating Bare Metal port for node: %s, with properties: %s.' + % (ironic_node.uuid, port)) + client.port.create( + address=port.get('address'), + physical_network=port.get('physical_network', 'ctlplane'), + local_link_connection=port.get('local_link_connection'), + node_uuid=ironic_node.uuid) validation = client.node.validate(ironic_node.uuid) if not validation.power['result']: @@ -388,9 +407,9 @@ def _populate_node_mapping(client): def _get_node_id(node, handler, node_map): candidates = set() - for mac in node.get('mac', []): + for port in node.get('ports', []): try: - candidates.add(node_map['mac'][mac.lower()]) + candidates.add(node_map['mac'][port['address'].lower()]) except KeyError: pass @@ -527,15 +546,16 @@ def validate_nodes(nodes_list): except exception.InvalidNode as exc: failures.append((index, exc)) - for mac in node.get('mac', ()): - if not netutils.is_valid_mac(mac): - failures.append((index, 'MAC address %s is invalid' % mac)) + for port in node.get('ports', ()): + if not netutils.is_valid_mac(port['address']): + failures.append((index, 'MAC address %s is invalid' % + port['address'])) - if mac in macs: + if port['address'] in macs: failures.append( - (index, 'MAC %s is not unique' % mac)) + (index, 'MAC %s is not unique' % port['address'])) else: - macs.add(mac) + macs.add(port['address']) unique_id = handler.unique_id_from_fields(node) if unique_id: @@ -569,7 +589,7 @@ def validate_nodes(nodes_list): for field in node: converted = handler.convert_key(field) if (converted is None and field not in _NON_DRIVER_FIELDS and - field not in ('mac', 'pm_type')): + field not in ('ports', 'pm_type')): failures.append((index, 'Unknown field %s' % field)) if failures: