summaryrefslogtreecommitdiff
path: root/cloudinit
diff options
context:
space:
mode:
authorLouis Bouchard <bouchard.louis@gmail.com>2023-04-05 23:11:07 +0200
committerGitHub <noreply@github.com>2023-04-05 15:11:07 -0600
commite02c4607923af64311c70ae9a06d6794355e9a61 (patch)
tree5a237764222434efc9db0d1d0bc0c9c4725a448f /cloudinit
parent6d42aa8e2c1a5454a658ab4e2b9cead2677c77cd (diff)
downloadcloud-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.py245
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