← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~smoser/cloud-init:feature/ec2-ds-warn into cloud-init:master

 

Scott Moser has proposed merging ~smoser/cloud-init:feature/ec2-ds-warn into cloud-init:master.

Requested reviews:
  cloud init development team (cloud-init-dev)
Related bugs:
  Bug #1660385 in cloud-init: "Alert user of Ec2 Datasource on lookalike cloud"
  https://bugs.launchpad.net/cloud-init/+bug/1660385

For more details, see:
https://code.launchpad.net/~smoser/cloud-init/+git/cloud-init/+merge/316033
-- 
Your team cloud init development team is requested to review the proposed merge of ~smoser/cloud-init:feature/ec2-ds-warn into cloud-init:master.
diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py
index 2d00255..9debe94 100644
--- a/cloudinit/sources/DataSourceAliYun.py
+++ b/cloudinit/sources/DataSourceAliYun.py
@@ -22,6 +22,10 @@ class DataSourceAliYun(EC2.DataSourceEc2):
     def get_public_ssh_keys(self):
         return parse_public_keys(self.metadata.get('public-keys', {}))
 
+    @property
+    def cloud_platform(self):
+        return EC2.Platforms.ALIYUN
+
 
 def parse_public_keys(public_keys):
     keys = []
diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
index c657fd0..cbcb61c 100644
--- a/cloudinit/sources/DataSourceEc2.py
+++ b/cloudinit/sources/DataSourceEc2.py
@@ -22,6 +22,69 @@ LOG = logging.getLogger(__name__)
 # Which version we are requesting of the ec2 metadata apis
 DEF_MD_VERSION = '2009-04-04'
 
+LOOKALIKE_BEHAVIOR = 'skip'
+LOOKALIKE_WARNING = """\
+************************************************************************
+This system is using the EC2 Metadata Service, but does not appear to be
+running on Amazon EC2.  At a date yet to be determined, cloud-init will
+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.
+
+After you have filed a bug, you can disable this warning by launching an
+instance with the cloud-config below, or putting that content into
+/etc/cloud/cloud.cfg.d/99-ec2-lookalike.cfg.
+
+ #cloud-config
+ datasource:
+  Ec2:
+   look_alike:
+    behavior: accept
+
+************************************************************************
+"""
+
+
+class _PoorMansEnum(object):
+    """A thing with enum like behavior, here to avoid dependency on enum
+       which is not present in python 2.x standard library."""
+    def __init__(self, **kwargs):
+        for (k, v) in kwargs.items():
+            setattr(self, k, v)
+        self._keys = tuple(sorted([k for k in kwargs.keys()]))
+        self._values = tuple([kwargs[k] for k in self._keys])
+
+    def __contains__(self, k):
+        return k in self._values
+
+    def keys(self):
+        return self._keys
+
+    def values(self):
+        return self._values
+
+    def dict(self):
+        return dict(zip(self.keys(), self.values()))
+
+    def __str__(self):
+        return str(self.dict())
+
+
+Platforms = _PoorMansEnum(
+    ALIYUN="AliYun",
+    GENUINE_AWS="GenuineAWS",
+    UNKNOWN="Unknown",
+)
+
+LOOKALIKE_DEFAULT = {
+    'behavior': 'skip',
+    'platform': Platforms.UNKNOWN,
+    'sleep': 10,
+}
+
 
 class DataSourceEc2(sources.DataSource):
     # Default metadata urls that will be used if none are provided
@@ -34,8 +97,23 @@ class DataSourceEc2(sources.DataSource):
         self.metadata_address = None
         self.seed_dir = os.path.join(paths.seed_dir, "ec2")
         self.api_ver = DEF_MD_VERSION
+        self._cloud_platform = None
 
     def get_data(self):
+        if self.cloud_platform == Platforms.UNKNOWN:
+            lcfg = lookalike_cfg(self.ds_cfg)
+            msg = ("Did not identify host as a EC2 Metadata Platform.  "
+                   "behavior = %s: %s")
+            behavior = lcfg['behavior']
+            if behavior == "skip":
+                LOG.debug(msg, behavior, "Not fetching EC2 Metadata")
+                return
+            else:
+                LOG.debug(msg, behavior, "Will attempt to fetch metadata.")
+        else:
+            LOG.info("Identified EC2 Metadata Platform: %s",
+                     self.cloud_platform)
+
         seed_ret = {}
         if util.read_optional_seed(seed_ret, base=(self.seed_dir + "/")):
             self.userdata_raw = seed_ret['user-data']
