← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~kaihuan-pkh/cloud-init:aliyun-datasource into cloud-init:master

 

lawrence peng has proposed merging ~kaihuan-pkh/cloud-init:aliyun-datasource into cloud-init:master.

Requested reviews:
  cloud init development team (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~kaihuan-pkh/cloud-init/+git/cloud-init/+merge/308483

add the datasource support for Ali-Cloud ECS, the elastic computing service of Alibaba Group Cloud Platform.
-- 
Your team cloud init development team is requested to review the proposed merge of ~kaihuan-pkh/cloud-init:aliyun-datasource into cloud-init:master.
diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py
new file mode 100644
index 0000000..bba0eec
--- /dev/null
+++ b/cloudinit/sources/DataSourceAliYun.py
@@ -0,0 +1,121 @@
+# vi: ts=4 expandtab
+
+import os
+import time
+
+from cloudinit import log as logging
+from cloudinit import sources
+from cloudinit import url_helper as uhelp
+from cloudinit import util
+from cloudinit.sources import DataSourceEc2 as EC2
+
+LOG = logging.getLogger(__name__)
+
+DEF_MD_URL = "http://100.100.100.200";
+
+DEF_MD_VERSION = "latest"
+
+# 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
+DEF_MD_URLS = [DEF_MD_URL, ]
+
+
+class DataSourceAliYun(EC2.DataSourceEc2):
+    def __init__(self, sys_cfg, distro, paths):
+        super(DataSourceAliYun, self).__init__(sys_cfg, distro, paths)
+        self.metadata_address = DEF_MD_URL
+        self.seed_dir = os.path.join(paths.seed_dir, "AliYun")
+        self.api_ver = DEF_MD_VERSION
+
+    def wait_for_metadata_service(self):
+        mcfg = self.ds_cfg
+        if not mcfg:
+            mcfg = {}
+
+        (max_wait, timeout) = self._get_url_settings()
+        if max_wait <= 0:
+            return False
+
+        # Remove addresses from the list that wont resolve.
+        mdurls = mcfg.get("metadata_urls", DEF_MD_URLS)
+        filtered = [x for x in mdurls if util.is_resolvable_url(x)]
+
+        if set(filtered) != set(mdurls):
+            LOG.debug("Removed the following from metadata urls: %s",
+                      list((set(mdurls) - set(filtered))))
+
+        if len(filtered):
+            mdurls = filtered
+        else:
+            LOG.warn("Empty metadata url list! using default list")
+            mdurls = DEF_MD_URLS
+
+        urls = []
+        url2base = {}
+        for url in mdurls:
+            cur = "%s/%s/meta-data/instance-id" % (url, self.api_ver)
+            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)
+
+        if url:
+            LOG.debug("Using metadata source: '%s'", url2base[url])
+        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 get_hostname(self, fqdn=False, _resolve_ip=False):
+        if not self.metadata:
+            return 'localhost.localdomain'
+        return self.metadata.get('hostname', 'localhost.localdomain')
+
+    def get_public_ssh_keys(self):
+        if not self.metadata:
+            return []
+        return self.parse_public_keys(self.metadata.get('public-keys', {}))
+
+    def parse_public_keys(self, public_keys):
+        keys = []
+        for key_id, key_body in public_keys.items():
+            if isinstance(key_body, str):
+                keys.append(key_body.strip())
+            elif isinstance(key_body, list):
+                keys.extend(key_body)
+            elif isinstance(key_body, dict):
+                key = key_body.get('openssh-key', [])
+                if isinstance(key, str):
+                    keys.append(key.strip())
+                elif isinstance(key, list):
+                    keys.extend(key)
+        return keys
+
+    def get_ntp_conf(self):
+        if not self.metadata:
+            return {}
+        return self.metadata.get('ntp-conf')
+
+    def get_source_address(self):
+        if not self.metadata or not self.metadata.get('source-address'):
+            return []
+        source_address = self.metadata.get('source-address')
+        if isinstance(source_address, str):
+            source_address = [source_address]
+        return source_address
+
+
+# Used to match classes to dependencies
+datasources = [
+  (DataSourceAliYun, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
+]
+
+
+# Return a list of data sources that match this set of dependencies
+def get_datasource_list(depends):
+    return sources.list_from_depends(depends, datasources)
diff --git a/tests/unittests/test_datasource/test_aliyun.py b/tests/unittests/test_datasource/test_aliyun.py
new file mode 100644
index 0000000..e1d7db8
--- /dev/null
+++ b/tests/unittests/test_datasource/test_aliyun.py
@@ -0,0 +1,143 @@
+import os
+import json
+import requests
+import httpretty
+import functools
+import hashlib
+
+from .. import helpers as test_helpers
+from cloudinit import helpers
+from cloudinit.sources import DataSourceAliYun as ay
+
+DEFAULT_METADATA = {
+    'instance-id': 'aliyun-test-vm-00',
+    'eipv4': '10.0.0.1',
+    'hostname': 'test-hostname',
+    'image-id': 'm-test',
+    'launch-index': '0',
+    'mac': '00:16:3e:00:00:00',
+    'network-type': 'vpc',
+    'private-ipv4': '192.168.0.1',
+    'serial-number': 'test-string',
+    'vpc-cidr-block': '192.168.0.0/16',
+    'vpc-id': 'test-vpc',
+    'vswitch-id': 'test-vpc',
+    'vswitch-cidr-block': '192.168.0.0/16',
+    'zone-id': 'test-zone-1',
+    'ntp-conf': {'ntp_servers': [
+                 'ntp1.aliyun.com',
+                 'ntp2.aliyun.com',
+                 'ntp3.aliyun.com']},
+    'source-address': ['http://mirrors.aliyun.com',
+                       'http://mirrors.aliyuncs.com'],
+    'public-keys': {'key-pair-1': {'openssh-key': 'ssh-rsa AAAAB3...'},
+                    'key-pair-2': {'openssh-key': 'ssh-rsa AAAAB3...'}}
+}
+
+DEFAULT_USERDATA = """\
+#cloud-config
+
+hostname: localhost"""
+
+
+def register_mock_metaserver(base_url, data):
+    def register_helper(register, base_url, body):
+        if isinstance(body, str):
+            register(base_url, body)
+        elif isinstance(body, list):
+            register(base_url.rstrip('/'), '\n'.join(body)+'\n')
+        elif isinstance(body, dict):
+            vals = []
+            for k, v in body.items():
+                if isinstance(v, (str, list)):
+                    suffix = k.rstrip('/')
+                else:
+                    suffix = k.rstrip('/')+'/'
+                vals.append(suffix)
+                url = base_url.rstrip('/') + '/' + suffix
+                register_helper(register, url, v)
+            register(base_url, '\n'.join(vals)+'\n')
+
+    register = functools.partial(httpretty.register_uri, httpretty.GET)
+    register_helper(register, base_url, data)
+
+
+class TestAliYunDatasource(test_helpers.HttprettyTestCase):
+    def setUp(self):
+        super(TestAliYunDatasource, self).setUp()
+        cfg = {'datasource': {'AliYun': {'timeout': '1', 'max_wait': '1'}}}
+        distro = {}
+        paths = helpers.Paths({})
+        self.ds = ay.DataSourceAliYun(cfg, distro, paths)
+        self.metadata_address = self.ds.metadata_address
+        self.api_ver = self.ds.api_ver
+
+    @property
+    def default_metadata(self):
+        return DEFAULT_METADATA
+
+    @property
+    def default_userdata(self):
+        return DEFAULT_USERDATA
+
+    @property
+    def metadata_url(self):
+        return os.path.join(self.metadata_address,
+                            self.api_ver, 'meta-data') + '/'
+
+    @property
+    def userdata_url(self):
+        return os.path.join(self.metadata_address,
+                            self.api_ver, 'user-data')
+
+    def regist_default_server(self):
+        register_mock_metaserver(self.metadata_url, self.default_metadata)
+        register_mock_metaserver(self.userdata_url, self.default_userdata)
+
+    def _test_get_data(self):
+        self.assertEqual(self.ds.metadata, self.default_metadata)
+        self.assertEqual(self.ds.userdata_raw,
+                         self.default_userdata.encode('utf8'))
+
+    def _test_get_sshkey(self):
+        pub_keys = [v['openssh-key'] for (_, v) in
+                    self.default_metadata['public-keys'].items()]
+        self.assertEqual(self.ds.get_public_ssh_keys(), pub_keys)
+
+    def _test_get_iid(self):
+        self.assertEqual(self.default_metadata['instance-id'],
+                         self.ds.get_instance_id())
+
+    def _test_host_name(self):
+        self.assertEqual(self.default_metadata['hostname'],
+                         self.ds.get_hostname())
+
+    def _test_ntp_conf(self):
+        self.assertEqual(self.default_metadata['ntp-conf'],
+                         self.ds.get_ntp_conf())
+
+    def _test_source_address(self):
+        self.assertEqual(self.default_metadata['source-address'],
+                         self.ds.get_source_address())
+
+    @httpretty.activate
+    def test_with_mock_server(self):
+        self.regist_default_server()
+        self.ds.get_data()
+        self._test_get_data()
+        self._test_get_sshkey()
+        self._test_get_iid()
+        self._test_host_name()
+        self._test_ntp_conf()
+        self._test_source_address()
+
+    def test_get_hostname_default(self):
+        self.assertEqual(self.ds.get_hostname(), 'localhost.localdomain')
+
+    def test_parse_public_keys(self):
+        public_keys = {'key-pair-0': 'ssh-key-0'}
+        self.assertEqual([public_keys['key-pair-0']],
+                         self.ds.parse_public_keys(public_keys))
+        public_keys = {'key-pair-0': ['ssh-key-0', 'ssh-key-1']}
+        self.assertEqual(public_keys['key-pair-0'],
+                         self.ds.parse_public_keys(public_keys))

Follow ups