From 9bb55c6c45bcc5e310cf7e4d42cad53759dcca15 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 23 Feb 2017 17:15:27 -0500 Subject: DatasourceEc2: add warning message when not on AWS. Based on the setting Datasource/Ec2/strict_id, the datasource will now warn once per instance. --- cloudinit/sources/DataSourceEc2.py | 178 ++++++++++++++++++++++++++++++++++++- 1 file changed, 176 insertions(+), 2 deletions(-) (limited to 'cloudinit/sources/DataSourceEc2.py') diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py index c657fd09..26da263a 100644 --- a/cloudinit/sources/DataSourceEc2.py +++ b/cloudinit/sources/DataSourceEc2.py @@ -9,6 +9,7 @@ # This file is part of cloud-init. See LICENSE file for license information. import os +import textwrap import time from cloudinit import ec2_utils as ec2 @@ -22,12 +23,23 @@ LOG = logging.getLogger(__name__) # Which version we are requesting of the ec2 metadata apis DEF_MD_VERSION = '2009-04-04' +STRICT_ID_PATH = ("datasource", "Ec2", "strict_id") +STRICT_ID_DEFAULT = "warn" + + +class Platforms(object): + ALIYUN = "AliYun" + AWS = "AWS" + SEEDED = "Seeded" + UNKNOWN = "Unknown" + 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"] + _cloud_platform = None def __init__(self, sys_cfg, distro, paths): sources.DataSource.__init__(self, sys_cfg, distro, paths) @@ -41,8 +53,18 @@ class DataSourceEc2(sources.DataSource): self.userdata_raw = seed_ret['user-data'] self.metadata = seed_ret['meta-data'] LOG.debug("Using seeded ec2 data from %s", self.seed_dir) + self._cloud_platform = Platforms.SEEDED return True + strict_mode, _sleep = read_strict_mode( + util.get_cfg_by_path(self.sys_cfg, STRICT_ID_PATH, + STRICT_ID_DEFAULT), ("warn", None)) + + LOG.debug("strict_mode: %s, cloud_platform=%s", + strict_mode, self.cloud_platform) + if strict_mode == "true" and self.cloud_platform == Platforms.UNKNOWN: + return False + try: if not self.wait_for_metadata_service(): return False @@ -51,8 +73,8 @@ class DataSourceEc2(sources.DataSource): 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 %s seconds", - int(time.time() - start_time)) + 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", @@ -190,6 +212,158 @@ class DataSourceEc2(sources.DataSource): return az[:-1] return None + @property + def cloud_platform(self): + if self._cloud_platform is None: + self._cloud_platform = identify_platform() + return self._cloud_platform + + def activate(self, cfg, is_new_instance): + if not is_new_instance: + return + if self.cloud_platform == Platforms.UNKNOWN: + warn_if_necessary( + util.get_cfg_by_path(cfg, STRICT_ID_PATH, STRICT_ID_DEFAULT)) + + +def read_strict_mode(cfgval, default): + try: + return parse_strict_mode(cfgval) + except ValueError as e: + LOG.warn(e) + return default + + +def parse_strict_mode(cfgval): + # given a mode like: + # true, false, warn,[sleep] + # return tuple with string mode (true|false|warn) and sleep. + if cfgval is True: + return 'true', None + if cfgval is False: + return 'false', None + + if not cfgval: + return 'warn', 0 + + mode, _, sleep = cfgval.partition(",") + if mode not in ('true', 'false', 'warn'): + raise ValueError( + "Invalid mode '%s' in strict_id setting '%s': " + "Expected one of 'true', 'false', 'warn'." % (mode, cfgval)) + + if sleep: + try: + sleep = int(sleep) + except ValueError: + raise ValueError("Invalid sleep '%s' in strict_id setting '%s': " + "not an integer" % (sleep, cfgval)) + else: + sleep = None + + return mode, sleep + + +def warn_if_necessary(cfgval): + try: + mode, sleep = parse_strict_mode(cfgval) + except ValueError as e: + LOG.warn(e) + return + + if mode == "false": + return + + show_warning(sleep) + + +def show_warning(sleep): + message = textwrap.dedent(""" + **************************************************************** + # This system is using the EC2 Metadata Service, but does not # + # appear to be running on Amazon EC2 or one of cloud-init's # + # known platforms that provide a EC2 Metadata service. In the # + # future, cloud-init may stop reading metadata from the EC2 # + # Metadata Service unless the platform can be identified # + # # + # If you are seeing this message, please file a bug against # + # cloud-init at https://bugs.launchpad.net/cloud-init/+filebug # + # Make sure to include the cloud provider your instance is # + # running on. # + # # + # For more information see # + # https://bugs.launchpad.net/cloud-init/+bug/1660385 # + # # + # After you have filed a bug, you can disable this warning by # + # launching your instance with the cloud-config below, or # + # putting that content into # + # /etc/cloud/cloud.cfg.d/99-ec2-datasource.cfg # + # # + # #cloud-config # + # datasource: # + # Ec2: # + # strict_id: false # + # # + """) + closemsg = "" + if sleep: + closemsg = " [sleeping for %d seconds] " % sleep + message += closemsg.center(64, "*") + print(message) + LOG.warn(message) + if sleep: + time.sleep(sleep) + + +def identify_aws(data): + # data is a dictionary returned by _collect_platform_data. + if (data['uuid'].startswith('ec2') and + (data['uuid_source'] == 'hypervisor' or + data['uuid'] == data['serial'])): + return Platforms.AWS + + return None + + +def identify_platform(): + # identify the platform and return an entry in Platforms. + data = _collect_platform_data() + checks = (identify_aws, lambda x: Platforms.UNKNOWN) + for checker in checks: + try: + result = checker(data) + if result: + return result + except Exception as e: + LOG.warn("calling %s with %s raised exception: %s", + checker, data, e) + + +def _collect_platform_data(): + # returns a dictionary with all lower case values: + # uuid: system-uuid from dmi or /sys/hypervisor + # uuid_source: 'hypervisor' (/sys/hypervisor/uuid) or 'dmi' + # serial: dmi 'system-serial-number' (/sys/.../product_serial) + data = {} + try: + uuid = util.load_file("/sys/hypervisor/uuid").strip() + data['uuid_source'] = 'hypervisor' + except Exception: + uuid = util.read_dmi_data('system-uuid') + data['uuid_source'] = 'dmi' + + if uuid is None: + uuid = '' + data['uuid'] = uuid.lower() + + serial = util.read_dmi_data('system-serial-number') + if serial is None: + serial = '' + + data['serial'] = serial.lower() + + return data + # Used to match classes to dependencies datasources = [ -- cgit v1.2.3 From 5dd5b2cb539a84ed59f2b3181020d2bd18989718 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Fri, 24 Feb 2017 14:19:20 -0500 Subject: Identify Brightbox as an Ec2 datasource user. Brightbox will identify their platform to the guest by setting the product serial to a string that ends with 'brightbox.com'. LP: #1661693 --- cloudinit/sources/DataSourceEc2.py | 8 +++++++- tools/ds-identify | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) (limited to 'cloudinit/sources/DataSourceEc2.py') diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py index 26da263a..c7df8060 100644 --- a/cloudinit/sources/DataSourceEc2.py +++ b/cloudinit/sources/DataSourceEc2.py @@ -30,6 +30,7 @@ STRICT_ID_DEFAULT = "warn" class Platforms(object): ALIYUN = "AliYun" AWS = "AWS" + BRIGHTBOX = "Brightbox" SEEDED = "Seeded" UNKNOWN = "Unknown" @@ -325,10 +326,15 @@ def identify_aws(data): return None +def identify_brightbox(data): + if data['serial'].endswith('brightbox.com'): + return Platforms.BRIGHTBOX + + def identify_platform(): # identify the platform and return an entry in Platforms. data = _collect_platform_data() - checks = (identify_aws, lambda x: Platforms.UNKNOWN) + checks = (identify_aws, identify_brightbox, lambda x: Platforms.UNKNOWN) for checker in checks: try: result = checker(data) diff --git a/tools/ds-identify b/tools/ds-identify index dfa856ff..c39956fc 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -639,6 +639,11 @@ ec2_identify_platform() { local default="$1" local serial="${DI_DMI_PRODUCT_SERIAL}" + # brightbox https://bugs.launchpad.net/cloud-init/+bug/1661693 + case "$serial" in + *brightbox.com) _RET="Brightbox"; return 0;; + esac + # AWS http://docs.aws.amazon.com/AWSEC2/ # latest/UserGuide/identify_ec2_instances.html local uuid="" hvuuid="$PATH_ROOT/sys/hypervisor/uuid" -- cgit v1.2.3 From ade8c2e0266b020089145075e8236b95c000a3cb Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 28 Feb 2017 17:14:45 -0500 Subject: Move warning functionality to cloudinit/warnings.py This moves the warning code that was added specifically for EC2 into a generic path at cloudinit/warnings.py. It also adds support for writing warning files into the warnings directory to be shown by Z99-cloudinit-warnings.sh. --- cloudinit/helpers.py | 1 + cloudinit/sources/DataSourceEc2.py | 47 ++------------- cloudinit/warnings.py | 115 +++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 42 deletions(-) create mode 100644 cloudinit/warnings.py (limited to 'cloudinit/sources/DataSourceEc2.py') diff --git a/cloudinit/helpers.py b/cloudinit/helpers.py index 38f5f899..7435d58d 100644 --- a/cloudinit/helpers.py +++ b/cloudinit/helpers.py @@ -340,6 +340,7 @@ class Paths(object): "vendordata": "vendor-data.txt.i", "instance_id": ".instance-id", "manual_clean_marker": "manual-clean", + "warnings": "warnings", } # Set when a datasource becomes active self.datasource = ds diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py index c7df8060..6f01a139 100644 --- a/cloudinit/sources/DataSourceEc2.py +++ b/cloudinit/sources/DataSourceEc2.py @@ -9,7 +9,6 @@ # This file is part of cloud-init. See LICENSE file for license information. import os -import textwrap import time from cloudinit import ec2_utils as ec2 @@ -17,6 +16,7 @@ from cloudinit import log as logging from cloudinit import sources from cloudinit import url_helper as uhelp from cloudinit import util +from cloudinit import warnings LOG = logging.getLogger(__name__) @@ -224,7 +224,8 @@ class DataSourceEc2(sources.DataSource): return if self.cloud_platform == Platforms.UNKNOWN: warn_if_necessary( - util.get_cfg_by_path(cfg, STRICT_ID_PATH, STRICT_ID_DEFAULT)) + util.get_cfg_by_path(cfg, STRICT_ID_PATH, STRICT_ID_DEFAULT), + cfg) def read_strict_mode(cfgval, default): @@ -265,7 +266,7 @@ def parse_strict_mode(cfgval): return mode, sleep -def warn_if_necessary(cfgval): +def warn_if_necessary(cfgval, cfg): try: mode, sleep = parse_strict_mode(cfgval) except ValueError as e: @@ -275,45 +276,7 @@ def warn_if_necessary(cfgval): if mode == "false": return - show_warning(sleep) - - -def show_warning(sleep): - message = textwrap.dedent(""" - **************************************************************** - # This system is using the EC2 Metadata Service, but does not # - # appear to be running on Amazon EC2 or one of cloud-init's # - # known platforms that provide a EC2 Metadata service. In the # - # future, cloud-init may stop reading metadata from the EC2 # - # Metadata Service unless the platform can be identified # - # # - # If you are seeing this message, please file a bug against # - # cloud-init at https://bugs.launchpad.net/cloud-init/+filebug # - # Make sure to include the cloud provider your instance is # - # running on. # - # # - # For more information see # - # https://bugs.launchpad.net/cloud-init/+bug/1660385 # - # # - # After you have filed a bug, you can disable this warning by # - # launching your instance with the cloud-config below, or # - # putting that content into # - # /etc/cloud/cloud.cfg.d/99-ec2-datasource.cfg # - # # - # #cloud-config # - # datasource: # - # Ec2: # - # strict_id: false # - # # - """) - closemsg = "" - if sleep: - closemsg = " [sleeping for %d seconds] " % sleep - message += closemsg.center(64, "*") - print(message) - LOG.warn(message) - if sleep: - time.sleep(sleep) + warnings.show_warning('non_ec2_md', cfg, mode=True, sleep=sleep) def identify_aws(data): diff --git a/cloudinit/warnings.py b/cloudinit/warnings.py new file mode 100644 index 00000000..77c092f9 --- /dev/null +++ b/cloudinit/warnings.py @@ -0,0 +1,115 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit import helpers +from cloudinit import log as logging +from cloudinit import util + +import os +import time + +LOG = logging.getLogger() + +WARNINGS = { + 'non_ec2_md': """ +This system is using the EC2 Metadata Service, but does not appear to +be running on Amazon EC2 or one of cloud-init's known platforms that +provide a EC2 Metadata service. In the future, cloud-init may stop +reading metadata from the EC2 Metadata Service unless the platform can +be identified. + +If you are seeing this message, please file a bug against +cloud-init at + https://bugs.launchpad.net/cloud-init/+filebug?field.tags=dsid +Make sure to include the cloud provider your instance is +running on. + +For more information see + https://bugs.launchpad.net/bugs/1660385 + +After you have filed a bug, you can disable this warning by +launching your instance with the cloud-config below, or +putting that content into + /etc/cloud/cloud.cfg.d/99-ec2-datasource.cfg + +#cloud-config +datasource: + Ec2: + strict_id: false""", +} + + +def _get_warn_dir(cfg): + paths = helpers.Paths( + path_cfgs=cfg.get('system_info', {}).get('paths', {})) + return paths.get_ipath_cur('warnings') + + +def _load_warn_cfg(cfg, name, mode=True, sleep=None): + # parse cfg['warnings']['name'] returning boolean, sleep + # expected value is form of: + # (on|off|true|false|sleep)[,sleeptime] + # boolean True == on, False == off + default = (mode, sleep) + if not cfg or not isinstance(cfg, dict): + return default + + ncfg = util.get_cfg_by_path(cfg, ('warnings', name)) + if ncfg is None: + return default + + if ncfg in ("on", "true", True): + return True, None + + if ncfg in ("off", "false", False): + return False, None + + mode, _, csleep = ncfg.partition(",") + if mode != "sleep": + return default + + if csleep: + try: + sleep = int(csleep) + except ValueError: + return default + + return True, sleep + + +def show_warning(name, cfg=None, sleep=None, mode=True, **kwargs): + # kwargs are used for .format of the message. + # sleep and mode are default values used if + # cfg['warnings']['name'] is not present. + if cfg is None: + cfg = {} + + mode, sleep = _load_warn_cfg(cfg, name, mode=mode, sleep=sleep) + if not mode: + return + + msg = WARNINGS[name].format(**kwargs) + msgwidth = 70 + linewidth = msgwidth + 4 + + fmt = "# %%-%ds #" % msgwidth + topline = "*" * linewidth + "\n" + fmtlines = [] + for line in msg.strip("\n").splitlines(): + fmtlines.append(fmt % line) + + closeline = topline + if sleep: + sleepmsg = " [sleeping for %d seconds] " % sleep + closeline = sleepmsg.center(linewidth, "*") + "\n" + + util.write_file( + os.path.join(_get_warn_dir(cfg), name), + topline + "\n".join(fmtlines) + "\n" + topline) + + LOG.warn(topline + "\n".join(fmtlines) + "\n" + closeline) + + if sleep: + LOG.debug("sleeping %d seconds for warning '%s'" % (sleep, name)) + time.sleep(sleep) + +# vi: ts=4 expandtab -- cgit v1.2.3