diff options
author | Chad Smith <chad.smith@canonical.com> | 2017-08-09 21:55:52 -0600 |
---|---|---|
committer | Chad Smith <chad.smith@canonical.com> | 2017-08-09 21:55:52 -0600 |
commit | d5f855dd96ccbea77f61b0515b574ad2c43d116d (patch) | |
tree | 5905412f51134f4055af997068005747826fd3ac /cloudinit/sources | |
parent | 5bba5db2655d88b8aba8fa06b30f8e91e2ca6836 (diff) | |
download | vyos-cloud-init-d5f855dd96ccbea77f61b0515b574ad2c43d116d.tar.gz vyos-cloud-init-d5f855dd96ccbea77f61b0515b574ad2c43d116d.zip |
ec2: Allow Ec2 to run in init-local using dhclient in a sandbox.
This branch is a prerequisite for IPv6 support in AWS by allowing Ec2
datasource to query the metadata source version 2016-09-02 about whether
or not it needs to configure IPv6 on interfaces. If version 2016-09-02
is not present, fallback to the min_metadata_version of 2009-04-04. The
DataSourceEc2Local not run on FreeBSD because dhclient in doesn't
support the -sf flag allowing us to run dhclient without filesystem
side-effects.
To query AWS' metadata address @ 169.254.169.254, the instance must have
a dhcp-allocated address configured. Configuring IPv4 link-local
addresses result in timeouts from the metadata service. We introduced a
DataSourceEc2Local subclass which will perform a sandboxed dhclient
discovery which obtains an authorized IP address on eth0 and crawl
metadata about full instance network configuration.
Since ec2 IPv6 metadata is not sufficient in itself to tell us all the
ipv6 knownledge we need, it only be used as a boolean to tell us which
nics need IPv6. Cloud-init will then configure desired interfaces to
DHCPv6 versus DHCPv4.
Performance side note: Shifting the dhcp work into init-local for Ec2
actually gets us 1 second faster deployments by skipping init-network
phase of alternate datasource checks because Ec2Local is configured in
an ealier boot stage. In 3 test runs prior to this change: cloud-init
runs were 5.5 seconds, with the change we now average 4.6 seconds.
This efficiency could be even further improved if we avoiding dhcp
discovery in order to talk to the metadata service from an AWS
authorized dhcp address if there were some way to advertize the dhcp
configuration via DMI/SMBIOS or system environment variables.
Inspecting time costs of the dhclient setup/teardown in 3 live runs the
time cost for the dhcp setup round trip on AWS is:
test 1: 76 milliseconds
dhcp discovery + metadata: 0.347 seconds
metadata alone: 0.271 seconds
test 2: 88 milliseconds
dhcp discovery + metadata: 0.388 seconds
metadata alone: 0.300 seconds
test 3: 75 milliseconds
dhcp discovery + metadata: 0.366 seconds
metadata alone: 0.291 seconds
LP: #1709772
Diffstat (limited to 'cloudinit/sources')
-rw-r--r-- | cloudinit/sources/DataSourceAliYun.py | 9 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceEc2.py | 121 |
2 files changed, 105 insertions, 25 deletions
diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py index 380e27cb..43a7e42c 100644 --- a/cloudinit/sources/DataSourceAliYun.py +++ b/cloudinit/sources/DataSourceAliYun.py @@ -6,17 +6,20 @@ from cloudinit import sources from cloudinit.sources import DataSourceEc2 as EC2 from cloudinit import util -DEF_MD_VERSION = "2016-01-01" ALIYUN_PRODUCT = "Alibaba Cloud ECS" class DataSourceAliYun(EC2.DataSourceEc2): - metadata_urls = ["http://100.100.100.200"] + + metadata_urls = ['http://100.100.100.200'] + + # The minimum supported metadata_version from the ec2 metadata apis + min_metadata_version = '2016-01-01' + extended_metadata_versions = [] def __init__(self, sys_cfg, distro, paths): super(DataSourceAliYun, self).__init__(sys_cfg, distro, paths) self.seed_dir = os.path.join(paths.seed_dir, "AliYun") - self.api_ver = DEF_MD_VERSION def get_hostname(self, fqdn=False, _resolve_ip=False): return self.metadata.get('hostname', 'localhost.localdomain') diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py index 4ec9592f..8e5f8ee4 100644 --- a/cloudinit/sources/DataSourceEc2.py +++ b/cloudinit/sources/DataSourceEc2.py @@ -13,6 +13,8 @@ import time from cloudinit import ec2_utils as ec2 from cloudinit import log as logging +from cloudinit import net +from cloudinit.net import dhcp from cloudinit import sources from cloudinit import url_helper as uhelp from cloudinit import util @@ -20,8 +22,7 @@ from cloudinit import warnings LOG = logging.getLogger(__name__) -# Which version we are requesting of the ec2 metadata apis -DEF_MD_VERSION = '2009-04-04' +SKIP_METADATA_URL_CODES = frozenset([uhelp.NOT_FOUND]) STRICT_ID_PATH = ("datasource", "Ec2", "strict_id") STRICT_ID_DEFAULT = "warn" @@ -41,17 +42,28 @@ class Platforms(object): class DataSourceEc2(sources.DataSource): + # Default metadata urls that will be used if none are provided # They will be checked for 'resolveability' and some of the # following may be discarded if they do not resolve metadata_urls = ["http://169.254.169.254", "http://instance-data.:8773"] + + # The minimum supported metadata_version from the ec2 metadata apis + min_metadata_version = '2009-04-04' + + # Priority ordered list of additional metadata versions which will be tried + # for extended metadata content. IPv6 support comes in 2016-09-02 + extended_metadata_versions = ['2016-09-02'] + _cloud_platform = None + # Whether we want to get network configuration from the metadata service. + get_network_metadata = False + def __init__(self, sys_cfg, distro, paths): sources.DataSource.__init__(self, sys_cfg, distro, paths) self.metadata_address = None self.seed_dir = os.path.join(paths.seed_dir, "ec2") - self.api_ver = DEF_MD_VERSION def get_data(self): seed_ret = {} @@ -73,21 +85,27 @@ class DataSourceEc2(sources.DataSource): elif self.cloud_platform == Platforms.NO_EC2_METADATA: return False - try: - if not self.wait_for_metadata_service(): + if self.get_network_metadata: # Setup networking in init-local stage. + if util.is_FreeBSD(): + LOG.debug("FreeBSD doesn't support running dhclient with -sf") return False - start_time = time.time() - self.userdata_raw = \ - ec2.get_instance_userdata(self.api_ver, self.metadata_address) - self.metadata = ec2.get_instance_metadata(self.api_ver, - self.metadata_address) - LOG.debug("Crawl of metadata service took %.3f seconds", - time.time() - start_time) - return True - except Exception: - util.logexc(LOG, "Failed reading from metadata address %s", - self.metadata_address) - return False + dhcp_leases = dhcp.maybe_perform_dhcp_discovery() + if not dhcp_leases: + # DataSourceEc2Local failed in init-local stage. DataSourceEc2 + # will still run in init-network stage. + return False + dhcp_opts = dhcp_leases[-1] + net_params = {'interface': dhcp_opts.get('interface'), + 'ip': dhcp_opts.get('fixed-address'), + 'prefix_or_mask': dhcp_opts.get('subnet-mask'), + 'broadcast': dhcp_opts.get('broadcast-address'), + 'router': dhcp_opts.get('routers')} + with net.EphemeralIPv4Network(**net_params): + return util.log_time( + logfunc=LOG.debug, msg='Crawl of metadata service', + func=self._crawl_metadata) + else: + return self._crawl_metadata() @property def launch_index(self): @@ -95,6 +113,32 @@ class DataSourceEc2(sources.DataSource): return None return self.metadata.get('ami-launch-index') + def get_metadata_api_version(self): + """Get the best supported api version from the metadata service. + + Loop through all extended support metadata versions in order and + return the most-fully featured metadata api version discovered. + + If extended_metadata_versions aren't present, return the datasource's + min_metadata_version. + """ + # Assumes metadata service is already up + for api_ver in self.extended_metadata_versions: + url = '{0}/{1}/meta-data/instance-id'.format( + self.metadata_address, api_ver) + try: + resp = uhelp.readurl(url=url) + except uhelp.UrlError as e: + LOG.debug('url %s raised exception %s', url, e) + else: + if resp.code == 200: + LOG.debug('Found preferred metadata version %s', api_ver) + return api_ver + elif resp.code == 404: + msg = 'Metadata api version %s not present. Headers: %s' + LOG.debug(msg, api_ver, resp.headers) + return self.min_metadata_version + def get_instance_id(self): return self.metadata['instance-id'] @@ -138,21 +182,22 @@ class DataSourceEc2(sources.DataSource): urls = [] url2base = {} for url in mdurls: - cur = "%s/%s/meta-data/instance-id" % (url, self.api_ver) + cur = '{0}/{1}/meta-data/instance-id'.format( + url, self.min_metadata_version) urls.append(cur) url2base[cur] = url start_time = time.time() - url = uhelp.wait_for_url(urls=urls, max_wait=max_wait, - timeout=timeout, status_cb=LOG.warn) + url = uhelp.wait_for_url( + urls=urls, max_wait=max_wait, timeout=timeout, status_cb=LOG.warn) if url: - LOG.debug("Using metadata source: '%s'", url2base[url]) + self.metadata_address = url2base[url] + LOG.debug("Using metadata source: '%s'", self.metadata_address) else: LOG.critical("Giving up on md from %s after %s seconds", urls, int(time.time() - start_time)) - self.metadata_address = url2base.get(url) return bool(url) def device_name_to_device(self, name): @@ -234,6 +279,37 @@ class DataSourceEc2(sources.DataSource): util.get_cfg_by_path(cfg, STRICT_ID_PATH, STRICT_ID_DEFAULT), cfg) + def _crawl_metadata(self): + """Crawl metadata service when available. + + @returns: True on success, False otherwise. + """ + if not self.wait_for_metadata_service(): + return False + api_version = self.get_metadata_api_version() + try: + self.userdata_raw = ec2.get_instance_userdata( + api_version, self.metadata_address) + self.metadata = ec2.get_instance_metadata( + api_version, self.metadata_address) + except Exception: + util.logexc( + LOG, "Failed reading from metadata address %s", + self.metadata_address) + return False + return True + + +class DataSourceEc2Local(DataSourceEc2): + """Datasource run at init-local which sets up network to query metadata. + + In init-local, no network is available. This subclass sets up minimal + networking with dhclient on a viable nic so that it can talk to the + metadata service. If the metadata service provides network configuration + then render the network configuration for that instance based on metadata. + """ + get_network_metadata = True # Get metadata network config if present + def read_strict_mode(cfgval, default): try: @@ -349,6 +425,7 @@ def _collect_platform_data(): # Used to match classes to dependencies datasources = [ + (DataSourceEc2Local, (sources.DEP_FILESYSTEM,)), # Run at init-local (DataSourceEc2, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), ] |