Merge ~tribaal/cloud-init:feat/datasource-exoscale into cloud-init:master

Proposed by Chris Glass
Status: Merged
Approved by: Dan Watkins
Approved revision: afe3b3bd3f0de7f5364115ad17b4e585dcdb75e4
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~tribaal/cloud-init:feat/datasource-exoscale
Merge into: cloud-init:master
Diff against target: 628 lines (+540/-1)
8 files modified
cloudinit/apport.py (+1/-0)
cloudinit/settings.py (+1/-0)
cloudinit/sources/DataSourceExoscale.py (+258/-0)
doc/rtd/topics/datasources.rst (+1/-0)
doc/rtd/topics/datasources/exoscale.rst (+68/-0)
tests/unittests/test_datasource/test_common.py (+2/-0)
tests/unittests/test_datasource/test_exoscale.py (+203/-0)
tools/ds-identify (+6/-1)
Reviewer Review Type Date Requested Status
Dan Watkins Approve
Server Team CI bot continuous-integration Approve
Ryan Harper Approve
Review via email: mp+369516@code.launchpad.net

Commit message

New data source for the Exoscale.com cloud platform

- dsidentify switches to the new Exoscale datasource on matching DMI name
- New Exoscale datasource added

Author: Chris Glass <email address hidden>
Signed-off-by: Mathieu Corbin <email address hidden>

Description of the change

New data source for the Exoscale.com cloud platform

While the Exoscale cloud platform started as a Cloudstack fork, over the years
more and more of the codebase was rewritten, replaced, or otherwise
fundamentally changed.

Our recent feature development around networking made it clear that while we
intend for our cloud platform to remain as compatible with the Cloudstack
datasource in the forseeable future (once forced - see below), it is time for
us to bite the proverbial bullet and submit our own datasource for review.

Networking glitch:

The Cloudstack datasource doesn't work for us in the scenario where a user boots
a VM attached to the default network and one or more private network(s) running
DHCP (hosted by us, or customer-deployed): in some race condition the private
interface will be the lease on which the datasource will try to query for
metadata - which can't work. The submitted datasource fixes the problem by
forcing the datasource IP address to always be the same, so that we don't need
to depend on DHCP at all to determine it, fixing the race (we ensure routes to
that address are provided).

DMI product name change:

Along with this change, we intend to switch the DMI product name from
"CloudStack KVM Hypervisor" to "Exoscale Compute Platform" and the DMI
manufacturer string from "Apache Software Foundation" to "Exoscale".

Current templates situation:

We currently do not advertise custom templates to our customers, but intend to do
so more widely in the near future.

All of our current production templates force the Cloudstack datasource usage
(via configuration in /etc/cloud*).

Once this code lands, and our DMI product name change is performed, we will
document the requirements for (future) custom templates to be either:

- A recent enough cloud-init (containing the proposed code)
- Forcing the datasource to be "Cloudstack" for older cloud-init versions. This
  will trigger the above-mentioned race, but that is a specific, relatively rare
  use-case.
- Certain (future) features might only be available to customers running a
  recent-enough cloud-init version.

Testing this code:

Inclusion in the cloud-init test suite in addition to the submitted unit tests
would be welcome.

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:7279a5570bc87909022d9f849ac609d7acdbb3ca
https://jenkins.ubuntu.com/server/job/cloud-init-ci/751/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    FAILED: Ubuntu LTS: Integration

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/751/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:7279a5570bc87909022d9f849ac609d7acdbb3ca
https://jenkins.ubuntu.com/server/job/cloud-init-ci/752/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/752/rebuild

review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for submitting this. Some inline comments and questions.

In your description you mention networking configuration issues. Have you looked at providing network-metadata and having your datasource provide a network-config property to allow cloud-init to render networking[1] for the instance?

1. https://cloudinit.readthedocs.io/en/latest/topics/network-config.html

Revision history for this message
Dan Watkins (oddbloke) wrote :

Quick drive-by review to ask if we can reuse the password fetching from the CloudStack DS.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:64974621d238bd2bb5bbbf79fc27cc318975b1d0
https://jenkins.ubuntu.com/server/job/cloud-init-ci/758/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/758/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Chris Glass (tribaal) wrote :