@@ -190,6 +268,117 @@ class DataSourceEc2(sources.DataSource):
             return az[:-1]
         return None
 
+    @property
+    def cloud_platform(self):
+        if self._cloud_platform is None:
+            platform = lookalike_cfg(self.ds_cfg)['platform']
+            if platform is Platforms.UNKNOWN:
+                self._cloud_platform = identify_platform()
+            else:
+                self._cloud_platform = platform
+        return self._cloud_platform
+
+    def activate(self, cfg, is_new_instance):
+        if is_new_instance:
+            lbehave, lsleep = lookalike_cfg(cfg)
+            apply_lookalike_settings(lbehave, lsleep)
+        return
+
+
+def lookalike_cfg(cfg):
+    # return a copy of the default lookalike config updated with
+    # values from cfg['look_alike']
+    result = LOOKALIKE_DEFAULT.copy()
+    if cfg is None:
+        return result
+    if not isinstance(cfg, dict):
+        raise ValueError("lookalike_cfg expected dict, found: %s", cfg)
+
+    lcfg = cfg.get('look_alike')
+    if lcfg is None:
+        return result
+    if not isinstance(cfg, dict):
+        raise ValueError("'look_alike' in input was not a dict: %s" % lcfg)
+
+    msg = 'Bad value {0} for look_alike/{1}. Using default: {2}'
+
+    def check_or_set_default(key, val, allowed):
+        if val in allowed:
+            result[key] = val
+        else:
+            LOG.warn(msg.format(val, key, result[key]))
+
+    check_or_set_default('behavior', lcfg.get('behavior'),
+                         ('warn', 'skip', 'accept'))
+    check_or_set_default('platform', lcfg.get('platform'), Platforms)
+
+    if 'sleep' in lcfg:
+        sval = cfg.get('sleep')
+        try:
+            result['sleep'] = int(sval, result['sleep'])
+        except:
+            LOG.warn(msg.format(sval, 'sleep', result['sleep']))
+
+    return result['behavior'], result['sleep'], result['platform']
+
+
+def apply_lookalike_settings(behavior, sleep):
+    if behavior == 'warn':
+        LOG.warn(LOOKALIKE_WARNING)
+        if sleep:
+            time.sleep(sleep)
+    return
+
+
+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.GENUINE_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'
+    data = {}
+    try:
+        uuid = util.load_file("/sys/hypervisor/uuid").strip()
+        data['uuid_source'] = 'hypervisor'
+    except:
+        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 = [
diff --git a/doc/rtd/topics/datasources/ec2.rst b/doc/rtd/topics/datasources/ec2.rst
index 4810c98..b8f4119 100644
--- a/doc/rtd/topics/datasources/ec2.rst
+++ b/doc/rtd/topics/datasources/ec2.rst
@@ -8,6 +8,8 @@ provided to the instance by the cloud provider. Typically this ip is
 instance so that the instance can make calls to get instance userdata and
 instance metadata.
 
+It supports reading from the `EC2 Instance Metadata`_ service.
+
 Metadata is accessible via the following URL:
 
 ::
@@ -58,4 +60,34 @@ To see which versions are supported from your cloud provider use the following U
     ...
     latest
 
+Configuration
+-------------
+Configuration for the datasource can be done from system config.
+The default values are shown as example below.
+
+.. code:: yaml
+
+  datasource:
+   Ec2:
+     # timeout: the timeout value for a request to the metadata service
+     timeout : 50
+     # The length in seconds to wait before giving up on the metadata
+     # service.  The actual total wait could be up to
+     #   len(resolvable_metadata_urls)*timeout
+     max_wait : 120
+     metadata_urls:
+      - http://169.254.169.254:80
+      - http://instance-data:8773
+
+     # 'look_alike' settings control what behavior is taken if this datasource
+     # is used but the system does not appear to be on EC2.  See bug 1660385
+     # for more information: http://bugs.launchpad.net/bugs/1660385
+     # behavior can be 'warn', 'accept', 'skip'
+     # sleep: during first instance boot, sleep N seconds.
+     look_alike:
+      - behavior: warn
+      - sleep: 10
+
+
+.. _EC2 Instance Metadata: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
 .. vi: textwidth=78

Follow ups