From c84c6af087f3173be8854fd94f1173a22efc451e Mon Sep 17 00:00:00 2001 From: Ilya Etingof Date: Wed, 16 Oct 2019 12:09:36 +0200 Subject: [PATCH] Add `get_node_network_data` to Neutron NetworkInterface Implements `get_node_network_data` network interface method for Neutron networks providing Nova network metadata (AKA network_data.json) collected from Neutron VIF of ironic port objects associated with the node. Co-Authored: Iury Gregory Melo Ferreira Change-Id: I0fa742110649ed2786f360e1ef43012e77671620 Story: 2006691 Task: 37071 --- ironic/common/neutron.py | 155 ++++++++++++++++++ ironic/drivers/modules/network/common.py | 50 +++++- .../json_samples/neutron_network_show.json | 33 ++++ .../neutron_network_show_ipv6.json | 33 ++++ .../json_samples/neutron_port_show.json | 59 +++++++ .../json_samples/neutron_port_show_ipv6.json | 59 +++++++ .../json_samples/neutron_subnet_show.json | 32 ++++ .../neutron_subnet_show_ipv6.json | 32 ++++ ironic/tests/unit/common/test_neutron.py | 123 ++++++++++++++ .../unit/drivers/modules/network/test_flat.py | 5 +- .../drivers/modules/network/test_neutron.py | 9 +- .../node-network-data-6f998aaa57020f4b.yaml | 7 + 12 files changed, 593 insertions(+), 4 deletions(-) create mode 100644 ironic/tests/unit/common/json_samples/neutron_network_show.json create mode 100644 ironic/tests/unit/common/json_samples/neutron_network_show_ipv6.json create mode 100644 ironic/tests/unit/common/json_samples/neutron_port_show.json create mode 100644 ironic/tests/unit/common/json_samples/neutron_port_show_ipv6.json create mode 100644 ironic/tests/unit/common/json_samples/neutron_subnet_show.json create mode 100644 ironic/tests/unit/common/json_samples/neutron_subnet_show_ipv6.json create mode 100644 releasenotes/notes/node-network-data-6f998aaa57020f4b.yaml diff --git a/ironic/common/neutron.py b/ironic/common/neutron.py index 7182cd3578..a37ddf84fd 100644 --- a/ironic/common/neutron.py +++ b/ironic/common/neutron.py @@ -11,6 +11,7 @@ # under the License. import copy +import ipaddress from keystoneauth1 import loading as ks_loading from neutronclient.common import exceptions as neutron_exceptions @@ -474,6 +475,160 @@ def remove_neutron_ports(task, params): {'node_uuid': node_uuid}) +def _uncidr(cidr, ipv6=False): + """Convert CIDR network representation into network/netmask form + + :param cidr: network in CIDR form + :param ipv6: if `True`, consider `cidr` being IPv6 + :returns: a tuple of network/host number in dotted + decimal notation, netmask in dotted decimal notation + + """ + net = ipaddress.ip_interface(cidr).network + return str(net.network_address), str(net.netmask) + + +def get_neutron_port_data(port_id, vif_id, client=None, context=None): + """Gather Neutron port and network configuration + + Query Neutron for port and network configuration, return whatever + is available. + + :param port_id: ironic port/portgroup ID. + :param vif_id: Neutron port ID. + :param client: Optional a Neutron client object. + :param context: request context + :type context: ironic.common.context.RequestContext + :raises: NetworkError + :returns: a dict holding network configuration information + associated with this ironic or Neutron port. + """ + + if not client: + client = get_client(context=context) + + try: + port_config = client.show_port( + vif_id, fields=['id', 'name', 'dns_assignment', 'fixed_ips', + 'mac_address', 'network_id']) + + except neutron_exceptions.NeutronClientException as e: + msg = (_('Unable to get port info for %(port_id)s. Error: ' + '%(err)s') % {'port_id': vif_id, 'err': e}) + LOG.exception(msg) + raise exception.NetworkError(msg) + + LOG.debug('Received port %(port)s data: %(info)s', + {'port': vif_id, 'info': port_config}) + + port_config = port_config['port'] + + port_id = port_config['name'] or port_id + + network_id = port_config.get('network_id') + + try: + network_config = client.show_network( + network_id, fields=['id', 'mtu', 'subnets']) + + except neutron_exceptions.NeutronClientException as e: + msg = (_('Unable to get network info for %(network_id)s. Error: ' + '%(err)s') % {'network_id': network_id, 'err': e}) + LOG.exception(msg) + raise exception.NetworkError(msg) + + LOG.debug('Received network %(network)s data: %(info)s', + {'network': network_id, 'info': network_config}) + + network_config = network_config['network'] + + subnets_config = {} + + network_data = { + 'links': [ + { + 'id': port_id, + 'type': 'vif', + 'ethernet_mac_address': port_config['mac_address'], + 'vif_id': port_config['id'], + 'mtu': network_config['mtu'] + } + ], + 'networks': [ + + ] + } + + for fixed_ip in port_config.get('fixed_ips', []): + subnet_id = fixed_ip['subnet_id'] + + try: + subnet_config = client.show_subnet( + subnet_id, fields=['id', 'name', 'enable_dhcp', + 'dns_nameservers', 'host_routes', + 'ip_version', 'gateway_ip', 'cidr']) + + LOG.debug('Received subnet %(subnet)s data: %(info)s', + {'subnet': subnet_id, 'info': subnet_config}) + + subnets_config[subnet_id] = subnet_config['subnet'] + + except neutron_exceptions.NeutronClientException as e: + msg = (_('Unable to get subnet info for %(subnet_id)s. Error: ' + '%(err)s') % {'subnet_id': subnet_id, 'err': e}) + LOG.exception(msg) + raise exception.NetworkError(msg) + + subnet_config = subnets_config[subnet_id] + + subnet_network, netmask = _uncidr( + subnet_config['cidr'], subnet_config['ip_version'] == 6) + + network = { + 'id': fixed_ip['subnet_id'], + 'network_id': port_config['network_id'], + 'type': 'ipv%s' % subnet_config['ip_version'], + 'link': port_id, + 'ip_address': fixed_ip['ip_address'], + 'netmask': netmask, + 'routes': [ + + ] + } + + # TODO(etingof): Adding default route if gateway is present. + # This is a hack, Neutron should have given us a route. + + if subnet_config['gateway_ip']: + zero_addr = ('::0' if subnet_config['ip_version'] == 6 + else '0.0.0.0') + + route = { + 'network': zero_addr, + 'netmask': zero_addr, + 'gateway': subnet_config['gateway_ip'] + } + + network['routes'].append(route) + + for host_config in subnet_config['host_routes']: + subnet_network, netmask = _uncidr( + host_config['destination'], + subnet_config['ip_version'] == 6) + + route = { + 'network': subnet_network, + 'netmask': netmask, + 'gateway': host_config['nexthop'] + } + + network['routes'].append(route) + + network_data['networks'].append(network) + + return network_data + + def get_node_portmap(task): """Extract the switch port information for the node. diff --git a/ironic/drivers/modules/network/common.py b/ironic/drivers/modules/network/common.py index ec98cac65d..2c3c4be0c0 100644 --- a/ironic/drivers/modules/network/common.py +++ b/ironic/drivers/modules/network/common.py @@ -410,7 +410,7 @@ class VIFPortIDMixin(object): or self._get_vif_id_by_port_like_obj(p_obj) or None) def get_node_network_data(self, task): - """Return network configuration for node NICs. + """Get network configuration data for node's ports/portgroups. Gather L2 and L3 network settings from ironic node `network_data` field. Ironic would eventually pass network configuration to the node @@ -633,3 +633,51 @@ class NeutronVIFPortIDMixin(VIFPortIDMixin): # DELETING state. if task.node.provision_state in [states.ACTIVE, states.DELETING]: neutron.unbind_neutron_port(vif_id, context=task.context) + + def get_node_network_data(self, task): + """Get network configuration data for node ports. + + Pull network data from ironic node object if present, otherwise + collect it for Neutron VIFs. + + :param task: A TaskManager instance. + :raises: InvalidParameterValue, if the network interface configuration + is invalid. + :raises: MissingParameterValue, if some parameters are missing. + :returns: a dict holding network configuration information adhearing + Nova network metadata layout (`network_data.json`). + """ + # NOTE(etingof): static network data takes precedence + network_data = ( + super(NeutronVIFPortIDMixin, self).get_node_network_data(task)) + if network_data: + return network_data + + node = task.node + + LOG.debug('Gathering network data from ports of node ' + '%(node)s', {'node': node.uuid}) + + network_data = collections.defaultdict(list) + + for port_obj in task.ports: + vif_port_id = self.get_current_vif(task, port_obj) + + LOG.debug('Considering node %(node)s port %(port)s, VIF %(vif)s', + {'node': node.uuid, 'port': port_obj.uuid, + 'vif': vif_port_id}) + + if not vif_port_id: + continue + + port_network_data = neutron.get_neutron_port_data( + port_obj.uuid, vif_port_id, context=task.context) + + for field, field_data in port_network_data.items(): + if field_data: + network_data[field].extend(field_data) + + LOG.debug('Collected network data for node %(node)s: %(data)s', + {'node': node.uuid, 'data': network_data}) + + return network_data diff --git a/ironic/tests/unit/common/json_samples/neutron_network_show.json b/ironic/tests/unit/common/json_samples/neutron_network_show.json new file mode 100644 index 0000000000..7c54850ca6 --- /dev/null +++ b/ironic/tests/unit/common/json_samples/neutron_network_show.json @@ -0,0 +1,33 @@ +{ + "network": { + "admin_state_up": true, + "availability_zone_hints": [], + "availability_zones": [ + "nova" + ], + "created_at": "2016-03-08T20:19:41", + "dns_domain": "my-domain.org.", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "ipv4_address_scope": null, + "ipv6_address_scope": null, + "l2_adjacency": false, + "mtu": 1500, + "name": "private-network", + "port_security_enabled": true, + "project_id": "4fd44f30292945e481c7b8a0c8908869", + "qos_policy_id": "6a8454ade84346f59e8d40665f878b2e", + "revision_number": 1, + "router:external": false, + "shared": true, + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "tags": ["tag1,tag2"], + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "updated_at": "2016-03-08T20:19:41", + "vlan_transparent": false, + "description": "", + "is_default": true + } +} \ No newline at end of file diff --git a/ironic/tests/unit/common/json_samples/neutron_network_show_ipv6.json b/ironic/tests/unit/common/json_samples/neutron_network_show_ipv6.json new file mode 100644 index 0000000000..eb955e3b5a --- /dev/null +++ b/ironic/tests/unit/common/json_samples/neutron_network_show_ipv6.json @@ -0,0 +1,33 @@ +{ + "network": { + "admin_state_up": true, + "availability_zone_hints": [], + "availability_zones": [ + "nova" + ], + "created_at": "2016-03-08T20:19:41", + "dns_domain": "my-domain.org.", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "ipv4_address_scope": null, + "ipv6_address_scope": null, + "l2_adjacency": false, + "mtu": 1500, + "name": "private-network", + "port_security_enabled": true, + "project_id": "5199666e520f4aed823710aec37cfd38", + "qos_policy_id": "6a8454ade84346f59e8d40665f878b2e", + "revision_number": 1, + "router:external": false, + "shared": true, + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "tags": ["tag1,tag2"], + "tenant_id": "5199666e520f4aed823710aec37cfd38", + "updated_at": "2016-03-08T20:19:41", + "vlan_transparent": false, + "description": "", + "is_default": true + } +} diff --git a/ironic/tests/unit/common/json_samples/neutron_port_show.json b/ironic/tests/unit/common/json_samples/neutron_port_show.json new file mode 100644 index 0000000000..925f00fd08 --- /dev/null +++ b/ironic/tests/unit/common/json_samples/neutron_port_show.json @@ -0,0 +1,59 @@ +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [], + "binding:host_id": "devstack", + "binding:profile": {}, + "binding:vif_details": { + "ovs_hybrid_plug": true, + "port_filter": true + }, + "binding:vif_type": "ovs", + "binding:vnic_type": "normal", + "created_at": "2016-03-08T20:19:41", + "data_plane_status": "ACTIVE", + "description": "", + "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e", + "device_owner": "network:router_interface", + "dns_assignment": { + "hostname": "myport", + "ip_address": "10.0.0.2", + "fqdn": "myport.my-domain.org" + }, + "dns_domain": "my-domain.org.", + "dns_name": "myport", + "extra_dhcp_opts": [ + { + "opt_value": "pxelinux.0", + "ip_version": 4, + "opt_name": "bootfile-name" + } + ], + "fixed_ips": [ + { + "ip_address": "10.0.0.2", + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2" + } + ], + "id": "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", + "ip_allocation": "immediate", + "mac_address": "fa:16:3e:23:fd:d7", + "mac_learning_enabled": false, + "name": "", + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "port_security_enabled": false, + "project_id": "7e02058126cc4950b75f9970368ba177", + "revision_number": 1, + "security_groups": [], + "status": "ACTIVE", + "tags": ["tag1,tag2"], + "tenant_id": "7e02058126cc4950b75f9970368ba177", + "updated_at": "2016-03-08T20:19:41", + "qos_policy_id": "29d5e02e-d5ab-4929-bee4-4a9fc12e22ae", + "resource_request": { + "required": ["CUSTOM_PHYSNET_PUBLIC", "CUSTOM_VNIC_TYPE_NORMAL"], + "resources": {"NET_BW_EGR_KILOBIT_PER_SEC": 1000} + }, + "uplink_status_propagation": false + } +} \ No newline at end of file diff --git a/ironic/tests/unit/common/json_samples/neutron_port_show_ipv6.json b/ironic/tests/unit/common/json_samples/neutron_port_show_ipv6.json new file mode 100644 index 0000000000..1dd3ead68b --- /dev/null +++ b/ironic/tests/unit/common/json_samples/neutron_port_show_ipv6.json @@ -0,0 +1,59 @@ +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [], + "binding:host_id": "devstack", + "binding:profile": {}, + "binding:vif_details": { + "ovs_hybrid_plug": true, + "port_filter": true + }, + "binding:vif_type": "ovs", + "binding:vnic_type": "normal", + "created_at": "2016-03-08T20:19:41", + "data_plane_status": "ACTIVE", + "description": "", + "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e", + "device_owner": "network:router_interface", + "dns_assignment": { + "hostname": "myport", + "ip_address": "fd00:203:0:113::2", + "fqdn": "myport.my-domain.org" + }, + "dns_domain": "my-domain.org.", + "dns_name": "myport", + "extra_dhcp_opts": [ + { + "opt_value": "pxelinux.0", + "ip_version": 6, + "opt_name": "bootfile-name" + } + ], + "fixed_ips": [ + { + "ip_address": "fd00:203:0:113::2", + "subnet_id": "906e685a-b964-4d58-9939-9cf3af197c67" + } + ], + "id": "96d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb8", + "ip_allocation": "immediate", + "mac_address": "52:54:00:4f:ef:b7", + "mac_learning_enabled": false, + "name": "", + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "port_security_enabled": false, + "project_id": "7e02058126cc4950b75f9970368ba177", + "revision_number": 1, + "security_groups": [], + "status": "ACTIVE", + "tags": ["tag1,tag2"], + "tenant_id": "7e02058126cc4950b75f9970368ba177", + "updated_at": "2016-03-08T20:19:41", + "qos_policy_id": "29d5e02e-d5ab-4929-bee4-4a9fc12e22ae", + "resource_request": { + "required": ["CUSTOM_PHYSNET_PUBLIC", "CUSTOM_VNIC_TYPE_NORMAL"], + "resources": {"NET_BW_EGR_KILOBIT_PER_SEC": 1000} + }, + "uplink_status_propagation": false + } +} diff --git a/ironic/tests/unit/common/json_samples/neutron_subnet_show.json b/ironic/tests/unit/common/json_samples/neutron_subnet_show.json new file mode 100644 index 0000000000..f1b7ae5a55 --- /dev/null +++ b/ironic/tests/unit/common/json_samples/neutron_subnet_show.json @@ -0,0 +1,32 @@ +{ + "subnet": { + "name": "private-subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "segment_id": null, + "project_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "dns_publish_fixed_ip": false, + "allocation_pools": [ + { + "start": "10.0.0.2", + "end": "10.0.0.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "10.0.0.1", + "cidr": "10.0.0.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b", + "created_at": "2016-10-10T14:35:34Z", + "description": "", + "ipv6_address_mode": null, + "ipv6_ra_mode": null, + "revision_number": 2, + "service_types": [], + "subnetpool_id": null, + "tags": ["tag1,tag2"], + "updated_at": "2016-10-10T14:35:34Z" + } +} diff --git a/ironic/tests/unit/common/json_samples/neutron_subnet_show_ipv6.json b/ironic/tests/unit/common/json_samples/neutron_subnet_show_ipv6.json new file mode 100644 index 0000000000..e5bd1e496e --- /dev/null +++ b/ironic/tests/unit/common/json_samples/neutron_subnet_show_ipv6.json @@ -0,0 +1,32 @@ +{ + "subnet": { + "name": "private-subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "segment_id": null, + "project_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "dns_publish_fixed_ip": false, + "allocation_pools": [ + { + "start": "fd00:203:0:113::2", + "end": "fd00:203:0:113:ffff:ffff:ffff:ffff" + } + ], + "host_routes": [], + "ip_version": 6, + "gateway_ip": "fd00:203:0:113::1", + "cidr": "fd00:203:0:113::/64", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b", + "created_at": "2016-10-10T14:35:34Z", + "description": "", + "ipv6_address_mode": "slaac", + "ipv6_ra_mode": null, + "revision_number": 2, + "service_types": [], + "subnetpool_id": null, + "tags": ["tag1,tag2"], + "updated_at": "2016-10-10T14:35:34Z" + } +} diff --git a/ironic/tests/unit/common/test_neutron.py b/ironic/tests/unit/common/test_neutron.py index 02c9893312..d290aaa61e 100644 --- a/ironic/tests/unit/common/test_neutron.py +++ b/ironic/tests/unit/common/test_neutron.py @@ -11,6 +11,8 @@ # under the License. import copy +import json +import os import time from unittest import mock @@ -270,6 +272,30 @@ class TestNeutronNetworkActions(db_base.DbTestCase): patcher.start() self.addCleanup(patcher.stop) + port_show_file = os.path.join( + os.path.dirname(__file__), 'json_samples', + 'neutron_port_show.json') + with open(port_show_file, 'rb') as fl: + self.port_data = json.load(fl) + + self.client_mock.show_port.return_value = self.port_data + + network_show_file = os.path.join( + os.path.dirname(__file__), 'json_samples', + 'neutron_network_show.json') + with open(network_show_file, 'rb') as fl: + self.network_data = json.load(fl) + + self.client_mock.show_network.return_value = self.network_data + + subnet_show_file = os.path.join( + os.path.dirname(__file__), 'json_samples', + 'neutron_subnet_show.json') + with open(subnet_show_file, 'rb') as fl: + self.subnet_data = json.load(fl) + + self.client_mock.show_subnet.return_value = self.subnet_data + @mock.patch.object(neutron, 'update_neutron_port', autospec=True) def _test_add_ports_to_network(self, update_mock, is_client_id, security_groups=None, @@ -667,6 +693,103 @@ class TestNeutronNetworkActions(db_base.DbTestCase): self.client_mock.delete_port.assert_called_once_with( self.neutron_port['id']) + def test__uncidr_ipv4(self): + network, netmask = neutron._uncidr('10.0.0.0/24') + self.assertEqual('10.0.0.0', network) + self.assertEqual('255.255.255.0', netmask) + + def test__uncidr_ipv6(self): + network, netmask = neutron._uncidr('::1/64', ipv6=True) + self.assertEqual('::', network) + self.assertEqual('ffff:ffff:ffff:ffff::', netmask) + + def test_get_neutron_port_data(self): + + network_data = neutron.get_neutron_port_data('port0', 'vif0') + + expected_port = { + 'id': 'port0', + 'type': 'vif', + 'ethernet_mac_address': 'fa:16:3e:23:fd:d7', + 'vif_id': '46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2', + 'mtu': 1500 + } + + self.assertEqual(expected_port, network_data['links'][0]) + + expected_network = { + 'id': 'a0304c3a-4f08-4c43-88af-d796509c97d2', + 'network_id': 'a87cc70a-3e15-4acf-8205-9b711a3531b7', + 'type': 'ipv4', + 'link': 'port0', + 'ip_address': '10.0.0.2', + 'netmask': '255.255.255.0', + 'routes': [ + {'gateway': '10.0.0.1', + 'netmask': '0.0.0.0', + 'network': '0.0.0.0'} + ] + } + + self.assertEqual(expected_network, network_data['networks'][0]) + + def load_ipv6_files(self): + port_show_file = os.path.join( + os.path.dirname(__file__), 'json_samples', + 'neutron_port_show_ipv6.json') + with open(port_show_file, 'rb') as fl: + self.port_data = json.load(fl) + + self.client_mock.show_port.return_value = self.port_data + + network_show_file = os.path.join( + os.path.dirname(__file__), 'json_samples', + 'neutron_network_show_ipv6.json') + with open(network_show_file, 'rb') as fl: + self.network_data = json.load(fl) + + self.client_mock.show_network.return_value = self.network_data + + subnet_show_file = os.path.join( + os.path.dirname(__file__), 'json_samples', + 'neutron_subnet_show_ipv6.json') + with open(subnet_show_file, 'rb') as fl: + self.subnet_data = json.load(fl) + + self.client_mock.show_subnet.return_value = self.subnet_data + + def test_get_neutron_port_data_ipv6(self): + self.load_ipv6_files() + + network_data = neutron.get_neutron_port_data('port1', 'vif1') + + print(network_data) + expected_port = { + 'id': 'port1', + 'type': 'vif', + 'ethernet_mac_address': '52:54:00:4f:ef:b7', + 'vif_id': '96d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb8', + 'mtu': 1500 + } + + self.assertEqual(expected_port, network_data['links'][0]) + + expected_network = { + 'id': '906e685a-b964-4d58-9939-9cf3af197c67', + 'network_id': 'a87cc70a-3e15-4acf-8205-9b711a3531b7', + 'type': 'ipv6', + 'link': 'port1', + 'ip_address': 'fd00:203:0:113::2', + 'netmask': 'ffff:ffff:ffff:ffff::', + 'routes': [ + {'gateway': 'fd00:203:0:113::1', + 'netmask': '::0', + 'network': '::0'} + ] + } + + self.assertEqual(expected_network, network_data['networks'][0]) + def test_get_node_portmap(self): with task_manager.acquire(self.context, self.node.uuid) as task: portmap = neutron.get_node_portmap(task) diff --git a/ironic/tests/unit/drivers/modules/network/test_flat.py b/ironic/tests/unit/drivers/modules/network/test_flat.py index d8ea1a9a0f..66c75441cf 100644 --- a/ironic/tests/unit/drivers/modules/network/test_flat.py +++ b/ironic/tests/unit/drivers/modules/network/test_flat.py @@ -339,7 +339,10 @@ class TestFlatInterface(db_base.DbTestCase): self.assertRaises(exception.UnsupportedDriverExtension, self.interface.validate_inspection, task) - def test_get_node_network_data(self): + @mock.patch.object(neutron, 'get_neutron_port_data', autospec=True) + def test_get_node_network_data(self, mock_gnpd): + mock_gnpd.return_value = {} + with task_manager.acquire(self.context, self.node.id) as task: network_data = self.interface.get_node_network_data(task) diff --git a/ironic/tests/unit/drivers/modules/network/test_neutron.py b/ironic/tests/unit/drivers/modules/network/test_neutron.py index b3a64cb0b6..4d8c5e7bee 100644 --- a/ironic/tests/unit/drivers/modules/network/test_neutron.py +++ b/ironic/tests/unit/drivers/modules/network/test_neutron.py @@ -699,13 +699,15 @@ class NeutronInterfaceTestCase(db_base.DbTestCase): self.node.save() self._test_configure_tenant_networks(is_client_id=True) + @mock.patch.object(neutron_common, 'get_neutron_port_data', autospec=True) @mock.patch.object(neutron_common, 'wait_for_host_agent', autospec=True) @mock.patch.object(neutron_common, 'update_neutron_port', autospec=True) @mock.patch.object(neutron_common, 'get_client', autospec=True) @mock.patch.object(neutron_common, 'get_local_group_information', autospec=True) def test_configure_tenant_networks_with_portgroups( - self, glgi_mock, client_mock, update_mock, wait_agent_mock): + self, glgi_mock, client_mock, update_mock, wait_agent_mock, + port_data_mock): pg = utils.create_test_portgroup( self.context, node_id=self.node.id, address='ff:54:00:cf:2d:32', extra={'vif_port_id': uuidutils.generate_uuid()}) @@ -860,7 +862,10 @@ class NeutronInterfaceTestCase(db_base.DbTestCase): self.assertRaises(exception.UnsupportedDriverExtension, self.interface.validate_inspection, task) - def test_get_node_network_data(self): + @mock.patch.object(neutron_common, 'get_neutron_port_data', autospec=True) + def test_get_node_network_data(self, mock_gnpd): + mock_gnpd.return_value = {} + with task_manager.acquire(self.context, self.node.id) as task: network_data = self.interface.get_node_network_data(task) diff --git a/releasenotes/notes/node-network-data-6f998aaa57020f4b.yaml b/releasenotes/notes/node-network-data-6f998aaa57020f4b.yaml new file mode 100644 index 0000000000..d10f42b2b8 --- /dev/null +++ b/releasenotes/notes/node-network-data-6f998aaa57020f4b.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds `network_data` property to the node, a dictionary that represents the + node static network configuration. The Ironic API performs formal JSON + validation of node `network_data` content against user-supplied JSON schema + at driver validation step.