Mathieu Corbin has proposed merging ~mcorbinexoscale/cloud-init:feat/datasource-exoscale into cloud-init:master.
Requested reviews: cloud-init commiters (cloud-init-dev) For more details, see: https://code.launchpad.net/~mcorbinexoscale/cloud-init/+git/cloud-init/+merge/369503 -- Your team cloud-init commiters is requested to review the proposed merge of ~mcorbinexoscale/cloud-init:feat/datasource-exoscale into cloud-init:master.
diff --git a/cloudinit/apport.py b/cloudinit/apport.py index 22cb7fd..003ff1f 100644 --- a/cloudinit/apport.py +++ b/cloudinit/apport.py @@ -23,6 +23,7 @@ KNOWN_CLOUD_NAMES = [ 'CloudStack', 'DigitalOcean', 'GCE - Google Compute Engine', + 'Exoscale', 'Hetzner Cloud', 'IBM - (aka SoftLayer or BlueMix)', 'LXD', diff --git a/cloudinit/settings.py b/cloudinit/settings.py index b1ebaad..2060d81 100644 --- a/cloudinit/settings.py +++ b/cloudinit/settings.py @@ -39,6 +39,7 @@ CFG_BUILTIN = { 'Hetzner', 'IBMCloud', 'Oracle', + 'Exoscale', # At the end to act as a 'catch' when none of the above work... 'None', ], diff --git a/cloudinit/sources/DataSourceExoscale.py b/cloudinit/sources/DataSourceExoscale.py new file mode 100644 index 0000000..4588c9d --- /dev/null +++ b/cloudinit/sources/DataSourceExoscale.py @@ -0,0 +1,126 @@ +import time + +from cloudinit import ec2_utils as ec2 +from cloudinit import log as logging +from cloudinit import sources +from cloudinit import url_helper + +API_VERSION = "latest" +LOG = logging.getLogger(__name__) +SERVICE_ADDRESS = "http://169.254.169.254" + + +class DataSourceExoscale(sources.DataSource): + + dsname = 'Exoscale' + url_timeout = 10 + url_retries = 6 + url_max_wait = 60 + + def __init__(self, sys_cfg, distro, paths): + sources.DataSource.__init__(self, sys_cfg, distro, paths) + LOG.info("Initializing the Exoscale datasource") + self.extra_config = {} + + def get_password(self): + """Return the VM's passwords.""" + LOG.info("Fetching password from metadata service") + password_url = "{}:8080".format(SERVICE_ADDRESS) + response = url_helper.read_file_or_url( + password_url, + ssl_details=None, + headers={"DomU_Request": "send_my_password"}, + timeout=self.url_timeout, + retries=self.url_retries) + password = response.contents.decode('utf-8') + # the password is empty or already saved + if password in ['', 'saved_password']: + LOG.info("Password is missing or already saved") + return None + LOG.info("Found the password in metadata service") + # save the password + url_helper.read_file_or_url( + password_url, + ssl_details=None, + headers={"DomU_Request": "saved_password"}, + timeout=self.url_timeout, + retries=self.url_retries) + LOG.info("password saved") + return password + + def wait_for_metadata_service(self): + """Wait for the metadata service to be reachable.""" + LOG.info("waiting for the metadata service") + start_time = time.time() + + metadata_url = "{}/{}/meta-data/instance-id".format( + SERVICE_ADDRESS, + API_VERSION) + + start_time = time.time() + url = url_helper.wait_for_url( + urls=[metadata_url], + max_wait=self.url_max_wait, + timeout=self.url_timeout, + status_cb=LOG.critical) + + if url: + LOG.info("metadata service ok") + return True + else: + wait_time = int(time.time() - start_time) + LOG.critical(("Giving up on waiting for the metadata from %s" + " after %s seconds"), + url, + wait_time) + return False + + def _get_data(self): + """Fetch the user data, the metadata and the VM password + from the metadata service.""" + LOG.info("fetching data") + if not self.wait_for_metadata_service(): + return False + start_time = time.time() + self.userdata_raw = ec2.get_instance_userdata(API_VERSION, + SERVICE_ADDRESS, + timeout=self.url_timeout, + retries=self.url_retries) + self.metadata = ec2.get_instance_metadata(API_VERSION, + SERVICE_ADDRESS, + timeout=self.url_timeout, + retries=self.url_retries) + password = self.get_password() + if password: + self.extra_config = { + 'ssh_pwauth': True, + 'password': password, + 'chpasswd': { + 'expire': False, + }, + } + get_data_time = int(time.time() - start_time) + LOG.info("finished fetching the metadata in %s seconds", + get_data_time) + return True + + def get_config_obj(self): + return self.extra_config + + def get_instance_id(self): + return self.metadata['instance-id'] + + @property + def availability_zone(self): + return self.metadata['availability-zone'] + + +# Used to match classes to dependencies +datasources = [ + (DataSourceExoscale, (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_common.py b/tests/unittests/test_datasource/test_common.py index 6b01a4e..24b0fac 100644 --- a/tests/unittests/test_datasource/test_common.py +++ b/tests/unittests/test_datasource/test_common.py @@ -13,6 +13,7 @@ from cloudinit.sources import ( DataSourceConfigDrive as ConfigDrive, DataSourceDigitalOcean as DigitalOcean, DataSourceEc2 as Ec2, + DataSourceExoscale as Exoscale, DataSourceGCE as GCE, DataSourceHetzner as Hetzner, DataSourceIBMCloud as IBMCloud, @@ -53,6 +54,7 @@ DEFAULT_NETWORK = [ CloudStack.DataSourceCloudStack, DSNone.DataSourceNone, Ec2.DataSourceEc2, + Exoscale.DataSourceExoscale, GCE.DataSourceGCE, MAAS.DataSourceMAAS, NoCloud.DataSourceNoCloudNet, diff --git a/tests/unittests/test_datasource/test_exoscale.py b/tests/unittests/test_datasource/test_exoscale.py new file mode 100644 index 0000000..4bb9379 --- /dev/null +++ b/tests/unittests/test_datasource/test_exoscale.py @@ -0,0 +1,93 @@ +# This file is part of cloud-init. See LICENSE file for license information. +from cloudinit import helpers +from cloudinit.sources.DataSourceExoscale import ( + API_VERSION, + DataSourceExoscale, + SERVICE_ADDRESS) +from cloudinit.tests.helpers import HttprettyTestCase + +import httpretty + + +@httpretty.activate +class TestDatasourceExoscale(HttprettyTestCase): + + def setUp(self): + super(TestDatasourceExoscale, self).setUp() + self.tmp = self.tmp_dir() + + self.password_url = "{}:8080/".format(SERVICE_ADDRESS) + self.metadata_url = "{}/{}/meta-data/".format(SERVICE_ADDRESS, + API_VERSION) + self.userdata_url = "{}/{}/user-data".format(SERVICE_ADDRESS, + API_VERSION) + + def test_password_saved(self): + """The password is not set when it is not found + in the metadata service.""" + path = helpers.Paths({'run_dir': self.tmp}) + ds = DataSourceExoscale({}, None, path) + httpretty.register_uri(httpretty.GET, + self.password_url, + body="saved_password") + self.assertFalse(ds.get_password()) + + def test_password_empty(self): + """No password is set if the metadata service returns + an empty string.""" + path = helpers.Paths({'run_dir': self.tmp}) + ds = DataSourceExoscale({}, None, path) + httpretty.register_uri(httpretty.GET, + self.password_url, + body="") + self.assertFalse(ds.get_password()) + + def test_password(self): + """The password is set to what is found in the metadata + service.""" + path = helpers.Paths({'run_dir': self.tmp}) + ds = DataSourceExoscale({}, None, path) + expected_password = "p@ssw0rd" + httpretty.register_uri(httpretty.GET, + self.password_url, + body=expected_password) + password = ds.get_password() + self.assertEqual(expected_password, password) + + def test_get_data(self): + """The datasource conforms to expected behavior when supplied + full test data.""" + path = helpers.Paths({'run_dir': self.tmp}) + ds = DataSourceExoscale({}, None, path) + expected_password = "p@ssw0rd" + expected_id = "12345" + expected_hostname = "myname" + expected_userdata = "#cloud-config" + httpretty.register_uri(httpretty.GET, + self.userdata_url, + body=expected_userdata) + httpretty.register_uri(httpretty.GET, + self.password_url, + body=expected_password) + httpretty.register_uri(httpretty.GET, + self.metadata_url, + body="instance-id\nlocal-hostname") + httpretty.register_uri(httpretty.GET, + "{}local-hostname".format(self.metadata_url), + body=expected_hostname) + httpretty.register_uri(httpretty.GET, + "{}local-hostname".format(self.metadata_url), + body=expected_hostname) + httpretty.register_uri(httpretty.GET, + "{}instance-id".format(self.metadata_url), + body=expected_id) + ds._get_data() + self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config") + self.assertEqual(ds.metadata, {"instance-id": expected_id, + "local-hostname": expected_hostname}) + self.assertEqual(ds.get_config_obj(), + {'ssh_pwauth': True, + 'password': expected_password, + 'chpasswd': { + 'expire': False, + }}) diff --git a/tools/ds-identify b/tools/ds-identify index e16708f..5727c24 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -124,7 +124,7 @@ DI_DSNAME="" # be searched if there is no setting found in config. DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \ CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \ -OVF SmartOS Scaleway Hetzner IBMCloud Oracle" +OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale" DI_DSLIST="" DI_MODE="" DI_ON_FOUND="" @@ -553,6 +553,12 @@ dscheck_CloudStack() { return $DS_NOT_FOUND } +dscheck_Exoscale() { + is_container && return ${DS_NOT_FOUND} + dmi_product_name_matches "Exoscale*" && return $DS_FOUND + return $DS_NOT_FOUND +} + dscheck_CloudSigma() { # http://paste.ubuntu.com/23624795/ dmi_product_name_matches "CloudSigma" && return $DS_FOUND
_______________________________________________ Mailing list: https://launchpad.net/~cloud-init-dev Post to : cloud-init-dev@lists.launchpad.net Unsubscribe : https://launchpad.net/~cloud-init-dev More help : https://help.launchpad.net/ListHelp