diff options
author | Louis Bouchard <bouchard.louis@gmail.com> | 2023-04-05 23:11:07 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-05 15:11:07 -0600 |
commit | e02c4607923af64311c70ae9a06d6794355e9a61 (patch) | |
tree | 5a237764222434efc9db0d1d0bc0c9c4725a448f /cloudinit | |
parent | 6d42aa8e2c1a5454a658ab4e2b9cead2677c77cd (diff) | |
download | cloud-init-git-e02c4607923af64311c70ae9a06d6794355e9a61.tar.gz |
Adapt DataSourceScaleway to upcoming IPv6 support (#2033)
Add support for IPv4 and/or IPv6 provided metadata. Upcoming
features will allow for Instances to be started with only IPv6
connectivity. The datasource must be able to fetch its metadata from
the following endpoints :
* IPv4 on http://169.264.42.42/conf
* IPv6 on http://[fd00:42::42]/conf
This URL may eventually be overridden in the future by a DNS
resolvable URL defined in /etc/cloud/config.d/scaleway.conf
Add support for the configuration of one or many IP addresses when
provided by the `public_ips` metadata key. In such configurations,
instances no longer have a `private_ip`. The previous configuration
method is kept for backward compatibility.
This commit also fixes a bug in the definition of ssh keys from tags.
Diffstat (limited to 'cloudinit')
-rw-r--r-- | cloudinit/sources/DataSourceScaleway.py | 245 |
1 files changed, 197 insertions, 48 deletions
diff --git a/cloudinit/sources/DataSourceScaleway.py b/cloudinit/sources/DataSourceScaleway.py index f45f9b04..5c420398 100644 --- a/cloudinit/sources/DataSourceScaleway.py +++ b/cloudinit/sources/DataSourceScaleway.py @@ -9,8 +9,10 @@ import json import os import socket import time +from urllib.parse import urlparse import requests +from requests.exceptions import ConnectionError # Note: `urllib3` is transitively installed by `requests` from urllib3.connection import HTTPConnection @@ -21,20 +23,16 @@ from cloudinit import log as logging from cloudinit import net, sources, url_helper, util from cloudinit.event import EventScope, EventType from cloudinit.net.dhcp import NoDHCPLeaseError -from cloudinit.net.ephemeral import EphemeralDHCPv4 +from cloudinit.net.ephemeral import EphemeralDHCPv4, EphemeralIPv6Network from cloudinit.sources import DataSourceHostname +from cloudinit.subp import ProcessExecutionError LOG = logging.getLogger(__name__) -DS_BASE_URL = "http://169.254.42.42" +DS_BASE_URLS = ["http://169.254.42.42", "http://[fd00:42::42]"] -BUILTIN_DS_CONFIG = { - "metadata_url": DS_BASE_URL + "/conf?format=json", - "userdata_url": DS_BASE_URL + "/user_data/cloud-init", - "vendordata_url": DS_BASE_URL + "/vendor_data/cloud-init", -} - -DEF_MD_RETRIES = 5 +DEF_MD_RETRIES = 3 +DEF_MD_MAX_WAIT = 2 DEF_MD_TIMEOUT = 10 @@ -117,9 +115,23 @@ def query_data_api(api_type, api_address, retries, timeout): "Trying to get %s data (bind on port %d)...", api_type, port ) requests_session = requests.Session() + # Adapt Session.mount to IPv4/IPv6 context + localhost = "0.0.0.0" + try: + url_address = urlparse(api_address).netloc + address = url_address + if url_address[0] == "[": + address = url_address[1:-1] + addr_proto = socket.getaddrinfo( + address, None, proto=socket.IPPROTO_TCP + )[0][0] + if addr_proto == socket.AF_INET6: + localhost = "0::" + except ValueError: + pass requests_session.mount( "http://", - SourceAddressAdapter(source_address=("0.0.0.0", port)), + SourceAddressAdapter(source_address=(localhost, port)), ) data = query_data_api_once( api_address, timeout=timeout, requests_session=requests_session @@ -154,30 +166,59 @@ class DataSourceScaleway(sources.DataSource): self.ds_cfg = util.mergemanydict( [ util.get_cfg_by_path(sys_cfg, ["datasource", "Scaleway"], {}), - BUILTIN_DS_CONFIG, ] ) - self.metadata_address = self.ds_cfg["metadata_url"] - self.userdata_address = self.ds_cfg["userdata_url"] - self.vendordata_address = self.ds_cfg["vendordata_url"] - self.retries = int(self.ds_cfg.get("retries", DEF_MD_RETRIES)) self.timeout = int(self.ds_cfg.get("timeout", DEF_MD_TIMEOUT)) + self.max_wait = int(self.ds_cfg.get("max_wait", DEF_MD_MAX_WAIT)) self._fallback_interface = None self._network_config = sources.UNSET + self.metadata_urls = DS_BASE_URLS + self.userdata_url = None + self.vendordata_url = None + self.ephemeral_fixed_address = None + self.has_ipv4 = True + if "metadata_urls" in self.ds_cfg.keys(): + self.metadata_urls += self.ds_cfg["metadata_urls"] + + def _set_metadata_url(self, urls): + """ + Define metadata_url based upon api-metadata URL availability. + """ + + start_time = time.time() + avail_url, _ = url_helper.wait_for_url( + urls=urls, + max_wait=self.max_wait, + timeout=self.timeout, + connect_synchronously=False, + ) + if avail_url: + LOG.debug("%s is reachable", avail_url) + self.metadata_url = f"{avail_url}/conf?format=json" + self.userdata_url = f"{avail_url}/user_data/cloud-init" + self.vendordata_url = f"{avail_url}/vendor_data/cloud-init" + return + else: + LOG.debug( + "Unable to reach api-metadata at %s after %s seconds", + urls, + int(time.time() - start_time), + ) + raise ConnectionError def _crawl_metadata(self): resp = url_helper.readurl( - self.metadata_address, timeout=self.timeout, retries=self.retries + self.metadata_url, timeout=self.timeout, retries=self.retries ) self.metadata = json.loads(util.decode_binary(resp.contents)) self.userdata_raw = query_data_api( - "user-data", self.userdata_address, self.retries, self.timeout + "user-data", self.userdata_url, self.retries, self.timeout ) self.vendordata_raw = query_data_api( - "vendor-data", self.vendordata_address, self.retries, self.timeout + "vendor-data", self.vendordata_url, self.retries, self.timeout ) @staticmethod @@ -201,20 +242,91 @@ class DataSourceScaleway(sources.DataSource): if "scaleway" in cmdline: return True + def _set_urls_on_ip_version(self, proto, urls): + + if proto not in ["ipv4", "ipv6"]: + LOG.debug("Invalid IP version : %s", proto) + return [] + + filtered_urls = [] + for url in urls: + # Numeric IPs + address = urlparse(url).netloc + if address[0] == "[": + address = address[1:-1] + addr_proto = socket.getaddrinfo( + address, None, proto=socket.IPPROTO_TCP + )[0][0] + if addr_proto == socket.AF_INET and proto == "ipv4": + filtered_urls += [url] + continue + elif addr_proto == socket.AF_INET6 and proto == "ipv6": + filtered_urls += [url] + continue + + return filtered_urls + def _get_data(self): if self._fallback_interface is None: self._fallback_interface = net.find_fallback_nic() - try: - with EphemeralDHCPv4(self._fallback_interface): - util.log_time( - logfunc=LOG.debug, - msg="Crawl of metadata service", - func=self._crawl_metadata, - ) - except (NoDHCPLeaseError) as e: - util.logexc(LOG, str(e)) - return False + + # The DataSource uses EventType.BOOT so we are called more than once. + # Try to crawl metadata on IPv4 first and set has_ipv4 to False if we + # timeout so we do not try to crawl on IPv4 more than once. + if self.has_ipv4: + try: + # DHCPv4 waits for timeout defined in /etc/dhcp/dhclient.conf + # before giving up. Lower it in config file and try it first as + # it will only reach timeout on VMs with only IPv6 addresses. + with EphemeralDHCPv4( + self._fallback_interface, + ) as ipv4: + util.log_time( + logfunc=LOG.debug, + msg="Set api-metadata URL depending on " + "IPv4 availability", + func=self._set_metadata_url, + args=(self.metadata_urls,), + ) + util.log_time( + logfunc=LOG.debug, + msg="Crawl of metadata service", + func=self._crawl_metadata, + ) + self.ephemeral_fixed_address = ipv4["fixed-address"] + self.metadata["net_in_use"] = "ipv4" + except ( + NoDHCPLeaseError, + ConnectionError, + ProcessExecutionError, + ) as e: + util.logexc(LOG, str(e)) + # DHCPv4 timeout means that there is no DHCPv4 on the NIC. + # Flag it so we do not try to crawl on IPv4 again. + self.has_ipv4 = False + + # Only crawl metadata on IPv6 if it has not been done on IPv4 + if not self.has_ipv4: + try: + with EphemeralIPv6Network( + self._fallback_interface, + ): + util.log_time( + logfunc=LOG.debug, + msg="Set api-metadata URL depending on " + "IPv6 availability", + func=self._set_metadata_url, + args=(self.metadata_urls,), + ) + util.log_time( + logfunc=LOG.debug, + msg="Crawl of metadata service", + func=self._crawl_metadata, + ) + self.metadata["net_in_use"] = "ipv6" + except (ConnectionError): + return False return True @property @@ -236,25 +348,62 @@ class DataSourceScaleway(sources.DataSource): if self._fallback_interface is None: self._fallback_interface = net.find_fallback_nic() - netcfg = {"type": "physical", "name": "%s" % self._fallback_interface} - subnets = [{"type": "dhcp4"}] - if self.metadata["ipv6"]: - subnets += [ - { - "type": "static", - "address": "%s" % self.metadata["ipv6"]["address"], - "netmask": "%s" % self.metadata["ipv6"]["netmask"], - "routes": [ - { - "network": "::", - "prefix": "0", - "gateway": "%s" % self.metadata["ipv6"]["gateway"], - } - ], - } - ] - netcfg["subnets"] = subnets - self._network_config = {"version": 1, "config": [netcfg]} + if self.metadata["private_ip"] is None: + # New method of network configuration + + netcfg = {} + ip_cfg = {} + for ip in self.metadata["public_ips"]: + # Use DHCP for primary address + if ip["address"] == self.ephemeral_fixed_address: + ip_cfg["dhcp4"] = True + # Force addition of a route to the metadata API + ip_cfg["routes"] = [ + {"to": "169.254.42.42/32", "via": "62.210.0.1"} + ] + else: + if "addresses" in ip_cfg.keys(): + ip_cfg["addresses"] += ( + f'{ip["address"]}/{ip["netmask"]}', + ) + else: + ip_cfg["addresses"] = ( + f'{ip["address"]}/{ip["netmask"]}', + ) + if ip["family"] == "inet6": + route = {"via": ip["gateway"], "to": "::/0"} + if "routes" in ip_cfg.keys(): + ip_cfg["routes"] += [route] + else: + ip_cfg["routes"] = [route] + netcfg[self._fallback_interface] = ip_cfg + self._network_config = {"version": 2, "ethernets": netcfg} + else: + # Kept for backward compatibility + netcfg = { + "type": "physical", + "name": "%s" % self._fallback_interface, + } + subnets = [{"type": "dhcp4"}] + if self.metadata["ipv6"]: + subnets += [ + { + "type": "static", + "address": "%s" % self.metadata["ipv6"]["address"], + "netmask": "%s" % self.metadata["ipv6"]["netmask"], + "routes": [ + { + "network": "::", + "prefix": "0", + "gateway": "%s" + % self.metadata["ipv6"]["gateway"], + } + ], + } + ] + netcfg["subnets"] = subnets + self._network_config = {"version": 1, "config": [netcfg]} + LOG.debug("network_config : %s", self._network_config) return self._network_config @property @@ -272,7 +421,7 @@ class DataSourceScaleway(sources.DataSource): for tag in self.metadata.get("tags", []): if not tag.startswith(akeypre): continue - ssh_keys.append(tag[:plen].replace("_", " ")) + ssh_keys.append(tag[plen:].replace("_", " ")) return ssh_keys |