A first pass fixing most review comments is done.

One open point here is the extra_config overwriting user-data - it seems to me that perhaps we should be using some other mechanism instead (my assumption was that extra_config would be overwritten by user data - but that doesn't seem to be the case).

Should we use "vendordata" instead for this particular case?

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for updating. The use of extra_config is fine for providing some default values when no-user-data is provided; You're correct that it will be overriden by user-data; I don't think you need to switch to vendor-data.

The jenkins failure was related to the switch to log_time and the time module import is no longer needed.

I've added some other questions inline. Thanks

review: Needs Information
Revision history for this message
Chris Glass (tribaal) wrote :

Thanks for the review(s).

I think all the points are once again addressed.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:bb9ca041b15d36a6b368ad6af09a99dbfef6295a
https://jenkins.ubuntu.com/server/job/cloud-init-ci/760/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/760/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for the updated, a couple more questions inline.

Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Chris Glass (tribaal) wrote :

Addressed all review comments.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:874cb3a6bfd158f9523cced13bd42f2dbaed0e4b
https://jenkins.ubuntu.com/server/job/cloud-init-ci/763/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/763/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:5c01e89c8f5abcc6150ebbcc82fc520fad87fbbb
https://jenkins.ubuntu.com/server/job/cloud-init-ci/764/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/764/rebuild

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

Some doc in doc/rtd/topics/datasources would be good.
https://cloudinit.readthedocs.io/en/latest/topics/datasources.html has examples.... Just at very least a link to exoscale.com and a mention on how that differs from ec2.

Revision history for this message
Chris Glass (tribaal) wrote :

Thanks for your review.

I think all your points have been addressed. While adding a bit of documentation I realized the datasource was not actually configurable. This is now the case.

Revision history for this message
Scott Moser (smoser) wrote :

This looks fine to me. one comment on the doc. i'm fine with this, but think it'd be good to have someone else also review.

Revision history for this message
Chris Glass (tribaal) wrote :

Dropped comments. Thanks!

Once again ready for review(s)!

Revision history for this message
Chad Smith (chad.smith) wrote :

thank you for the updates here. Couple of minor nits that may involve documentation changes and a request for unit test coverage.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:2fc709c9b07cba1fc43cc0aa95c899fac4a8910d
https://jenkins.ubuntu.com/server/job/cloud-init-ci/769/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/769/rebuild

review: Approve (continuous-integration)
Revision history for this message
Chris Glass (tribaal) wrote :

Should be ready for another pass once again.

Thanks a lot for your reviews so far!

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:fca069fa63f8d541d6ec9ce1d25a1ffcab824bb7
https://jenkins.ubuntu.com/server/job/cloud-init-ci/770/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/770/rebuild

review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:c5ea4ac4642feeb0deb35ec2f4117ee948491ebc
https://jenkins.ubuntu.com/server/job/cloud-init-ci/771/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/771/rebuild

review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

A couple of minor changes in docs requested.

review: Needs Fixing
Revision history for this message
Chris Glass (tribaal) wrote :

All requested changes should be addressed.

Thanks!

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:89c1eff973dcd6b8927a9835b0264fac498a94be
https://jenkins.ubuntu.com/server/job/cloud-init-ci/785/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/785/rebuild

review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

One typo, and question about the templates vs built-in datasource config for the set_password frequency.

fe07f84... by Chris Glass

Datasource makes password module run "always"

Doing this here ensures the config is merged the proper way (explicit
user choices via user-data override this setting).

9f73b66... by Chris Glass

Fix typo.

20f0c6a... by Chris Glass

Add extra tests.

Revision history for this message
Chris Glass (tribaal) wrote :

This is now ready for another round of review.

Thanks a lot!

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:20f0c6a1487572eaf5975b80665641098ba965b8
https://jenkins.ubuntu.com/server/job/cloud-init-ci/801/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/801/rebuild

review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

Thanks Chris,

This looks really good.

review: Approve
Revision history for this message
Dan Watkins (oddbloke) wrote :

Hey Chris, Mathieu!

Thanks for this change, it's looking in pretty good shape! There are quite a few inline comments here _but_ there are only two that I'm considering a hard blocker for landing this[0]:

* the missing copyright header
* my comment on line 143; I think that's the only (potential!) bug I spotted, and fixing it might drive some other changes to the data source so I'd like us to understand the scope of those before merging this

Please do review my other comments, and let me know what you think. Provided you can commit to addressing them in a follow-up merge proposal[1], once we've resolved the above we can land this and SRU it back with 19.2.

Thanks!

Dan

[0] Terms and conditions apply.
[1] These are the terms and conditions. ;)

