diff --git a/cloudbaseinit/metadata/services/base.py b/cloudbaseinit/metadata/services/base.py index 1cde6802..4e8b64cc 100644 --- a/cloudbaseinit/metadata/services/base.py +++ b/cloudbaseinit/metadata/services/base.py @@ -28,6 +28,9 @@ from cloudbaseinit.utils import encoding CONF = cloudbaseinit_conf.CONF LOG = oslo_logging.getLogger(__name__) +EXPERIMENTAL_NOTICE = ("EXPERIMENTAL: The structure and format of content " + "scoped under the 'ds' key may change in subsequent " + "releases of cloud-init.") class NotExistingMetadataException(Exception): @@ -236,28 +239,52 @@ class BaseMetadataService(object, metaclass=abc.ABCMeta): The ds namespace can change without prior notice and should not be used in production. """ - instance_id = self.get_instance_id() hostname = self.get_host_name() v1_data = { + "instance-id": instance_id, "instance_id": instance_id, + "local-hostname": hostname, "local_hostname": hostname, - "public_ssh_keys": self.get_public_keys() } # Copy the v1 data to the ds.meta_data and add more fields - ds_meta_data = copy.deepcopy(v1_data) - ds_meta_data.update({ - "hostname": hostname - }) + ds_meta_data = self._get_datasource_instance_meta_data() + if not ds_meta_data: + ds_meta_data = copy.deepcopy(v1_data) + ds_meta_data.update({ + "hostname": hostname + }) - return { + v1_data["public_ssh_keys"] = self.get_public_keys() + md = { "v1": v1_data, "ds": { + "_doc": EXPERIMENTAL_NOTICE, "meta_data": ds_meta_data, - } + }, + "instance-id": instance_id, + "instance_id": instance_id, + "local-hostname": hostname, + "local_hostname": hostname, + "public_ssh_keys": self.get_public_keys() } + return md + + def _get_datasource_instance_meta_data(self): + """Returns a dictionary with datasource specific instance data + + The instance data structure is based on the cloud-init specifications: + https://cloudinit.readthedocs.io/en/latest/explanation/instancedata.html + + Datasource-specific metadata crawled for the specific cloud platform. + It should closely represent the structure of the cloud metadata + crawled. The structure of content and details provided are entirely + cloud-dependent. + + """ + pass class BaseHTTPMetadataService(BaseMetadataService): diff --git a/cloudbaseinit/metadata/services/nocloudservice.py b/cloudbaseinit/metadata/services/nocloudservice.py index 9bb6d5b4..70190d01 100644 --- a/cloudbaseinit/metadata/services/nocloudservice.py +++ b/cloudbaseinit/metadata/services/nocloudservice.py @@ -30,6 +30,9 @@ from cloudbaseinit.utils import serialization CONF = cloudbaseinit_conf.CONF LOG = oslo_logging.getLogger(__name__) +DEFAULT_GATEWAY_CIDR_IPV4 = u"0.0.0.0/0" +DEFAULT_GATEWAY_CIDR_IPV6 = u"::/0" + class NoCloudNetworkConfigV1Parser(object): NETWORK_LINK_TYPE_PHY = 'physical' @@ -280,9 +283,6 @@ class NoCloudNetworkConfigV1Parser(object): class NoCloudNetworkConfigV2Parser(object): - DEFAULT_GATEWAY_CIDR_IPV4 = u"0.0.0.0/0" - DEFAULT_GATEWAY_CIDR_IPV6 = u"::/0" - NETWORK_LINK_TYPE_ETHERNET = 'ethernet' NETWORK_LINK_TYPE_BOND = 'bond' NETWORK_LINK_TYPE_VLAN = 'vlan' @@ -308,11 +308,11 @@ class NoCloudNetworkConfigV2Parser(object): default_route = None if gateway6 and netaddr.valid_ipv6(gateway6): default_route = network_model.Route( - network_cidr=self.DEFAULT_GATEWAY_CIDR_IPV6, + network_cidr=DEFAULT_GATEWAY_CIDR_IPV6, gateway=gateway6) elif gateway4 and netaddr.valid_ipv4(gateway4): default_route = network_model.Route( - network_cidr=self.DEFAULT_GATEWAY_CIDR_IPV4, + network_cidr=DEFAULT_GATEWAY_CIDR_IPV4, gateway=gateway4) if default_route: routes.append(default_route) @@ -324,9 +324,9 @@ class NoCloudNetworkConfigV2Parser(object): gateway = route_config.get("via") if network_cidr.lower() == "default": if netaddr.valid_ipv6(gateway): - network_cidr = self.DEFAULT_GATEWAY_CIDR_IPV6 + network_cidr = DEFAULT_GATEWAY_CIDR_IPV6 else: - network_cidr = self.DEFAULT_GATEWAY_CIDR_IPV4 + network_cidr = DEFAULT_GATEWAY_CIDR_IPV4 route = network_model.Route( network_cidr=network_cidr, gateway=gateway) @@ -547,6 +547,113 @@ class NoCloudNetworkConfigParser(object): return network_config_parser.parse(network_data) + @staticmethod + def network_details_v1_to_v2(v1_networks): + """Converts `NetworkDetails` objects to `NetworkDetailsV2` object. + + """ + if not v1_networks: + return None + + links = [] + networks = [] + services = [] + for nic in v1_networks: + link = network_model.Link( + id=nic.name, + name=nic.name, + type=network_model.LINK_TYPE_PHYSICAL, + mac_address=nic.mac, + enabled=None, + mtu=None, + bond=None, + vlan_link=None, + vlan_id=None, + ) + links.append(link) + + dns_addresses_v4 = [] + dns_addresses_v6 = [] + if nic.dnsnameservers: + for ns in nic.dnsnameservers: + if netaddr.valid_ipv6(ns): + dns_addresses_v6.append(ns) + else: + dns_addresses_v4.append(ns) + + dns_services_v6 = None + if dns_addresses_v6: + dns_service_v6 = network_model.NameServerService( + addresses=dns_addresses_v6, + search=None, + ) + dns_services_v6 = [dns_service_v6] + services.append(dns_service_v6) + + dns_services_v4 = None + if dns_addresses_v4: + dns_service_v4 = network_model.NameServerService( + addresses=dns_addresses_v4, + search=None, + ) + dns_services_v4 = [dns_service_v4] + services.append(dns_service_v4) + + # Note: IPv6 address might be set to IPv4 field + # Not sure if it's a bug + default_route_v6 = None + default_route_v4 = None + if nic.gateway6: + default_route_v6 = network_model.Route( + network_cidr=DEFAULT_GATEWAY_CIDR_IPV6, + gateway=nic.gateway6) + + if nic.gateway: + if netaddr.valid_ipv6(nic.gateway): + default_route_v6 = network_model.Route( + network_cidr=DEFAULT_GATEWAY_CIDR_IPV6, + gateway=nic.gateway) + else: + default_route_v4 = network_model.Route( + network_cidr=DEFAULT_GATEWAY_CIDR_IPV4, + gateway=nic.gateway) + + routes_v6 = [default_route_v6] if default_route_v6 else [] + routes_v4 = [default_route_v4] if default_route_v4 else [] + + if nic.address6: + net = network_model.Network( + link=link.name, + address_cidr=network_utils.ip_netmask_to_cidr( + nic.address6, nic.netmask6), + routes=routes_v6, + dns_nameservers=dns_services_v6, + ) + networks.append(net) + + if nic.address: + if netaddr.valid_ipv6(nic.address): + net = network_model.Network( + link=link.name, + address_cidr=network_utils.ip_netmask_to_cidr( + nic.address, nic.netmask), + routes=routes_v6, + dns_nameservers=dns_services_v6, + ) + else: + net = network_model.Network( + link=link.name, + address_cidr=network_utils.ip_netmask_to_cidr( + nic.address, nic.netmask), + routes=routes_v4, + dns_nameservers=dns_services_v4, + ) + networks.append(net) + + return network_model.NetworkDetailsV2(links=links, + networks=networks, + services=services) + class NoCloudConfigDriveService(baseconfigdrive.BaseConfigDriveService): diff --git a/cloudbaseinit/metadata/services/vmwareguestinfoservice.py b/cloudbaseinit/metadata/services/vmwareguestinfoservice.py index 2f865c72..d5452ee9 100644 --- a/cloudbaseinit/metadata/services/vmwareguestinfoservice.py +++ b/cloudbaseinit/metadata/services/vmwareguestinfoservice.py @@ -27,6 +27,7 @@ from cloudbaseinit import exception from cloudbaseinit.metadata.services import base from cloudbaseinit.metadata.services import nocloudservice from cloudbaseinit.osutils import factory as osutils_factory +from cloudbaseinit.utils import network from cloudbaseinit.utils import serialization CONF = cloudbaseinit_conf.CONF @@ -239,3 +240,22 @@ class VMwareGuestInfoService(base.BaseMetadataService): LOG.debug("network data %s", network) return {"network": network} + + def _get_datasource_instance_meta_data(self): + """Returns a dictionary with datasource specific instance data + + The instance data structure is based on the cloud-init specifications: + https://cloudinit.readthedocs.io/en/latest/explanation/instancedata.html + + Datasource-specific metadata crawled for the specific cloud platform. + It should closely represent the structure of the cloud metadata + crawled. The structure of content and details provided are entirely + cloud-dependent. + + """ + ds = dict() + network_details = self.get_network_details_v2() + host_info = network.get_host_info(self.get_host_name(), + network_details) + ds.update(host_info) + return ds diff --git a/cloudbaseinit/tests/metadata/services/test_base.py b/cloudbaseinit/tests/metadata/services/test_base.py index c6ee3dfa..17a9457e 100644 --- a/cloudbaseinit/tests/metadata/services/test_base.py +++ b/cloudbaseinit/tests/metadata/services/test_base.py @@ -61,38 +61,63 @@ class TestBase(unittest.TestCase): result = self._service.get_user_pwd_encryption_key() self.assertEqual(result, mock_get_public_keys.return_value[0]) + @mock.patch('cloudbaseinit.metadata.services.base.' + 'BaseMetadataService._get_datasource_instance_meta_data') @mock.patch('cloudbaseinit.metadata.services.base.' 'BaseMetadataService.get_public_keys') @mock.patch('cloudbaseinit.metadata.services.base.' 'BaseMetadataService.get_host_name') @mock.patch('cloudbaseinit.metadata.services.base.' 'BaseMetadataService.get_instance_id') - def test_get_instance_data(self, mock_instance_id, mock_hostname, - mock_public_keys): + def _test_get_instance_data_with_datasource_meta_data( + self, mock_instance_id, mock_hostname, mock_public_keys, + mock_get_datasource_instance_meta_data, datasource_meta_data=None): fake_instance_id = 'id' mock_instance_id.return_value = fake_instance_id fake_hostname = 'host' mock_hostname.return_value = fake_hostname fake_keys = ['ssh1', 'ssh2'] mock_public_keys.return_value = fake_keys + mock_get_datasource_instance_meta_data.return_value = \ + datasource_meta_data + if datasource_meta_data: + ds_md = datasource_meta_data + else: + ds_md = { + "instance_id": fake_instance_id, + "instance-id": fake_instance_id, + "local_hostname": fake_hostname, + "local-hostname": fake_hostname, + "hostname": fake_hostname + } expected_response = { 'v1': { "instance_id": fake_instance_id, + "instance-id": fake_instance_id, "local_hostname": fake_hostname, + "local-hostname": fake_hostname, "public_ssh_keys": fake_keys }, 'ds': { - 'meta_data': { - "instance_id": fake_instance_id, - "local_hostname": fake_hostname, - "public_ssh_keys": fake_keys, - "hostname": fake_hostname - }, - } + '_doc': base.EXPERIMENTAL_NOTICE, + 'meta_data': ds_md, + }, + "instance_id": fake_instance_id, + "instance-id": fake_instance_id, + "local_hostname": fake_hostname, + "local-hostname": fake_hostname, + "public_ssh_keys": fake_keys, } self.assertEqual(expected_response, self._service.get_instance_data()) + def test_get_instance_data(self): + self._test_get_instance_data_with_datasource_meta_data() + + def test_get_instance_data_with_datasource_meta_data(self): + self._test_get_instance_data_with_datasource_meta_data( + datasource_meta_data={'fake-data': 'fake-value'}) + class TestBaseHTTPMetadataService(unittest.TestCase): diff --git a/cloudbaseinit/tests/metadata/services/test_nocloudservice.py b/cloudbaseinit/tests/metadata/services/test_nocloudservice.py index fab3fe2d..d3266116 100644 --- a/cloudbaseinit/tests/metadata/services/test_nocloudservice.py +++ b/cloudbaseinit/tests/metadata/services/test_nocloudservice.py @@ -20,10 +20,16 @@ import unittest import unittest.mock as mock from cloudbaseinit.metadata.services import base +from cloudbaseinit.metadata.services.nocloudservice import \ + NoCloudNetworkConfigParser from cloudbaseinit.models import network as nm +from cloudbaseinit.tests.metadata import fake_json_response from cloudbaseinit.tests import testutils +from cloudbaseinit.utils import debiface +from cloudbaseinit.utils import network from cloudbaseinit.utils import serialization + MODULE_PATH = "cloudbaseinit.metadata.services.nocloudservice" NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_EMPTY_CONFIG = """ network: @@ -531,3 +537,98 @@ class TestNoCloudConfigDriveService(unittest.TestCase): mock_get_cache_data.assert_called_with( "network-config", decode=True) + + def test_to_network_details_v2(self): + date = "2013-04-04" + content = fake_json_response.get_fake_metadata_json(date) + nics = debiface.parse(content["network_config"]["debian_config"]) + v2 = NoCloudNetworkConfigParser.network_details_v1_to_v2(nics) + link0 = nm.Link( + id=fake_json_response.NAME0, + name=fake_json_response.NAME0, + type=nm.LINK_TYPE_PHYSICAL, + mac_address=fake_json_response.MAC0.upper(), + enabled=None, + mtu=None, + bond=None, + vlan_link=None, + vlan_id=None, + ) + link1 = nm.Link( + id=fake_json_response.NAME1, + name=fake_json_response.NAME1, + type=nm.LINK_TYPE_PHYSICAL, + mac_address=None, + enabled=None, + mtu=None, + bond=None, + vlan_link=None, + vlan_id=None, + ) + link2 = nm.Link( + id=fake_json_response.NAME2, + name=fake_json_response.NAME2, + type=nm.LINK_TYPE_PHYSICAL, + mac_address=fake_json_response.MAC2, + enabled=None, + mtu=None, + bond=None, + vlan_link=None, + vlan_id=None, + ) + dns_service0 = nm.NameServerService( + addresses=fake_json_response.DNSNS0.split(), + search=None, + ) + network0 = nm.Network( + link=fake_json_response.NAME0, + address_cidr=network.ip_netmask_to_cidr( + fake_json_response.ADDRESS0, fake_json_response.NETMASK0), + routes=[nm.Route( + network_cidr=u"0.0.0.0/0", + gateway=fake_json_response.GATEWAY0, + )], + dns_nameservers=[dns_service0], + ) + network0_v6 = nm.Network( + link=fake_json_response.NAME0, + address_cidr=network.ip_netmask_to_cidr( + fake_json_response.ADDRESS60, fake_json_response.NETMASK60), + routes=[nm.Route( + network_cidr=u"::/0", + gateway=fake_json_response.GATEWAY60, + )], + dns_nameservers=None, + ) + network1 = nm.Network( + link=fake_json_response.NAME1, + address_cidr=network.ip_netmask_to_cidr( + fake_json_response.ADDRESS1, fake_json_response.NETMASK1), + routes=[nm.Route( + network_cidr=u"0.0.0.0/0", + gateway=fake_json_response.GATEWAY1, + )], + dns_nameservers=None, + ) + network2 = nm.Network( + link=fake_json_response.NAME2, + address_cidr=network.ip_netmask_to_cidr( + fake_json_response.ADDRESS2, fake_json_response.NETMASK2), + routes=[nm.Route( + network_cidr=u"::/0", + gateway=fake_json_response.GATEWAY2, + )], + dns_nameservers=None, + ) + expected = nm.NetworkDetailsV2( + links=[ + link0, link1, link2, + ], + networks=[ + network0_v6, network0, network1, network2, + ], + services=[ + dns_service0 + ], + ) + self.assertEqual(expected, v2) diff --git a/cloudbaseinit/tests/metadata/services/test_vmwareguestinfoservice.py b/cloudbaseinit/tests/metadata/services/test_vmwareguestinfoservice.py index a2dcab73..b80e365d 100644 --- a/cloudbaseinit/tests/metadata/services/test_vmwareguestinfoservice.py +++ b/cloudbaseinit/tests/metadata/services/test_vmwareguestinfoservice.py @@ -292,3 +292,10 @@ class VMwareGuestInfoServiceTest(unittest.TestCase): mock_get_guestinfo_value.return_value = "no encoding" self.assertRaises(exception.CloudbaseInitException, self._service._get_guest_data, 'fake_key') + + @mock.patch("cloudbaseinit.utils.network.get_host_info") + def test_get_datasource_instance_meta_data(self, mock_get_host_info): + expect_md = {'network': 'fake_host_info'} + mock_get_host_info.return_value = expect_md + self.assertEqual(expect_md, + self._service._get_datasource_instance_meta_data()) diff --git a/cloudbaseinit/tests/utils/test_network.py b/cloudbaseinit/tests/utils/test_network.py index f895dab3..3d39dec3 100644 --- a/cloudbaseinit/tests/utils/test_network.py +++ b/cloudbaseinit/tests/utils/test_network.py @@ -12,10 +12,12 @@ # License for the specific language governing permissions and limitations # under the License. +import collections import unittest import unittest.mock as mock from cloudbaseinit import conf as cloudbaseinit_conf +from cloudbaseinit.models import network as network_model from cloudbaseinit.tests import testutils from cloudbaseinit.utils import network @@ -24,6 +26,89 @@ CONF = cloudbaseinit_conf.CONF class NetworkUtilsTest(unittest.TestCase): + link0 = network_model.Link( + id="eth0", + name="eth0", + type=network_model.LINK_TYPE_PHYSICAL, + mac_address="ab:cd:ef:ef:cd:ab", + enabled=None, + mtu=None, + bond=None, + vlan_link=None, + vlan_id=None, + ) + network_private_default_route = network_model.Network( + link="eth0", + address_cidr="192.168.1.2/24", + routes=[ + network_model.Route( + network_cidr=u"0.0.0.0/0", + gateway="192.168.1.1", + ), + ], + dns_nameservers=[] + ) + network_public = network_model.Network( + link="eth0", + address_cidr="2.3.4.2/24", + routes=[ + network_model.Route( + network_cidr=u"2.3.4.1/24", + gateway="2.3.4.1", + ), + ], + dns_nameservers=[] + ) + network_public_default_route = network_model.Network( + link="eth0", + address_cidr="2.3.4.2/24", + routes=[ + network_model.Route( + network_cidr=u"0.0.0.0/0", + gateway="2.3.4.1", + ), + ], + dns_nameservers=[] + ) + network_private = network_model.Network( + link="eth0", + address_cidr="172.10.1.2/24", + routes=[ + network_model.Route( + network_cidr=u"172.10.1.1/24", + gateway="172.10.1.1", + ), + ], + dns_nameservers=[] + ) + network_local = network_model.Network( + link="eth0", + address_cidr="127.0.0.4/24", + routes=[ + network_model.Route( + network_cidr=u"127.0.0.4/24", + gateway="127.0.0.2", + ), + ], + dns_nameservers=[] + ) + ipv6_addr = '1a8f:9aaf:2904:858f:1bce:6f85:2b04:f38' + network_v6 = network_model.Network( + link="eth0", + address_cidr=ipv6_addr + '/64', + routes=[network_model.Route( + network_cidr=u"::/0", + gateway="::1", + )], + dns_nameservers=[] + ) + ipv6_addr_private = 'fe80::216:3eff:fe16:db54' + network_v6_local = network_model.Network( + link="eth0", + address_cidr=ipv6_addr_private + '/64', + routes=[], + dns_nameservers=[] + ) @mock.patch('urllib.request.urlopen') def test_check_url(self, mock_url_open): @@ -110,3 +195,152 @@ class NetworkUtilsTest(unittest.TestCase): fake_netmask = None self._test_ip_netmask_to_cidr(fake_ip_address, fake_ip_address, fake_netmask) + + def test_get_default_ip_addresses(self): + network_details = network_model.NetworkDetailsV2( + links=[ + self.link0 + ], + networks=[ + self.network_private_default_route, + ], + services=[ + ], + ) + ipv4, ipv6 = network.get_default_ip_addresses(network_details) + self.assertEqual('192.168.1.2', ipv4) + self.assertIsNone(ipv6) + + def test_get_default_ip_addresses_link_local(self): + network_details = network_model.NetworkDetailsV2( + links=[ + self.link0 + ], + networks=[ + self.network_private, + ], + services=[ + ], + ) + ipv4, ipv6 = network.get_default_ip_addresses(network_details) + self.assertIsNone(ipv4) + self.assertIsNone(ipv6) + + def test_get_default_ip_addresses_public_default_route(self): + network_details = network_model.NetworkDetailsV2( + links=[ + self.link0 + ], + networks=[ + self.network_public_default_route, + ], + services=[ + ], + ) + ipv4, ipv6 = network.get_default_ip_addresses(network_details) + self.assertEqual("2.3.4.2", ipv4) + self.assertIsNone(ipv6) + + def test_get_default_ip_addresses_v6(self): + network_details = network_model.NetworkDetailsV2( + links=[ + self.link0 + ], + networks=[ + self.network_v6, + ], + services=[ + ], + ) + ipv4, ipv6 = network.get_default_ip_addresses(network_details) + self.assertIsNone(ipv4) + self.assertEqual(self.ipv6_addr, ipv6) + + def test_get_default_ip_addresses_v6_local(self): + network_details = network_model.NetworkDetailsV2( + links=[ + self.link0 + ], + networks=[ + self.network_v6_local, + ], + services=[ + ], + ) + ipv4, ipv6 = network.get_default_ip_addresses(network_details) + self.assertIsNone(ipv4) + self.assertIsNone(ipv6) + + def test_get_default_ip_addresses_dual_stack(self): + network_details = network_model.NetworkDetailsV2( + links=[ + self.link0 + ], + networks=[ + self.network_private_default_route, + self.network_public, + self.network_v6, + ], + services=[ + ], + ) + ipv4, ipv6 = network.get_default_ip_addresses(network_details) + self.assertEqual('192.168.1.2', ipv4) + self.assertEqual(self.ipv6_addr, ipv6) + + def test_get_host_info(self): + network_details = network_model.NetworkDetailsV2( + links=[ + self.link0 + ], + networks=[ + self.network_private_default_route, + self.network_public, self.network_v6, + ], + services=[ + ], + ) + expect = { + 'hostname': 'fake_host', + 'local-hostname': 'fake_host', + 'local-ipv4': '192.168.1.2', + 'local_ipv4': '192.168.1.2', + 'local-ipv6': '1a8f:9aaf:2904:858f:1bce:6f85:2b04:f38', + 'local_ipv6': '1a8f:9aaf:2904:858f:1bce:6f85:2b04:f38', + 'local_hostname': 'fake_host', + 'network': {'interfaces': { + 'by-ipv4': collections.OrderedDict([ + ('192.168.1.2', + {'broadcast': '192.168.1.255', + 'mac': 'ab:cd:ef:ef:cd:ab', + 'netmask': '255.255.255.0'} + ), + ('2.3.4.2', + {'broadcast': '2.3.4.255', + 'mac': 'ab:cd:ef:ef:cd:ab', + 'netmask': '255.255.255.0'} + ) + ]), + 'by-ipv6': collections.OrderedDict([ + ('1a8f:9aaf:2904:858f:1bce:6f85:2b04:f38', + {'broadcast': '1a8f:9aaf:2904:858f:ffff:ffff:ffff:ffff', + 'mac': 'ab:cd:ef:ef:cd:ab', + } + ) + ]), + 'by-mac': collections.OrderedDict([ + ('ab:cd:ef:ef:cd:ab', + {'ipv4': {'addr': '2.3.4.2', + 'broadcast': '2.3.4.255', + 'netmask': '255.255.255.0'}, + 'ipv6': { + 'addr': '1a8f:9aaf:2904:858f:1bce:6f85:2b04:f38', + 'broadcast': '1a8f:9aaf:2904:858f:' + 'ffff:ffff:ffff:ffff'} + } + ), + ]) + }} + } + host_info = network.get_host_info('fake_host', network_details) + self.assertEqual(expect, host_info) diff --git a/cloudbaseinit/utils/network.py b/cloudbaseinit/utils/network.py index 9886dfe3..ccef427a 100644 --- a/cloudbaseinit/utils/network.py +++ b/cloudbaseinit/utils/network.py @@ -14,6 +14,9 @@ import binascii +import collections +import ipaddress + import netaddr import socket import struct @@ -28,6 +31,10 @@ from cloudbaseinit.osutils import factory as osutils_factory LOG = oslo_logging.getLogger(__name__) MAX_URL_CHECK_RETRIES = 3 +DEFAULT_GATEWAY_CIDR_IPV4 = u"0.0.0.0/0" +DEFAULT_GATEWAY_CIDR_IPV6 = u"::/0" +LOCAL_IPV4 = "local-ipv4" +LOCAL_IPV6 = "local-ipv6" def get_local_ip(address=None): @@ -98,3 +105,135 @@ def ip_netmask_to_cidr(ip_address, netmask): prefix_len = netaddr.IPNetwork( u"%s/%s" % (ip_address, netmask)).prefixlen return u"%s/%s" % (ip_address, prefix_len) + + +def get_default_ip_addresses(network_details): + ipv4_address = None + ipv6_address = None + if network_details: + for net in network_details.networks: + ip_net = netaddr.IPNetwork(net.address_cidr) + addr = ip_net.ip + default_route = False + for route in net.routes: + if addr.version == 6 and \ + route.network_cidr == DEFAULT_GATEWAY_CIDR_IPV6: + default_route = True + + elif addr.version == 4 and \ + route.network_cidr == DEFAULT_GATEWAY_CIDR_IPV4: + default_route = True + + if not default_route: + continue + + if not ipv6_address and addr.version == 6: + v6_addr = ipaddress.IPv6Address(addr) + if v6_addr.is_private or v6_addr.is_global: + ipv6_address = str(v6_addr) + + if not ipv4_address and addr.version == 4: + v4_addr = ipaddress.IPv4Address(addr) + if v4_addr.is_private or v4_addr.is_global: + ipv4_address = str(v4_addr) + + return ipv4_address, ipv6_address + + +def get_host_info(hostname, network_details): + """Returns host information such as the host name and network interfaces. + + """ + host_info = { + "network": { + "interfaces": { + "by-mac": collections.OrderedDict(), + "by-ipv4": collections.OrderedDict(), + "by-ipv6": collections.OrderedDict(), + }, + }, + } + if hostname: + host_info["hostname"] = hostname + host_info["local-hostname"] = hostname + host_info["local_hostname"] = hostname + + by_mac = host_info["network"]["interfaces"]["by-mac"] + by_ipv4 = host_info["network"]["interfaces"]["by-ipv4"] + by_ipv6 = host_info["network"]["interfaces"]["by-ipv6"] + + if not network_details: + return host_info + + default_ipv4, default_ipv6 = get_default_ip_addresses(network_details) + if default_ipv4: + host_info[LOCAL_IPV4] = default_ipv4 + host_info[LOCAL_IPV4.replace('-', '_')] = default_ipv4 + if default_ipv6: + host_info[LOCAL_IPV6] = default_ipv6 + host_info[LOCAL_IPV6.replace('-', '_')] = default_ipv6 + + """ + IPv4: { + 'bcast': '', + 'ip': '127.0.0.1', + 'mask': '255.0.0.0', + 'scope': 'host', + } + IPv6: { + 'ip': '::1/128', + 'scope6': 'host' + } + """ + mac_by_link_names = {} + for link in network_details.links: + mac_by_link_names[link.name] = link.mac_address + + for net in network_details.networks: + mac = mac_by_link_names[net.link] + + # Do not bother recording localhost + if mac == "00:00:00:00:00:00": + continue + + ip_net = netaddr.IPNetwork(net.address_cidr) + addr = ip_net.ip + is_v6 = addr.version == 6 + is_v4 = addr.version == 4 + + if mac: + if mac not in by_mac: + val = {} + else: + val = by_mac[mac] + key = None + if is_v4: + key = 'ipv4' + val[key] = { + 'addr': str(addr), + 'netmask': str(ip_net.netmask), + 'broadcast': str(ip_net.broadcast), + } + elif is_v6: + key = 'ipv6' + val[key] = { + 'addr': str(addr), + 'broadcast': str(ip_net.broadcast), + } + if key: + by_mac[mac] = val + + if is_v4: + by_ipv4[str(addr)] = { + 'mac': mac, + 'netmask': str(ip_net.netmask), + 'broadcast': str(ip_net.broadcast), + } + + if is_v6: + by_ipv6[str(addr)] = { + 'mac': mac, + 'broadcast': str(ip_net.broadcast), + } + + return host_info diff --git a/doc/source/userdata.rst b/doc/source/userdata.rst index afe15e9f..10dc994b 100644 --- a/doc/source/userdata.rst +++ b/doc/source/userdata.rst @@ -99,6 +99,9 @@ The following cloud-config directives are supported: 1. path - Absolute path on disk where the content should be written. 2. content - The content which will be written in the given file. + Instance metadata can be used as template variables. + You may refer to https://cloudinit.readthedocs.io/en/latest/explanation/instancedata.html + for the example of instance data. 3. permissions - Integer representing file permissions. 4. encoding - The encoding of the data in content. Supported encodings are: b64, base64 for base64-encoded content, gz, @@ -133,6 +136,11 @@ The following cloud-config directives are supported: H4sIAGUfoFQC/zMxAgCIsCQyAgAAAA== path: C:\gzip permissions: '0644' + - path: C:\run\node-config.yaml + permissions: '0640' + content: | + --- + node_ip: '{{ ds.meta_data.local_ipv4 }}' * set_timezone - Change the underlying timezone.