diff --git a/cloudbaseinit/metadata/services/baseopenstackservice.py b/cloudbaseinit/metadata/services/baseopenstackservice.py index 6f84da89..453f83d8 100644 --- a/cloudbaseinit/metadata/services/baseopenstackservice.py +++ b/cloudbaseinit/metadata/services/baseopenstackservice.py @@ -16,14 +16,28 @@ import json import posixpath +import netaddr from oslo_log import log as oslo_logging from cloudbaseinit import conf as cloudbaseinit_conf +from cloudbaseinit import exception from cloudbaseinit.metadata.services import base +from cloudbaseinit.models import network as network_model from cloudbaseinit.utils import debiface from cloudbaseinit.utils import encoding from cloudbaseinit.utils import x509constants +NETWORK_LINK_TYPE_PHYSICAL = "phy" +NETWORK_LINK_TYPE_BOND = "bond" +NETWORK_LINK_TYPE_VLAN = "vlan" + +NETWORK_TYPE_IPV4 = "ipv4" +NETWORK_TYPE_IPV4_DHCP = "ipv4_dhcp" +NETWORK_TYPE_IPV6 = "ipv6" +NETWORK_TYPE_IPV6_DHCP = "ipv6_dhcp" + +NETWORK_SERVICE_TYPE_DNS = "dns" + CONF = cloudbaseinit_conf.CONF LOG = oslo_logging.getLogger(__name__) @@ -40,13 +54,19 @@ class BaseOpenStackService(base.BaseMetadataService): posixpath.join('openstack', 'latest', 'user_data')) return self._get_cache_data(path) - def _get_meta_data(self, version='latest'): + def _get_openstack_json_data(self, version, file_name): path = posixpath.normpath( - posixpath.join('openstack', version, 'meta_data.json')) + posixpath.join('openstack', version, file_name)) data = self._get_cache_data(path, decode=True) if data: return json.loads(data) + def _get_meta_data(self, version='latest'): + return self._get_openstack_json_data(version, 'meta_data.json') + + def _get_network_data(self, version='latest'): + return self._get_openstack_json_data(version, 'network_data.json') + def get_instance_id(self): return self._get_meta_data().get('uuid') @@ -81,6 +101,158 @@ class BaseOpenStackService(base.BaseMetadataService): return debiface.parse(content) + @staticmethod + def _ip_netmask_to_cidr(ip_address, netmask): + if netmask is None: + return ip_address + prefix_len = netaddr.IPNetwork( + u"%s/%s" % (ip_address, netmask)).prefixlen + return u"%s/%s" % (ip_address, prefix_len) + + @staticmethod + def _parse_network_data_links(links_data): + links = [] + for link_data in links_data: + link_id = link_data.get("id") + mac = link_data.get("ethernet_mac_address") + mtu = link_data.get("mtu") + openstack_link_type = link_data.get("type") + + bond = None + vlan_id = None + vlan_link = None + if openstack_link_type == NETWORK_LINK_TYPE_BOND: + link_type = network_model.LINK_TYPE_BOND + bond_links = link_data.get("bond_links") + bond_mode = link_data.get("bond_mode") + bond_xmit_hash_policy = link_data.get("bond_xmit_hash_policy") + + if bond_mode not in network_model.AVAILABLE_BOND_TYPES: + raise exception.CloudbaseInitException( + "Unsupported bond mode: %s" % bond_mode) + + if (bond_xmit_hash_policy is not None and + bond_xmit_hash_policy not in + network_model.AVAILABLE_BOND_LB_ALGORITHMS): + raise exception.CloudbaseInitException( + "Unsupported bond hash policy: %s" % + bond_xmit_hash_policy) + + bond = network_model.Bond( + members=bond_links, + type=bond_mode, + lb_algorithm=bond_xmit_hash_policy, + lacp_rate=None, + ) + elif openstack_link_type == NETWORK_LINK_TYPE_VLAN: + link_type = network_model.LINK_TYPE_VLAN + vlan_id = link_data.get("vlan_id") + vlan_link = link_data.get("vlan_link") + vlan_mac_address = link_data.get("vlan_mac_address") + if vlan_mac_address is not None: + mac = vlan_mac_address + else: + # Any other link type is considered physical + link_type = network_model.LINK_TYPE_PHYSICAL + + link = network_model.Link( + id=link_id, + name=link_id, + type=link_type, + enabled=True, + mac_address=mac, + mtu=mtu, + bond=bond, + vlan_id=vlan_id, + vlan_link=vlan_link) + links.append(link) + + return links + + @staticmethod + def _parse_dns_data(services_data): + dns_nameservers = [] + for service_data in services_data: + service_type = service_data.get("type") + if service_type != NETWORK_SERVICE_TYPE_DNS: + LOG.warn("Skipping unsupported service type: %s", service_type) + continue + + address = service_data.get("address") + if address is not None: + dns_nameservers.append(address) + + return dns_nameservers + + @staticmethod + def _parse_network_data_networks(networks_data): + networks = [] + for network_data in networks_data: + network_type = network_data.get("type") + if network_type not in [NETWORK_TYPE_IPV4, NETWORK_TYPE_IPV6]: + continue + + link_id = network_data.get("link") + ip_address = network_data.get("ip_address") + netmask = network_data.get("netmask") + address_cidr = BaseOpenStackService._ip_netmask_to_cidr( + ip_address, netmask) + + routes = [] + for route_data in network_data.get("routes", []): + gateway = route_data.get("gateway") + network = route_data.get("network") + netmask = route_data.get("netmask") + network_cidr = BaseOpenStackService._ip_netmask_to_cidr( + network, netmask) + + route = network_model.Route( + network_cidr=network_cidr, + gateway=gateway + ) + routes.append(route) + + dns_nameservers = BaseOpenStackService._parse_dns_data( + network_data.get("services", [])) + + network = network_model.Network( + link=link_id, + address_cidr=address_cidr, + dns_nameservers=dns_nameservers, + routes=routes + ) + networks.append(network) + + return networks + + @staticmethod + def _parse_network_data_services(services_data): + services = [] + dns_nameservers = BaseOpenStackService._parse_dns_data(services_data) + if len(dns_nameservers): + service = network_model.NameServerService( + addresses=dns_nameservers, + search=None + ) + services.append(service) + return services + + def get_network_details_v2(self): + network_data = self._get_network_data() + + links = self._parse_network_data_links( + network_data.get("links", [])) + networks = self._parse_network_data_networks( + network_data.get("networks", [])) + services = self._parse_network_data_services( + network_data.get("services", [])) + + return network_model.NetworkDetailsV2( + links=links, + networks=networks, + services=services + ) + def get_admin_password(self): meta_data = self._get_meta_data() meta = meta_data.get('meta') diff --git a/cloudbaseinit/tests/metadata/services/test_baseopenstackservice.py b/cloudbaseinit/tests/metadata/services/test_baseopenstackservice.py index 548d6fd7..684c49aa 100644 --- a/cloudbaseinit/tests/metadata/services/test_baseopenstackservice.py +++ b/cloudbaseinit/tests/metadata/services/test_baseopenstackservice.py @@ -17,12 +17,15 @@ import functools import posixpath import unittest +import netaddr + try: import unittest.mock as mock except ImportError: import mock from cloudbaseinit import conf as cloudbaseinit_conf +from cloudbaseinit import exception from cloudbaseinit.metadata.services import base from cloudbaseinit.metadata.services import baseopenstackservice from cloudbaseinit.models import network as network_model @@ -265,3 +268,223 @@ class TestBaseOpenStackService(unittest.TestCase): def test_get_network_details(self): self._partial_test_get_network_details() + + @staticmethod + def _get_network_data(): + return { + "links": [{ + "ethernet_mac_address": mock.sentinel.link_mac1, + "type": baseopenstackservice.NETWORK_LINK_TYPE_PHYSICAL, + "id": mock.sentinel.link_id1, + "mtu": mock.sentinel.link_mtu1, + }, { + "ethernet_mac_address": mock.sentinel.link_mac2, + "type": mock.sentinel.another_link_type, + "id": mock.sentinel.link_id2, + "mtu": mock.sentinel.link_mtu2, + }, { + "bond_miimon": mock.sentinel.bond_miimon1, + "bond_xmit_hash_policy": mock.sentinel.bond_lb_algo1, + "ethernet_mac_address": mock.sentinel.bond_mac1, + "mtu": mock.sentinel.bond_mtu1, + "bond_mode": mock.sentinel.bond_type1, + "bond_links": [ + mock.sentinel.link_id1, + mock.sentinel.link_id2, + ], + "type": baseopenstackservice.NETWORK_LINK_TYPE_BOND, + "id": mock.sentinel.bond_id1, + }, { + "id": mock.sentinel.vlan_link_id1, + "type": baseopenstackservice.NETWORK_LINK_TYPE_VLAN, + "vlan_link": mock.sentinel.bond_id1, + "vlan_id": mock.sentinel.vlan_id1, + "mtu": mock.sentinel.vlan_mtu1, + "ethernet_mac_address": mock.sentinel.vlan_mac1, + }], + "networks": [{ + "id": mock.sentinel.network_id1, + "network_id": mock.sentinel.network_openstack_id1, + "link": mock.sentinel.bond_id1, + "type": baseopenstackservice.NETWORK_TYPE_IPV4_DHCP, + }, { + "id": mock.sentinel.network_id2, + "type": baseopenstackservice.NETWORK_TYPE_IPV4, + "link": mock.sentinel.bond_id1, + "ip_address": mock.sentinel.ip_address1, + "netmask": mock.sentinel.netmask1, + "services": [{ + "type": baseopenstackservice.NETWORK_SERVICE_TYPE_DNS, + "address": mock.sentinel.dns1, + }, { + "type": baseopenstackservice.NETWORK_SERVICE_TYPE_DNS, + "address": mock.sentinel.dns2 + }], + "routes": [{ + "network": mock.sentinel.route_network1, + "netmask": mock.sentinel.route_netmask1, + "gateway": mock.sentinel.route_gateway1, + }, { + "network": mock.sentinel.route_network2, + "netmask": mock.sentinel.route_netmask2, + "gateway": mock.sentinel.route_gateway2, + }], + "network_id": mock.sentinel.network_openstack_id2 + }, { + "id": mock.sentinel.network_id3, + "type": baseopenstackservice.NETWORK_TYPE_IPV6, + "link": mock.sentinel.bond_id1, + "ip_address": mock.sentinel.ip_address_ipv61, + "routes": [{ + "network": mock.sentinel.route_network_ipv61, + "gateway": mock.sentinel.route_gateway_ipv61, + }], + "network_id": mock.sentinel.network_openstack_id3 + }], + "services": [{ + "type": baseopenstackservice.NETWORK_SERVICE_TYPE_DNS, + "address": mock.sentinel.dns3, + }, { + "type": baseopenstackservice.NETWORK_SERVICE_TYPE_DNS, + "address": mock.sentinel.dns4 + }], + } + + @mock.patch(MODPATH + ".BaseOpenStackService._get_network_data") + def _test_get_network_details_v2(self, mock_get_network_data, + invalid_bond_type=False, + invalid_bond_lb_algo=False): + mock.sentinel.ip_address1 = "10.0.0.1" + mock.sentinel.netmask1 = "255.255.255.0" + mock.sentinel.route_network1 = "172.16.0.0" + mock.sentinel.route_netmask1 = "255.255.0.0" + mock.sentinel.route_gateway1 = "172.16.1.1" + mock.sentinel.route_network2 = "0.0.0.0" + mock.sentinel.route_netmask2 = "0.0.0.0" + mock.sentinel.route_gateway2 = "10.0.0.254" + mock.sentinel.ip_address_ipv61 = "2001:cdba::3257:9652/24" + mock.sentinel.route_network_ipv61 = "::/0" + mock.sentinel.route_gateway_ipv61 = "fd00::1" + + if invalid_bond_type: + mock.sentinel.bond_type1 = "invalid bond type" + else: + mock.sentinel.bond_type1 = network_model.BOND_TYPE_ACTIVE_BACKUP + + if invalid_bond_lb_algo: + mock.sentinel.bond_lb_algo1 = "invalid lb algorithm" + else: + mock.sentinel.bond_lb_algo1 = network_model.BOND_LB_ALGO_L2 + + network_data = self._get_network_data() + + mock_get_network_data.return_value = network_data + + if invalid_bond_type or invalid_bond_lb_algo: + with self.assertRaises(exception.CloudbaseInitException): + self._service.get_network_details_v2() + return + + network_details = self._service.get_network_details_v2() + + self.assertEqual( + len(network_data["links"]), len(network_details.links)) + + self.assertEqual(1, len([ + l for l in network_details.links if + l.type == network_model.LINK_TYPE_PHYSICAL and + l.id == mock.sentinel.link_id1 and + l.name == mock.sentinel.link_id1 and + l.mac_address == mock.sentinel.link_mac1 and + l.mtu == mock.sentinel.link_mtu1])) + + self.assertEqual(1, len([ + l for l in network_details.links if + l.type == network_model.LINK_TYPE_PHYSICAL and + l.id == mock.sentinel.link_id2 and + l.name == mock.sentinel.link_id2 and + l.mac_address == mock.sentinel.link_mac2 and + l.mtu == mock.sentinel.link_mtu2])) + + self.assertEqual(1, len([ + l for l in network_details.links if + l.type == network_model.LINK_TYPE_BOND and + l.id == mock.sentinel.bond_id1 and + l.name == mock.sentinel.bond_id1 and + l.mtu == mock.sentinel.bond_mtu1 and + l.mac_address == mock.sentinel.bond_mac1 and + l.vlan_link is None and + l.vlan_id is None and + l.bond.type == network_model.BOND_TYPE_ACTIVE_BACKUP and + l.bond.members == [ + mock.sentinel.link_id1, mock.sentinel.link_id2] and + l.bond.lb_algorithm == network_model.BOND_LB_ALGO_L2 and + l.bond.lacp_rate is None])) + + self.assertEqual(1, len([ + l for l in network_details.links if + l.type == network_model.LINK_TYPE_VLAN and + l.id == mock.sentinel.vlan_link_id1 and + l.name == mock.sentinel.vlan_link_id1 and + l.mac_address == mock.sentinel.vlan_mac1 and + l.mtu == mock.sentinel.vlan_mtu1 and + l.vlan_link == mock.sentinel.bond_id1 and + l.vlan_id == mock.sentinel.vlan_id1])) + + self.assertEqual( + len([n for n in network_data["networks"] + if n["type"] in [ + baseopenstackservice.NETWORK_TYPE_IPV4, + baseopenstackservice.NETWORK_TYPE_IPV6]]), + len(network_details.networks)) + + def _get_cidr_address(ip_address, netmask): + prefix_len = netaddr.IPNetwork( + u"%s/%s" % (ip_address, netmask)).prefixlen + return u"%s/%s" % (ip_address, prefix_len) + + address_cidr = _get_cidr_address( + mock.sentinel.ip_address1, mock.sentinel.netmask1) + + network = [ + n for n in network_details.networks + if n.address_cidr == address_cidr and + n.dns_nameservers == [mock.sentinel.dns1, mock.sentinel.dns2] and + n.link == mock.sentinel.bond_id1] + self.assertEqual(1, len(network)) + + network_cidr1 = _get_cidr_address( + mock.sentinel.route_network1, mock.sentinel.route_netmask1) + + network_cidr2 = _get_cidr_address( + mock.sentinel.route_network2, mock.sentinel.route_netmask2) + + self.assertEqual([ + network_model.Route( + network_cidr=network_cidr1, + gateway=mock.sentinel.route_gateway1), + network_model.Route( + network_cidr=network_cidr2, + gateway=mock.sentinel.route_gateway2)], + network[0].routes) + + network_ipv6 = [ + n for n in network_details.networks + if n.address_cidr == mock.sentinel.ip_address_ipv61 and + n.link == mock.sentinel.bond_id1] + self.assertEqual(1, len(network_ipv6)) + + self.assertEqual( + [network_model.NameServerService( + addresses=[mock.sentinel.dns3, mock.sentinel.dns4], + search=None)], + network_details.services) + + def test_get_network_details_v2(self): + self._test_get_network_details_v2() + + def test_get_network_details_v2_invalid_bond_type(self): + self._test_get_network_details_v2(invalid_bond_type=True) + + def test_get_network_details_v2_invalid_bond_lb_algo(self): + self._test_get_network_details_v2(invalid_bond_lb_algo=True)