review: Needs Fixing
Revision history for this message
Dan Watkins (oddbloke) :
afe3b3b... by Chris Glass

Address review comments

- Copyright headers
- Various phrasing fixes in the documentation
- Code fixes
- Ensure a non-responding password server still lets the datasource use
  the scraped metadata/userdata information

Revision history for this message
Chris Glass (tribaal) wrote :

Hey Dan, thanks for the review!

All comments should be addressed.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:afe3b3bd3f0de7f5364115ad17b4e585dcdb75e4
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1027/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1027//rebuild

review: Approve (continuous-integration)
Revision history for this message
Dan Watkins (oddbloke) wrote :

Hey Chris, thanks for the updates and the thoughtful responses, this looks great!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 = [
23 'CloudStack',23 'CloudStack',
24 'DigitalOcean',24 'DigitalOcean',
25 'GCE - Google Compute Engine',25 'GCE - Google Compute Engine',
26 'Exoscale',
26 'Hetzner Cloud',27 'Hetzner Cloud',
27 'IBM - (aka SoftLayer or BlueMix)',28 'IBM - (aka SoftLayer or BlueMix)',
28 'LXD',29 '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 = {
39 'Hetzner',39 'Hetzner',
40 'IBMCloud',40 'IBMCloud',
41 'Oracle',41 'Oracle',
42 'Exoscale',
42 # At the end to act as a 'catch' when none of the above work...43 # At the end to act as a 'catch' when none of the above work...
43 'None',44 'None',
44 ],45 ],
diff --git a/cloudinit/sources/DataSourceExoscale.py b/cloudinit/sources/DataSourceExoscale.py
45new file mode 10064446new file mode 100644
index 0000000..52e7f6f
--- /dev/null
+++ b/cloudinit/sources/DataSourceExoscale.py
@@ -0,0 +1,258 @@
1# Author: Mathieu Corbin <mathieu.corbin@exoscale.com>
2# Author: Christopher Glass <christopher.glass@exoscale.com>
3#
4# This file is part of cloud-init. See LICENSE file for license information.
5
6from cloudinit import ec2_utils as ec2
7from cloudinit import log as logging
8from cloudinit import sources
9from cloudinit import url_helper
10from cloudinit import util
11
12LOG = logging.getLogger(__name__)
13
14METADATA_URL = "http://169.254.169.254"
15API_VERSION = "1.0"
16PASSWORD_SERVER_PORT = 8080
17
18URL_TIMEOUT = 10
19URL_RETRIES = 6
20
21EXOSCALE_DMI_NAME = "Exoscale"
22
23BUILTIN_DS_CONFIG = {
24 # We run the set password config module on every boot in order to enable
25 # resetting the instance's password via the exoscale console (and a
26 # subsequent instance reboot).
27 'cloud_config_modules': [["set-passwords", "always"]]
28}
29
30
31class DataSourceExoscale(sources.DataSource):
32
33 dsname = 'Exoscale'
34
35 def __init__(self, sys_cfg, distro, paths):
36 super(DataSourceExoscale, self).__init__(sys_cfg, distro, paths)
37 LOG.debug("Initializing the Exoscale datasource")
38
39 self.metadata_url = self.ds_cfg.get('metadata_url', METADATA_URL)
40 self.api_version = self.ds_cfg.get('api_version', API_VERSION)
41 self.password_server_port = int(
42 self.ds_cfg.get('password_server_port', PASSWORD_SERVER_PORT))
43 self.url_timeout = self.ds_cfg.get('timeout', URL_TIMEOUT)
44 self.url_retries = self.ds_cfg.get('retries', URL_RETRIES)
45
46 self.extra_config = BUILTIN_DS_CONFIG
47
48 def wait_for_metadata_service(self):
49 """Wait for the metadata service to be reachable."""
50
51 metadata_url = "{}/{}/meta-data/instance-id".format(
52 self.metadata_url, self.api_version)
53
54 url = url_helper.wait_for_url(
55 urls=[metadata_url],
56 max_wait=self.url_max_wait,
57 timeout=self.url_timeout,
58 status_cb=LOG.critical)
59
60 return bool(url)
61
62 def crawl_metadata(self):
63 """
64 Crawl the metadata service when available.
65
66 @returns: Dictionary of crawled metadata content.
67 """
68 metadata_ready = util.log_time(
69 logfunc=LOG.info,
70 msg='waiting for the metadata service',
71 func=self.wait_for_metadata_service)
72
73 if not metadata_ready:
74 return {}
75
76 return read_metadata(self.metadata_url, self.api_version,
77 self.password_server_port, self.url_timeout,
78 self.url_retries)
79
80 def _get_data(self):
81 """Fetch the user data, the metadata and the VM password
82 from the metadata service.
83
84 Please refer to the datasource documentation for details on how the
85 metadata server and password server are crawled.
86 """
87 if not self._is_platform_viable():
88 return False
89
90 data = util.log_time(
91 logfunc=LOG.debug,
92 msg='Crawl of metadata service',
93 func=self.crawl_metadata)
94
95 if not data:
96 return False
97
98 self.userdata_raw = data['user-data']
99 self.metadata = data['meta-data']
100 password = data.get('password')
101
102 password_config = {}
103 if password:
104 # Since we have a password, let's make sure we are allowed to use
105 # it by allowing ssh_pwauth.
106 # The password module's default behavior is to leave the
107 # configuration as-is in this regard, so that means it will either
108 # leave the password always disabled if no password is ever set, or
109 # leave the password login enabled if we set it once.
110 password_config = {
111 'ssh_pwauth': True,
112 'password': password,
113 'chpasswd': {
114 'expire': False,
115 },
116 }
117
118 # builtin extra_config overrides password_config
119 self.extra_config = util.mergemanydict(
120 [self.extra_config, password_config])
121
122 return True
123
124 def get_config_obj(self):
125 return self.extra_config
126
127 def _is_platform_viable(self):
128 return util.read_dmi_data('system-product-name').startswith(
129 EXOSCALE_DMI_NAME)
130
131
132# Used to match classes to dependencies
133datasources = [
134 (DataSourceExoscale, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
135]
136
137
138# Return a list of data sources that match this set of dependencies
139def get_datasource_list(depends):
140 return sources.list_from_depends(depends, datasources)
141
142
143def get_password(metadata_url=METADATA_URL,
144 api_version=API_VERSION,
145 password_server_port=PASSWORD_SERVER_PORT,
146 url_timeout=URL_TIMEOUT,
147 url_retries=URL_RETRIES):
148 """Obtain the VM's password if set.
149
150 Once fetched the password is marked saved. Future calls to this method may
151 return empty string or 'saved_password'."""
152 password_url = "{}:{}/{}/".format(metadata_url, password_server_port,
153 api_version)
154 response = url_helper.read_file_or_url(
155 password_url,
156 ssl_details=None,
157 headers={"DomU_Request": "send_my_password"},
158 timeout=url_timeout,
159 retries=url_retries)
160 password = response.contents.decode('utf-8')
161 # the password is empty or already saved
162 # Note: the original metadata server would answer an additional
163 # 'bad_request' status, but the Exoscale implementation does not.
164 if password in ['', 'saved_password']:
165 return None
166 # save the password
167 url_helper.read_file_or_url(
168 password_url,
169 ssl_details=None,
170 headers={"DomU_Request": "saved_password"},
171 timeout=url_timeout,
172 retries=url_retries)
173 return password
174
175
176def read_metadata(metadata_url=METADATA_URL,
177 api_version=API_VERSION,
178 password_server_port=PASSWORD_SERVER_PORT,
179 url_timeout=URL_TIMEOUT,
180 url_retries=URL_RETRIES):
181 """Query the metadata server and return the retrieved data."""
182 crawled_metadata = {}
183 crawled_metadata['_metadata_api_version'] = api_version
184 try:
185 crawled_metadata['user-data'] = ec2.get_instance_userdata(
186 api_version,
187 metadata_url,
188 timeout=url_timeout,
189 retries=url_retries)
190 crawled_metadata['meta-data'] = ec2.get_instance_metadata(
191 api_version,
192 metadata_url,
193 timeout=url_timeout,
194 retries=url_retries)
195 except Exception as e:
196 util.logexc(LOG, "failed reading from metadata url %s (%s)",
197 metadata_url, e)
198 return {}
199
200 try:
201 crawled_metadata['password'] = get_password(
202 api_version=api_version,
203 metadata_url=metadata_url,
204 password_server_port=password_server_port,
205 url_retries=url_retries,
206 url_timeout=url_timeout)
207 except Exception as e:
208 util.logexc(LOG, "failed to read from password server url %s:%s (%s)",
209 metadata_url, password_server_port, e)
210
211 return crawled_metadata
212
213
214if __name__ == "__main__":
215 import argparse
216
217 parser = argparse.ArgumentParser(description='Query Exoscale Metadata')
218 parser.add_argument(
219 "--endpoint",
220 metavar="URL",
221 help="The url of the metadata service.",
222 default=METADATA_URL)
223 parser.add_argument(
224 "--version",
225 metavar="VERSION",
226 help="The version of the metadata endpoint to query.",
227 default=API_VERSION)
228 parser.add_argument(
229 "--retries",
230 metavar="NUM",
231 type=int,
232 help="The number of retries querying the endpoint.",
233 default=URL_RETRIES)
234 parser.add_argument(
235 "--timeout",
236 metavar="NUM",
237 type=int,
238 help="The time in seconds to wait before timing out.",
239 default=URL_TIMEOUT)
240 parser.add_argument(
241 "--password-port",
242 metavar="PORT",
243 type=int,
244 help="The port on which the password endpoint listens",
245 default=PASSWORD_SERVER_PORT)
246
247 args = parser.parse_args()
248
249 data = read_metadata(
250 metadata_url=args.endpoint,
251 api_version=args.version,
252 password_server_port=args.password_port,
253 url_timeout=args.timeout,
254 url_retries=args.retries)
255
256 print(util.json_dumps(data))
257
258# vi: ts=4 expandtab
diff --git a/doc/rtd/topics/datasources.rst b/doc/rtd/topics/datasources.rst
index 648c606..2148cd5 100644
--- a/doc/rtd/topics/datasources.rst
+++ b/doc/rtd/topics/datasources.rst
@@ -155,6 +155,7 @@ Follow for more information.
155 datasources/configdrive.rst155 datasources/configdrive.rst
156 datasources/digitalocean.rst156 datasources/digitalocean.rst
157 datasources/ec2.rst157 datasources/ec2.rst
158 datasources/exoscale.rst
158 datasources/maas.rst159 datasources/maas.rst
159 datasources/nocloud.rst160 datasources/nocloud.rst
160 datasources/opennebula.rst161 datasources/opennebula.rst
diff --git a/doc/rtd/topics/datasources/exoscale.rst b/doc/rtd/topics/datasources/exoscale.rst
161new file mode 100644162new file mode 100644
index 0000000..27aec9c
--- /dev/null
+++ b/doc/rtd/topics/datasources/exoscale.rst
@@ -0,0 +1,68 @@
1.. _datasource_exoscale:
2
3Exoscale
4========
5
6This datasource supports reading from the metadata server used on the
7`Exoscale platform <https://exoscale.com>`_.
8
9Use of the Exoscale datasource is recommended to benefit from new features of
10the Exoscale platform.
11
12The datasource relies on the availability of a compatible metadata server
13(``http://169.254.169.254`` is used by default) and its companion password
14server, reachable at the same address (by default on port 8080).
15
16Crawling of metadata
17--------------------
18
19The metadata service and password server are crawled slightly differently:
20
21 * The "metadata service" is crawled every boot.
22 * The password server is also crawled every boot (the Exoscale datasource
23 forces the password module to run with "frequency always").
24
25In the password server case, the following rules apply in order to enable the
26"restore instance password" functionality:
27
28 * If a password is returned by the password server, it is then marked "saved"
29 by the cloud-init datasource. Subsequent boots will skip setting the password
30 (the password server will return "saved_password").
31 * When the instance password is reset (via the Exoscale UI), the password
32 server will return the non-empty password at next boot, therefore causing
33 cloud-init to reset the instance's password.
34
35Configuration
36-------------
37
38Users of this datasource are discouraged from changing the default settings
39unless instructed to by Exoscale support.
40
41The following settings are available and can be set for the datasource in system
42configuration (in `/etc/cloud/cloud.cfg.d/`).
43
44The settings available are:
45
46 * **metadata_url**: The URL for the metadata service (defaults to
47 ``http://169.254.169.254``)
48 * **api_version**: The API version path on which to query the instance metadata
49 (defaults to ``1.0``)
50 * **password_server_port**: The port (on the metadata server) on which the
51 password server listens (defaults to ``8080``).
52 * **timeout**: the timeout value provided to urlopen for each individual http
53 request. (defaults to ``10``)
54 * **retries**: The number of retries that should be done for an http request
55 (defaults to ``6``)
56
57
58An example configuration with the default values is provided below:
59
60.. sourcecode:: yaml
61
62 datasource:
63 Exoscale:
64 metadata_url: "http://169.254.169.254"
65 api_version: "1.0"
66 password_server_port: 8080
67 timeout: 10
68 retries: 6
diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/test_datasource/test_common.py
index 2a9cfb2..61a7a76 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 (
13 DataSourceConfigDrive as ConfigDrive,13 DataSourceConfigDrive as ConfigDrive,
14 DataSourceDigitalOcean as DigitalOcean,14 DataSourceDigitalOcean as DigitalOcean,
15 DataSourceEc2 as Ec2,15 DataSourceEc2 as Ec2,
16 DataSourceExoscale as Exoscale,
16 DataSourceGCE as GCE,17 DataSourceGCE as GCE,
17 DataSourceHetzner as Hetzner,18 DataSourceHetzner as Hetzner,
18 DataSourceIBMCloud as IBMCloud,19 DataSourceIBMCloud as IBMCloud,
@@ -53,6 +54,7 @@ DEFAULT_NETWORK = [
53 CloudStack.DataSourceCloudStack,54 CloudStack.DataSourceCloudStack,
54 DSNone.DataSourceNone,55 DSNone.DataSourceNone,
55 Ec2.DataSourceEc2,56 Ec2.DataSourceEc2,
57 Exoscale.DataSourceExoscale,
56 GCE.DataSourceGCE,58 GCE.DataSourceGCE,
57 MAAS.DataSourceMAAS,59 MAAS.DataSourceMAAS,
58 NoCloud.DataSourceNoCloudNet,60 NoCloud.DataSourceNoCloudNet,
diff --git a/tests/unittests/test_datasource/test_exoscale.py b/tests/unittests/test_datasource/test_exoscale.py
59new file mode 10064461new file mode 100644
index 0000000..350c330
--- /dev/null
+++ b/tests/unittests/test_datasource/test_exoscale.py
@@ -0,0 +1,203 @@
1# Author: Mathieu Corbin <mathieu.corbin@exoscale.com>
2# Author: Christopher Glass <christopher.glass@exoscale.com>
3#
4# This file is part of cloud-init. See LICENSE file for license information.
5from cloudinit import helpers
6from cloudinit.sources.DataSourceExoscale import (
7 API_VERSION,
8 DataSourceExoscale,
9 METADATA_URL,
10 get_password,
11 PASSWORD_SERVER_PORT,
12 read_metadata)
13from cloudinit.tests.helpers import HttprettyTestCase, mock
14
15import httpretty
16import requests
17
18
19TEST_PASSWORD_URL = "{}:{}/{}/".format(METADATA_URL,
20 PASSWORD_SERVER_PORT,
21 API_VERSION)
22
23TEST_METADATA_URL = "{}/{}/meta-data/".format(METADATA_URL,
24 API_VERSION)
25
26TEST_USERDATA_URL = "{}/{}/user-data".format(METADATA_URL,
27 API_VERSION)
28
29
30@httpretty.activate
31class TestDatasourceExoscale(HttprettyTestCase):
32
33 def setUp(self):
34 super(TestDatasourceExoscale, self).setUp()
35 self.tmp = self.tmp_dir()
36 self.password_url = TEST_PASSWORD_URL
37 self.metadata_url = TEST_METADATA_URL
38 self.userdata_url = TEST_USERDATA_URL
39
40 def test_password_saved(self):
41 """The password is not set when it is not found
42 in the metadata service."""
43 httpretty.register_uri(httpretty.GET,
44 self.password_url,
45 body="saved_password")
46 self.assertFalse(get_password())
47
48 def test_password_empty(self):
49 """No password is set if the metadata service returns
50 an empty string."""
51 httpretty.register_uri(httpretty.GET,
52 self.password_url,
53 body="")
54 self.assertFalse(get_password())
55
56 def test_password(self):
57 """The password is set to what is found in the metadata
58 service."""
59 expected_password = "p@ssw0rd"
60 httpretty.register_uri(httpretty.GET,
61 self.password_url,
62 body=expected_password)
63 password = get_password()
64 self.assertEqual(expected_password, password)
65
66 def test_get_data(self):
67 """The datasource conforms to expected behavior when supplied
68 full test data."""
69 path = helpers.Paths({'run_dir': self.tmp})
70 ds = DataSourceExoscale({}, None, path)
71 ds._is_platform_viable = lambda: True
72 expected_password = "p@ssw0rd"
73 expected_id = "12345"
74 expected_hostname = "myname"
75 expected_userdata = "#cloud-config"
76 httpretty.register_uri(httpretty.GET,
77 self.userdata_url,
78 body=expected_userdata)
79 httpretty.register_uri(httpretty.GET,
80 self.password_url,
81 body=expected_password)
82 httpretty.register_uri(httpretty.GET,
83 self.metadata_url,
84 body="instance-id\nlocal-hostname")
85 httpretty.register_uri(httpretty.GET,
86 "{}local-hostname".format(self.metadata_url),
87 body=expected_hostname)
88 httpretty.register_uri(httpretty.GET,
89 "{}instance-id".format(self.metadata_url),
90 body=expected_id)
91 self.assertTrue(ds._get_data())
92 self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config")
93 self.assertEqual(ds.metadata, {"instance-id": expected_id,
94 "local-hostname": expected_hostname})
95 self.assertEqual(ds.get_config_obj(),
96 {'ssh_pwauth': True,
97 'password': expected_password,
98 'cloud_config_modules': [
99 ["set-passwords", "always"]],
100 'chpasswd': {
101 'expire': False,
102 }})
103
104 def test_get_data_saved_password(self):
105 """The datasource conforms to expected behavior when saved_password is
106 returned by the password server."""
107 path = helpers.Paths({'run_dir': self.tmp})
108 ds = DataSourceExoscale({}, None, path)
109 ds._is_platform_viable = lambda: True
110 expected_answer = "saved_password"
111 expected_id = "12345"
112 expected_hostname = "myname"
113 expected_userdata = "#cloud-config"
114 httpretty.register_uri(httpretty.GET,
115 self.userdata_url,
116 body=expected_userdata)
117 httpretty.register_uri(httpretty.GET,
118 self.password_url,
119 body=expected_answer)
120 httpretty.register_uri(httpretty.GET,
121 self.metadata_url,
122 body="instance-id\nlocal-hostname")
123 httpretty.register_uri(httpretty.GET,
124 "{}local-hostname".format(self.metadata_url),
125 body=expected_hostname)
126 httpretty.register_uri(httpretty.GET,
127 "{}instance-id".format(self.metadata_url),
128 body=expected_id)
129 self.assertTrue(ds._get_data())
130 self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config")
131 self.assertEqual(ds.metadata, {"instance-id": expected_id,
132 "local-hostname": expected_hostname})
133 self.assertEqual(ds.get_config_obj(),
134 {'cloud_config_modules': [
135 ["set-passwords", "always"]]})
136
137 def test_get_data_no_password(self):
138 """The datasource conforms to expected behavior when no password is
139 returned by the password server."""
140 path = helpers.Paths({'run_dir': self.tmp})
141 ds = DataSourceExoscale({}, None, path)
142 ds._is_platform_viable = lambda: True
143 expected_answer = ""
144 expected_id = "12345"
145 expected_hostname = "myname"
146 expected_userdata = "#cloud-config"
147 httpretty.register_uri(httpretty.GET,
148 self.userdata_url,
149 body=expected_userdata)
150 httpretty.register_uri(httpretty.GET,
151 self.password_url,
152 body=expected_answer)
153 httpretty.register_uri(httpretty.GET,
154 self.metadata_url,
155 body="instance-id\nlocal-hostname")
156 httpretty.register_uri(httpretty.GET,
157 "{}local-hostname".format(self.metadata_url),
158 body=expected_hostname)
159 httpretty.register_uri(httpretty.GET,
160 "{}instance-id".format(self.metadata_url),
161 body=expected_id)
162 self.assertTrue(ds._get_data())
163 self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config")
164 self.assertEqual(ds.metadata, {"instance-id": expected_id,
165 "local-hostname": expected_hostname})
166 self.assertEqual(ds.get_config_obj(),
167 {'cloud_config_modules': [
168 ["set-passwords", "always"]]})
169
170 @mock.patch('cloudinit.sources.DataSourceExoscale.get_password')
171 def test_read_metadata_when_password_server_unreachable(self, m_password):
172 """The read_metadata function returns partial results in case the
173 password server (only) is unreachable."""
174 expected_id = "12345"
175 expected_hostname = "myname"
176 expected_userdata = "#cloud-config"
177
178 m_password.side_effect = requests.Timeout('Fake Connection Timeout')
179 httpretty.register_uri(httpretty.GET,
180 self.userdata_url,
181 body=expected_userdata)
182 httpretty.register_uri(httpretty.GET,
183 self.metadata_url,
184 body="instance-id\nlocal-hostname")
185 httpretty.register_uri(httpretty.GET,
186 "{}local-hostname".format(self.metadata_url),
187 body=expected_hostname)
188 httpretty.register_uri(httpretty.GET,
189 "{}instance-id".format(self.metadata_url),
190 body=expected_id)
191
192 result = read_metadata()
193
194 self.assertIsNone(result.get("password"))
195 self.assertEqual(result.get("user-data").decode("utf-8"),
196 expected_userdata)
197
198 def test_non_viable_platform(self):
199 """The datasource fails fast when the platform is not viable."""
200 path = helpers.Paths({'run_dir': self.tmp})
201 ds = DataSourceExoscale({}, None, path)
202 ds._is_platform_viable = lambda: False
203 self.assertFalse(ds._get_data())
diff --git a/tools/ds-identify b/tools/ds-identify
index 0305e36..e0d4865 100755
--- a/tools/ds-identify
+++ b/tools/ds-identify
@@ -124,7 +124,7 @@ DI_DSNAME=""
124# be searched if there is no setting found in config.124# be searched if there is no setting found in config.
125DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \125DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \
126CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \126CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \
127OVF SmartOS Scaleway Hetzner IBMCloud Oracle"127OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale"
128DI_DSLIST=""128DI_DSLIST=""
129DI_MODE=""129DI_MODE=""
130DI_ON_FOUND=""130DI_ON_FOUND=""
@@ -553,6 +553,11 @@ dscheck_CloudStack() {
553 return $DS_NOT_FOUND553 return $DS_NOT_FOUND
554}554}
555555
556dscheck_Exoscale() {
557 dmi_product_name_matches "Exoscale*" && return $DS_FOUND
558 return $DS_NOT_FOUND
559}
560
556dscheck_CloudSigma() {561dscheck_CloudSigma() {
557 # http://paste.ubuntu.com/23624795/562 # http://paste.ubuntu.com/23624795/
558 dmi_product_name_matches "CloudSigma" && return $DS_FOUND563 dmi_product_name_matches "CloudSigma" && return $DS_FOUND

Subscribers

People subscribed via source and target branches