Merge ~tribaal/cloud-init:feat/datasource-exoscale into cloud-init:master
- Git
- lp:~tribaal/cloud-init
- feat/datasource-exoscale
- Merge into master
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) |
Related bugs: |
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.
Server Team CI bot (server-team-bot) wrote : | # |
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:7279a5570bc
https:/
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:/
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:/
Dan Watkins (oddbloke) wrote : | # |
Quick drive-by review to ask if we can reuse the password fetching from the CloudStack DS.
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:64974621d23
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
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?
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
Chris Glass (tribaal) wrote : | # |
Thanks for the review(s).
I think all the points are once again addressed.
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:bb9ca041b15
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Ryan Harper (raharper) wrote : | # |
Thanks for the updated, a couple more questions inline.
Chad Smith (chad.smith) : | # |
Chad Smith (chad.smith) : | # |
Chris Glass (tribaal) wrote : | # |
Addressed all review comments.
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:874cb3a6bfd
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:5c01e89c8f5
https:/
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:/
Scott Moser (smoser) wrote : | # |
Some doc in doc/rtd/
https:/
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.
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.
Chris Glass (tribaal) wrote : | # |
Dropped comments. Thanks!
Once again ready for review(s)!
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.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:2fc709c9b07
https:/
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:/
Chris Glass (tribaal) wrote : | # |
Should be ready for another pass once again.
Thanks a lot for your reviews so far!
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:fca069fa63f
https:/
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:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:c5ea4ac4642
https:/
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:/
Ryan Harper (raharper) wrote : | # |
A couple of minor changes in docs requested.
Chris Glass (tribaal) wrote : | # |
All requested changes should be addressed.
Thanks!
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:89c1eff973d
https:/
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:/
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.
Chris Glass (tribaal) wrote : | # |
This is now ready for another round of review.
Thanks a lot!
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:20f0c6a1487
https:/
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:/
Ryan Harper (raharper) wrote : | # |
Thanks Chris,
This looks really good.
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. ;)
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
Chris Glass (tribaal) wrote : | # |
Hey Dan, thanks for the review!
All comments should be addressed.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:afe3b3bd3f0
https:/
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:/
Dan Watkins (oddbloke) wrote : | # |
Hey Chris, thanks for the updates and the thoughtful responses, this looks great!
Preview Diff
1 | diff --git a/cloudinit/apport.py b/cloudinit/apport.py | |||
2 | index 22cb7fd..003ff1f 100644 | |||
3 | --- a/cloudinit/apport.py | |||
4 | +++ b/cloudinit/apport.py | |||
5 | @@ -23,6 +23,7 @@ KNOWN_CLOUD_NAMES = [ | |||
6 | 23 | 'CloudStack', | 23 | 'CloudStack', |
7 | 24 | 'DigitalOcean', | 24 | 'DigitalOcean', |
8 | 25 | 'GCE - Google Compute Engine', | 25 | 'GCE - Google Compute Engine', |
9 | 26 | 'Exoscale', | ||
10 | 26 | 'Hetzner Cloud', | 27 | 'Hetzner Cloud', |
11 | 27 | 'IBM - (aka SoftLayer or BlueMix)', | 28 | 'IBM - (aka SoftLayer or BlueMix)', |
12 | 28 | 'LXD', | 29 | 'LXD', |
13 | diff --git a/cloudinit/settings.py b/cloudinit/settings.py | |||
14 | index b1ebaad..2060d81 100644 | |||
15 | --- a/cloudinit/settings.py | |||
16 | +++ b/cloudinit/settings.py | |||
17 | @@ -39,6 +39,7 @@ CFG_BUILTIN = { | |||
18 | 39 | 'Hetzner', | 39 | 'Hetzner', |
19 | 40 | 'IBMCloud', | 40 | 'IBMCloud', |
20 | 41 | 'Oracle', | 41 | 'Oracle', |
21 | 42 | 'Exoscale', | ||
22 | 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... |
23 | 43 | 'None', | 44 | 'None', |
24 | 44 | ], | 45 | ], |
25 | diff --git a/cloudinit/sources/DataSourceExoscale.py b/cloudinit/sources/DataSourceExoscale.py | |||
26 | 45 | new file mode 100644 | 46 | new file mode 100644 |
27 | index 0000000..52e7f6f | |||
28 | --- /dev/null | |||
29 | +++ b/cloudinit/sources/DataSourceExoscale.py | |||
30 | @@ -0,0 +1,258 @@ | |||
31 | 1 | # Author: Mathieu Corbin <mathieu.corbin@exoscale.com> | ||
32 | 2 | # Author: Christopher Glass <christopher.glass@exoscale.com> | ||
33 | 3 | # | ||
34 | 4 | # This file is part of cloud-init. See LICENSE file for license information. | ||
35 | 5 | |||
36 | 6 | from cloudinit import ec2_utils as ec2 | ||
37 | 7 | from cloudinit import log as logging | ||
38 | 8 | from cloudinit import sources | ||
39 | 9 | from cloudinit import url_helper | ||
40 | 10 | from cloudinit import util | ||
41 | 11 | |||
42 | 12 | LOG = logging.getLogger(__name__) | ||
43 | 13 | |||
44 | 14 | METADATA_URL = "http://169.254.169.254" | ||
45 | 15 | API_VERSION = "1.0" | ||
46 | 16 | PASSWORD_SERVER_PORT = 8080 | ||
47 | 17 | |||
48 | 18 | URL_TIMEOUT = 10 | ||
49 | 19 | URL_RETRIES = 6 | ||
50 | 20 | |||
51 | 21 | EXOSCALE_DMI_NAME = "Exoscale" | ||
52 | 22 | |||
53 | 23 | BUILTIN_DS_CONFIG = { | ||
54 | 24 | # We run the set password config module on every boot in order to enable | ||
55 | 25 | # resetting the instance's password via the exoscale console (and a | ||
56 | 26 | # subsequent instance reboot). | ||
57 | 27 | 'cloud_config_modules': [["set-passwords", "always"]] | ||
58 | 28 | } | ||
59 | 29 | |||
60 | 30 | |||
61 | 31 | class DataSourceExoscale(sources.DataSource): | ||
62 | 32 | |||
63 | 33 | dsname = 'Exoscale' | ||
64 | 34 | |||
65 | 35 | def __init__(self, sys_cfg, distro, paths): | ||
66 | 36 | super(DataSourceExoscale, self).__init__(sys_cfg, distro, paths) | ||
67 | 37 | LOG.debug("Initializing the Exoscale datasource") | ||
68 | 38 | |||
69 | 39 | self.metadata_url = self.ds_cfg.get('metadata_url', METADATA_URL) | ||
70 | 40 | self.api_version = self.ds_cfg.get('api_version', API_VERSION) | ||
71 | 41 | self.password_server_port = int( | ||
72 | 42 | self.ds_cfg.get('password_server_port', PASSWORD_SERVER_PORT)) | ||
73 | 43 | self.url_timeout = self.ds_cfg.get('timeout', URL_TIMEOUT) | ||
74 | 44 | self.url_retries = self.ds_cfg.get('retries', URL_RETRIES) | ||
75 | 45 | |||
76 | 46 | self.extra_config = BUILTIN_DS_CONFIG | ||
77 | 47 | |||
78 | 48 | def wait_for_metadata_service(self): | ||
79 | 49 | """Wait for the metadata service to be reachable.""" | ||
80 | 50 | |||
81 | 51 | metadata_url = "{}/{}/meta-data/instance-id".format( | ||
82 | 52 | self.metadata_url, self.api_version) | ||
83 | 53 | |||
84 | 54 | url = url_helper.wait_for_url( | ||
85 | 55 | urls=[metadata_url], | ||
86 | 56 | max_wait=self.url_max_wait, | ||
87 | 57 | timeout=self.url_timeout, | ||
88 | 58 | status_cb=LOG.critical) | ||
89 | 59 | |||
90 | 60 | return bool(url) | ||
91 | 61 | |||
92 | 62 | def crawl_metadata(self): | ||
93 | 63 | """ | ||
94 | 64 | Crawl the metadata service when available. | ||
95 | 65 | |||
96 | 66 | @returns: Dictionary of crawled metadata content. | ||
97 | 67 | """ | ||
98 | 68 | metadata_ready = util.log_time( | ||
99 | 69 | logfunc=LOG.info, | ||
100 | 70 | msg='waiting for the metadata service', | ||
101 | 71 | func=self.wait_for_metadata_service) | ||
102 | 72 | |||
103 | 73 | if not metadata_ready: | ||
104 | 74 | return {} | ||
105 | 75 | |||
106 | 76 | return read_metadata(self.metadata_url, self.api_version, | ||
107 | 77 | self.password_server_port, self.url_timeout, | ||
108 | 78 | self.url_retries) | ||
109 | 79 | |||
110 | 80 | def _get_data(self): | ||
111 | 81 | """Fetch the user data, the metadata and the VM password | ||
112 | 82 | from the metadata service. | ||
113 | 83 | |||
114 | 84 | Please refer to the datasource documentation for details on how the | ||
115 | 85 | metadata server and password server are crawled. | ||
116 | 86 | """ | ||
117 | 87 | if not self._is_platform_viable(): | ||
118 | 88 | return False | ||
119 | 89 | |||
120 | 90 | data = util.log_time( | ||
121 | 91 | logfunc=LOG.debug, | ||
122 | 92 | msg='Crawl of metadata service', | ||
123 | 93 | func=self.crawl_metadata) | ||
124 | 94 | |||
125 | 95 | if not data: | ||
126 | 96 | return False | ||
127 | 97 | |||
128 | 98 | self.userdata_raw = data['user-data'] | ||
129 | 99 | self.metadata = data['meta-data'] | ||
130 | 100 | password = data.get('password') | ||
131 | 101 | |||
132 | 102 | password_config = {} | ||
133 | 103 | if password: | ||
134 | 104 | # Since we have a password, let's make sure we are allowed to use | ||
135 | 105 | # it by allowing ssh_pwauth. | ||
136 | 106 | # The password module's default behavior is to leave the | ||
137 | 107 | # configuration as-is in this regard, so that means it will either | ||
138 | 108 | # leave the password always disabled if no password is ever set, or | ||
139 | 109 | # leave the password login enabled if we set it once. | ||
140 | 110 | password_config = { | ||
141 | 111 | 'ssh_pwauth': True, | ||
142 | 112 | 'password': password, | ||
143 | 113 | 'chpasswd': { | ||
144 | 114 | 'expire': False, | ||
145 | 115 | }, | ||
146 | 116 | } | ||
147 | 117 | |||
148 | 118 | # builtin extra_config overrides password_config | ||
149 | 119 | self.extra_config = util.mergemanydict( | ||
150 | 120 | [self.extra_config, password_config]) | ||
151 | 121 | |||
152 | 122 | return True | ||
153 | 123 | |||
154 | 124 | def get_config_obj(self): | ||
155 | 125 | return self.extra_config | ||
156 | 126 | |||
157 | 127 | def _is_platform_viable(self): | ||
158 | 128 | return util.read_dmi_data('system-product-name').startswith( | ||
159 | 129 | EXOSCALE_DMI_NAME) | ||
160 | 130 | |||
161 | 131 | |||
162 | 132 | # Used to match classes to dependencies | ||
163 | 133 | datasources = [ | ||
164 | 134 | (DataSourceExoscale, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), | ||
165 | 135 | ] | ||
166 | 136 | |||
167 | 137 | |||
168 | 138 | # Return a list of data sources that match this set of dependencies | ||
169 | 139 | def get_datasource_list(depends): | ||
170 | 140 | return sources.list_from_depends(depends, datasources) | ||
171 | 141 | |||
172 | 142 | |||
173 | 143 | def get_password(metadata_url=METADATA_URL, | ||
174 | 144 | api_version=API_VERSION, | ||
175 | 145 | password_server_port=PASSWORD_SERVER_PORT, | ||
176 | 146 | url_timeout=URL_TIMEOUT, | ||
177 | 147 | url_retries=URL_RETRIES): | ||
178 | 148 | """Obtain the VM's password if set. | ||
179 | 149 | |||
180 | 150 | Once fetched the password is marked saved. Future calls to this method may | ||
181 | 151 | return empty string or 'saved_password'.""" | ||
182 | 152 | password_url = "{}:{}/{}/".format(metadata_url, password_server_port, | ||
183 | 153 | api_version) | ||
184 | 154 | response = url_helper.read_file_or_url( | ||
185 | 155 | password_url, | ||
186 | 156 | ssl_details=None, | ||
187 | 157 | headers={"DomU_Request": "send_my_password"}, | ||
188 | 158 | timeout=url_timeout, | ||
189 | 159 | retries=url_retries) | ||
190 | 160 | password = response.contents.decode('utf-8') | ||
191 | 161 | # the password is empty or already saved | ||
192 | 162 | # Note: the original metadata server would answer an additional | ||
193 | 163 | # 'bad_request' status, but the Exoscale implementation does not. | ||
194 | 164 | if password in ['', 'saved_password']: | ||
195 | 165 | return None | ||
196 | 166 | # save the password | ||
197 | 167 | url_helper.read_file_or_url( | ||
198 | 168 | password_url, | ||
199 | 169 | ssl_details=None, | ||
200 | 170 | headers={"DomU_Request": "saved_password"}, | ||
201 | 171 | timeout=url_timeout, | ||
202 | 172 | retries=url_retries) | ||
203 | 173 | return password | ||
204 | 174 | |||
205 | 175 | |||
206 | 176 | def read_metadata(metadata_url=METADATA_URL, | ||
207 | 177 | api_version=API_VERSION, | ||
208 | 178 | password_server_port=PASSWORD_SERVER_PORT, | ||
209 | 179 | url_timeout=URL_TIMEOUT, | ||
210 | 180 | url_retries=URL_RETRIES): | ||
211 | 181 | """Query the metadata server and return the retrieved data.""" | ||
212 | 182 | crawled_metadata = {} | ||
213 | 183 | crawled_metadata['_metadata_api_version'] = api_version | ||
214 | 184 | try: | ||
215 | 185 | crawled_metadata['user-data'] = ec2.get_instance_userdata( | ||
216 | 186 | api_version, | ||
217 | 187 | metadata_url, | ||
218 | 188 | timeout=url_timeout, | ||
219 | 189 | retries=url_retries) | ||
220 | 190 | crawled_metadata['meta-data'] = ec2.get_instance_metadata( | ||
221 | 191 | api_version, | ||
222 | 192 | metadata_url, | ||
223 | 193 | timeout=url_timeout, | ||
224 | 194 | retries=url_retries) | ||
225 | 195 | except Exception as e: | ||
226 | 196 | util.logexc(LOG, "failed reading from metadata url %s (%s)", | ||
227 | 197 | metadata_url, e) | ||
228 | 198 | return {} | ||
229 | 199 | |||
230 | 200 | try: | ||
231 | 201 | crawled_metadata['password'] = get_password( | ||
232 | 202 | api_version=api_version, | ||
233 | 203 | metadata_url=metadata_url, | ||
234 | 204 | password_server_port=password_server_port, | ||
235 | 205 | url_retries=url_retries, | ||
236 | 206 | url_timeout=url_timeout) | ||
237 | 207 | except Exception as e: | ||
238 | 208 | util.logexc(LOG, "failed to read from password server url %s:%s (%s)", | ||
239 | 209 | metadata_url, password_server_port, e) | ||
240 | 210 | |||
241 | 211 | return crawled_metadata | ||
242 | 212 | |||
243 | 213 | |||
244 | 214 | if __name__ == "__main__": | ||
245 | 215 | import argparse | ||
246 | 216 | |||
247 | 217 | parser = argparse.ArgumentParser(description='Query Exoscale Metadata') | ||
248 | 218 | parser.add_argument( | ||
249 | 219 | "--endpoint", | ||
250 | 220 | metavar="URL", | ||
251 | 221 | help="The url of the metadata service.", | ||
252 | 222 | default=METADATA_URL) | ||
253 | 223 | parser.add_argument( | ||
254 | 224 | "--version", | ||
255 | 225 | metavar="VERSION", | ||
256 | 226 | help="The version of the metadata endpoint to query.", | ||
257 | 227 | default=API_VERSION) | ||
258 | 228 | parser.add_argument( | ||
259 | 229 | "--retries", | ||
260 | 230 | metavar="NUM", | ||
261 | 231 | type=int, | ||
262 | 232 | help="The number of retries querying the endpoint.", | ||
263 | 233 | default=URL_RETRIES) | ||
264 | 234 | parser.add_argument( | ||
265 | 235 | "--timeout", | ||
266 | 236 | metavar="NUM", | ||
267 | 237 | type=int, | ||
268 | 238 | help="The time in seconds to wait before timing out.", | ||
269 | 239 | default=URL_TIMEOUT) | ||
270 | 240 | parser.add_argument( | ||
271 | 241 | "--password-port", | ||
272 | 242 | metavar="PORT", | ||
273 | 243 | type=int, | ||
274 | 244 | help="The port on which the password endpoint listens", | ||
275 | 245 | default=PASSWORD_SERVER_PORT) | ||
276 | 246 | |||
277 | 247 | args = parser.parse_args() | ||
278 | 248 | |||
279 | 249 | data = read_metadata( | ||
280 | 250 | metadata_url=args.endpoint, | ||
281 | 251 | api_version=args.version, | ||
282 | 252 | password_server_port=args.password_port, | ||
283 | 253 | url_timeout=args.timeout, | ||
284 | 254 | url_retries=args.retries) | ||
285 | 255 | |||
286 | 256 | print(util.json_dumps(data)) | ||
287 | 257 | |||
288 | 258 | # vi: ts=4 expandtab | ||
289 | diff --git a/doc/rtd/topics/datasources.rst b/doc/rtd/topics/datasources.rst | |||
290 | index 648c606..2148cd5 100644 | |||
291 | --- a/doc/rtd/topics/datasources.rst | |||
292 | +++ b/doc/rtd/topics/datasources.rst | |||
293 | @@ -155,6 +155,7 @@ Follow for more information. | |||
294 | 155 | datasources/configdrive.rst | 155 | datasources/configdrive.rst |
295 | 156 | datasources/digitalocean.rst | 156 | datasources/digitalocean.rst |
296 | 157 | datasources/ec2.rst | 157 | datasources/ec2.rst |
297 | 158 | datasources/exoscale.rst | ||
298 | 158 | datasources/maas.rst | 159 | datasources/maas.rst |
299 | 159 | datasources/nocloud.rst | 160 | datasources/nocloud.rst |
300 | 160 | datasources/opennebula.rst | 161 | datasources/opennebula.rst |
301 | diff --git a/doc/rtd/topics/datasources/exoscale.rst b/doc/rtd/topics/datasources/exoscale.rst | |||
302 | 161 | new file mode 100644 | 162 | new file mode 100644 |
303 | index 0000000..27aec9c | |||
304 | --- /dev/null | |||
305 | +++ b/doc/rtd/topics/datasources/exoscale.rst | |||
306 | @@ -0,0 +1,68 @@ | |||
307 | 1 | .. _datasource_exoscale: | ||
308 | 2 | |||
309 | 3 | Exoscale | ||
310 | 4 | ======== | ||
311 | 5 | |||
312 | 6 | This datasource supports reading from the metadata server used on the | ||
313 | 7 | `Exoscale platform <https://exoscale.com>`_. | ||
314 | 8 | |||
315 | 9 | Use of the Exoscale datasource is recommended to benefit from new features of | ||
316 | 10 | the Exoscale platform. | ||
317 | 11 | |||
318 | 12 | The datasource relies on the availability of a compatible metadata server | ||
319 | 13 | (``http://169.254.169.254`` is used by default) and its companion password | ||
320 | 14 | server, reachable at the same address (by default on port 8080). | ||
321 | 15 | |||
322 | 16 | Crawling of metadata | ||
323 | 17 | -------------------- | ||
324 | 18 | |||
325 | 19 | The metadata service and password server are crawled slightly differently: | ||
326 | 20 | |||
327 | 21 | * The "metadata service" is crawled every boot. | ||
328 | 22 | * The password server is also crawled every boot (the Exoscale datasource | ||
329 | 23 | forces the password module to run with "frequency always"). | ||
330 | 24 | |||
331 | 25 | In the password server case, the following rules apply in order to enable the | ||
332 | 26 | "restore instance password" functionality: | ||
333 | 27 | |||
334 | 28 | * If a password is returned by the password server, it is then marked "saved" | ||
335 | 29 | by the cloud-init datasource. Subsequent boots will skip setting the password | ||
336 | 30 | (the password server will return "saved_password"). | ||
337 | 31 | * When the instance password is reset (via the Exoscale UI), the password | ||
338 | 32 | server will return the non-empty password at next boot, therefore causing | ||
339 | 33 | cloud-init to reset the instance's password. | ||
340 | 34 | |||
341 | 35 | Configuration | ||
342 | 36 | ------------- | ||
343 | 37 | |||
344 | 38 | Users of this datasource are discouraged from changing the default settings | ||
345 | 39 | unless instructed to by Exoscale support. | ||
346 | 40 | |||
347 | 41 | The following settings are available and can be set for the datasource in system | ||
348 | 42 | configuration (in `/etc/cloud/cloud.cfg.d/`). | ||
349 | 43 | |||
350 | 44 | The settings available are: | ||
351 | 45 | |||
352 | 46 | * **metadata_url**: The URL for the metadata service (defaults to | ||
353 | 47 | ``http://169.254.169.254``) | ||
354 | 48 | * **api_version**: The API version path on which to query the instance metadata | ||
355 | 49 | (defaults to ``1.0``) | ||
356 | 50 | * **password_server_port**: The port (on the metadata server) on which the | ||
357 | 51 | password server listens (defaults to ``8080``). | ||
358 | 52 | * **timeout**: the timeout value provided to urlopen for each individual http | ||
359 | 53 | request. (defaults to ``10``) | ||
360 | 54 | * **retries**: The number of retries that should be done for an http request | ||
361 | 55 | (defaults to ``6``) | ||
362 | 56 | |||
363 | 57 | |||
364 | 58 | An example configuration with the default values is provided below: | ||
365 | 59 | |||
366 | 60 | .. sourcecode:: yaml | ||
367 | 61 | |||
368 | 62 | datasource: | ||
369 | 63 | Exoscale: | ||
370 | 64 | metadata_url: "http://169.254.169.254" | ||
371 | 65 | api_version: "1.0" | ||
372 | 66 | password_server_port: 8080 | ||
373 | 67 | timeout: 10 | ||
374 | 68 | retries: 6 | ||
375 | diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/test_datasource/test_common.py | |||
376 | index 2a9cfb2..61a7a76 100644 | |||
377 | --- a/tests/unittests/test_datasource/test_common.py | |||
378 | +++ b/tests/unittests/test_datasource/test_common.py | |||
379 | @@ -13,6 +13,7 @@ from cloudinit.sources import ( | |||
380 | 13 | DataSourceConfigDrive as ConfigDrive, | 13 | DataSourceConfigDrive as ConfigDrive, |
381 | 14 | DataSourceDigitalOcean as DigitalOcean, | 14 | DataSourceDigitalOcean as DigitalOcean, |
382 | 15 | DataSourceEc2 as Ec2, | 15 | DataSourceEc2 as Ec2, |
383 | 16 | DataSourceExoscale as Exoscale, | ||
384 | 16 | DataSourceGCE as GCE, | 17 | DataSourceGCE as GCE, |
385 | 17 | DataSourceHetzner as Hetzner, | 18 | DataSourceHetzner as Hetzner, |
386 | 18 | DataSourceIBMCloud as IBMCloud, | 19 | DataSourceIBMCloud as IBMCloud, |
387 | @@ -53,6 +54,7 @@ DEFAULT_NETWORK = [ | |||
388 | 53 | CloudStack.DataSourceCloudStack, | 54 | CloudStack.DataSourceCloudStack, |
389 | 54 | DSNone.DataSourceNone, | 55 | DSNone.DataSourceNone, |
390 | 55 | Ec2.DataSourceEc2, | 56 | Ec2.DataSourceEc2, |
391 | 57 | Exoscale.DataSourceExoscale, | ||
392 | 56 | GCE.DataSourceGCE, | 58 | GCE.DataSourceGCE, |
393 | 57 | MAAS.DataSourceMAAS, | 59 | MAAS.DataSourceMAAS, |
394 | 58 | NoCloud.DataSourceNoCloudNet, | 60 | NoCloud.DataSourceNoCloudNet, |
395 | diff --git a/tests/unittests/test_datasource/test_exoscale.py b/tests/unittests/test_datasource/test_exoscale.py | |||
396 | 59 | new file mode 100644 | 61 | new file mode 100644 |
397 | index 0000000..350c330 | |||
398 | --- /dev/null | |||
399 | +++ b/tests/unittests/test_datasource/test_exoscale.py | |||
400 | @@ -0,0 +1,203 @@ | |||
401 | 1 | # Author: Mathieu Corbin <mathieu.corbin@exoscale.com> | ||
402 | 2 | # Author: Christopher Glass <christopher.glass@exoscale.com> | ||
403 | 3 | # | ||
404 | 4 | # This file is part of cloud-init. See LICENSE file for license information. | ||
405 | 5 | from cloudinit import helpers | ||
406 | 6 | from cloudinit.sources.DataSourceExoscale import ( | ||
407 | 7 | API_VERSION, | ||
408 | 8 | DataSourceExoscale, | ||
409 | 9 | METADATA_URL, | ||
410 | 10 | get_password, | ||
411 | 11 | PASSWORD_SERVER_PORT, | ||
412 | 12 | read_metadata) | ||
413 | 13 | from cloudinit.tests.helpers import HttprettyTestCase, mock | ||
414 | 14 | |||
415 | 15 | import httpretty | ||
416 | 16 | import requests | ||
417 | 17 | |||
418 | 18 | |||
419 | 19 | TEST_PASSWORD_URL = "{}:{}/{}/".format(METADATA_URL, | ||
420 | 20 | PASSWORD_SERVER_PORT, | ||
421 | 21 | API_VERSION) | ||
422 | 22 | |||
423 | 23 | TEST_METADATA_URL = "{}/{}/meta-data/".format(METADATA_URL, | ||
424 | 24 | API_VERSION) | ||
425 | 25 | |||
426 | 26 | TEST_USERDATA_URL = "{}/{}/user-data".format(METADATA_URL, | ||
427 | 27 | API_VERSION) | ||
428 | 28 | |||
429 | 29 | |||
430 | 30 | @httpretty.activate | ||
431 | 31 | class TestDatasourceExoscale(HttprettyTestCase): | ||
432 | 32 | |||
433 | 33 | def setUp(self): | ||
434 | 34 | super(TestDatasourceExoscale, self).setUp() | ||
435 | 35 | self.tmp = self.tmp_dir() | ||
436 | 36 | self.password_url = TEST_PASSWORD_URL | ||
437 | 37 | self.metadata_url = TEST_METADATA_URL | ||
438 | 38 | self.userdata_url = TEST_USERDATA_URL | ||
439 | 39 | |||
440 | 40 | def test_password_saved(self): | ||
441 | 41 | """The password is not set when it is not found | ||
442 | 42 | in the metadata service.""" | ||
443 | 43 | httpretty.register_uri(httpretty.GET, | ||
444 | 44 | self.password_url, | ||
445 | 45 | body="saved_password") | ||
446 | 46 | self.assertFalse(get_password()) | ||
447 | 47 | |||
448 | 48 | def test_password_empty(self): | ||
449 | 49 | """No password is set if the metadata service returns | ||
450 | 50 | an empty string.""" | ||
451 | 51 | httpretty.register_uri(httpretty.GET, | ||
452 | 52 | self.password_url, | ||
453 | 53 | body="") | ||
454 | 54 | self.assertFalse(get_password()) | ||
455 | 55 | |||
456 | 56 | def test_password(self): | ||
457 | 57 | """The password is set to what is found in the metadata | ||
458 | 58 | service.""" | ||
459 | 59 | expected_password = "p@ssw0rd" | ||
460 | 60 | httpretty.register_uri(httpretty.GET, | ||
461 | 61 | self.password_url, | ||
462 | 62 | body=expected_password) | ||
463 | 63 | password = get_password() | ||
464 | 64 | self.assertEqual(expected_password, password) | ||
465 | 65 | |||
466 | 66 | def test_get_data(self): | ||
467 | 67 | """The datasource conforms to expected behavior when supplied | ||
468 | 68 | full test data.""" | ||
469 | 69 | path = helpers.Paths({'run_dir': self.tmp}) | ||
470 | 70 | ds = DataSourceExoscale({}, None, path) | ||
471 | 71 | ds._is_platform_viable = lambda: True | ||
472 | 72 | expected_password = "p@ssw0rd" | ||
473 | 73 | expected_id = "12345" | ||
474 | 74 | expected_hostname = "myname" | ||
475 | 75 | expected_userdata = "#cloud-config" | ||
476 | 76 | httpretty.register_uri(httpretty.GET, | ||
477 | 77 | self.userdata_url, | ||
478 | 78 | body=expected_userdata) | ||
479 | 79 | httpretty.register_uri(httpretty.GET, | ||
480 | 80 | self.password_url, | ||
481 | 81 | body=expected_password) | ||
482 | 82 | httpretty.register_uri(httpretty.GET, | ||
483 | 83 | self.metadata_url, | ||
484 | 84 | body="instance-id\nlocal-hostname") | ||
485 | 85 | httpretty.register_uri(httpretty.GET, | ||
486 | 86 | "{}local-hostname".format(self.metadata_url), | ||
487 | 87 | body=expected_hostname) | ||
488 | 88 | httpretty.register_uri(httpretty.GET, | ||
489 | 89 | "{}instance-id".format(self.metadata_url), | ||
490 | 90 | body=expected_id) | ||
491 | 91 | self.assertTrue(ds._get_data()) | ||
492 | 92 | self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config") | ||
493 | 93 | self.assertEqual(ds.metadata, {"instance-id": expected_id, | ||
494 | 94 | "local-hostname": expected_hostname}) | ||
495 | 95 | self.assertEqual(ds.get_config_obj(), | ||
496 | 96 | {'ssh_pwauth': True, | ||
497 | 97 | 'password': expected_password, | ||
498 | 98 | 'cloud_config_modules': [ | ||
499 | 99 | ["set-passwords", "always"]], | ||
500 | 100 | 'chpasswd': { | ||
501 | 101 | 'expire': False, | ||
502 | 102 | }}) | ||
503 | 103 | |||
504 | 104 | def test_get_data_saved_password(self): | ||
505 | 105 | """The datasource conforms to expected behavior when saved_password is | ||
506 | 106 | returned by the password server.""" | ||
507 | 107 | path = helpers.Paths({'run_dir': self.tmp}) | ||
508 | 108 | ds = DataSourceExoscale({}, None, path) | ||
509 | 109 | ds._is_platform_viable = lambda: True | ||
510 | 110 | expected_answer = "saved_password" | ||
511 | 111 | expected_id = "12345" | ||
512 | 112 | expected_hostname = "myname" | ||
513 | 113 | expected_userdata = "#cloud-config" | ||
514 | 114 | httpretty.register_uri(httpretty.GET, | ||
515 | 115 | self.userdata_url, | ||
516 | 116 | body=expected_userdata) | ||
517 | 117 | httpretty.register_uri(httpretty.GET, | ||
518 | 118 | self.password_url, | ||
519 | 119 | body=expected_answer) | ||
520 | 120 | httpretty.register_uri(httpretty.GET, | ||
521 | 121 | self.metadata_url, | ||
522 | 122 | body="instance-id\nlocal-hostname") | ||
523 | 123 | httpretty.register_uri(httpretty.GET, | ||
524 | 124 | "{}local-hostname".format(self.metadata_url), | ||
525 | 125 | body=expected_hostname) | ||
526 | 126 | httpretty.register_uri(httpretty.GET, | ||
527 | 127 | "{}instance-id".format(self.metadata_url), | ||
528 | 128 | body=expected_id) | ||
529 | 129 | self.assertTrue(ds._get_data()) | ||
530 | 130 | self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config") | ||
531 | 131 | self.assertEqual(ds.metadata, {"instance-id": expected_id, | ||
532 | 132 | "local-hostname": expected_hostname}) | ||
533 | 133 | self.assertEqual(ds.get_config_obj(), | ||
534 | 134 | {'cloud_config_modules': [ | ||
535 | 135 | ["set-passwords", "always"]]}) | ||
536 | 136 | |||
537 | 137 | def test_get_data_no_password(self): | ||
538 | 138 | """The datasource conforms to expected behavior when no password is | ||
539 | 139 | returned by the password server.""" | ||
540 | 140 | path = helpers.Paths({'run_dir': self.tmp}) | ||
541 | 141 | ds = DataSourceExoscale({}, None, path) | ||
542 | 142 | ds._is_platform_viable = lambda: True | ||
543 | 143 | expected_answer = "" | ||
544 | 144 | expected_id = "12345" | ||
545 | 145 | expected_hostname = "myname" | ||
546 | 146 | expected_userdata = "#cloud-config" | ||
547 | 147 | httpretty.register_uri(httpretty.GET, | ||
548 | 148 | self.userdata_url, | ||
549 | 149 | body=expected_userdata) | ||
550 | 150 | httpretty.register_uri(httpretty.GET, | ||
551 | 151 | self.password_url, | ||
552 | 152 | body=expected_answer) | ||
553 | 153 | httpretty.register_uri(httpretty.GET, | ||
554 | 154 | self.metadata_url, | ||
555 | 155 | body="instance-id\nlocal-hostname") | ||
556 | 156 | httpretty.register_uri(httpretty.GET, | ||
557 | 157 | "{}local-hostname".format(self.metadata_url), | ||
558 | 158 | body=expected_hostname) | ||
559 | 159 | httpretty.register_uri(httpretty.GET, | ||
560 | 160 | "{}instance-id".format(self.metadata_url), | ||
561 | 161 | body=expected_id) | ||
562 | 162 | self.assertTrue(ds._get_data()) | ||
563 | 163 | self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config") | ||
564 | 164 | self.assertEqual(ds.metadata, {"instance-id": expected_id, | ||
565 | 165 | "local-hostname": expected_hostname}) | ||
566 | 166 | self.assertEqual(ds.get_config_obj(), | ||
567 | 167 | {'cloud_config_modules': [ | ||
568 | 168 | ["set-passwords", "always"]]}) | ||
569 | 169 | |||
570 | 170 | @mock.patch('cloudinit.sources.DataSourceExoscale.get_password') | ||
571 | 171 | def test_read_metadata_when_password_server_unreachable(self, m_password): | ||
572 | 172 | """The read_metadata function returns partial results in case the | ||
573 | 173 | password server (only) is unreachable.""" | ||
574 | 174 | expected_id = "12345" | ||
575 | 175 | expected_hostname = "myname" | ||
576 | 176 | expected_userdata = "#cloud-config" | ||
577 | 177 | |||
578 | 178 | m_password.side_effect = requests.Timeout('Fake Connection Timeout') | ||
579 | 179 | httpretty.register_uri(httpretty.GET, | ||
580 | 180 | self.userdata_url, | ||
581 | 181 | body=expected_userdata) | ||
582 | 182 | httpretty.register_uri(httpretty.GET, | ||
583 | 183 | self.metadata_url, | ||
584 | 184 | body="instance-id\nlocal-hostname") | ||
585 | 185 | httpretty.register_uri(httpretty.GET, | ||
586 | 186 | "{}local-hostname".format(self.metadata_url), | ||
587 | 187 | body=expected_hostname) | ||
588 | 188 | httpretty.register_uri(httpretty.GET, | ||
589 | 189 | "{}instance-id".format(self.metadata_url), | ||
590 | 190 | body=expected_id) | ||
591 | 191 | |||
592 | 192 | result = read_metadata() | ||
593 | 193 | |||
594 | 194 | self.assertIsNone(result.get("password")) | ||
595 | 195 | self.assertEqual(result.get("user-data").decode("utf-8"), | ||
596 | 196 | expected_userdata) | ||
597 | 197 | |||
598 | 198 | def test_non_viable_platform(self): | ||
599 | 199 | """The datasource fails fast when the platform is not viable.""" | ||
600 | 200 | path = helpers.Paths({'run_dir': self.tmp}) | ||
601 | 201 | ds = DataSourceExoscale({}, None, path) | ||
602 | 202 | ds._is_platform_viable = lambda: False | ||
603 | 203 | self.assertFalse(ds._get_data()) | ||
604 | diff --git a/tools/ds-identify b/tools/ds-identify | |||
605 | index 0305e36..e0d4865 100755 | |||
606 | --- a/tools/ds-identify | |||
607 | +++ b/tools/ds-identify | |||
608 | @@ -124,7 +124,7 @@ DI_DSNAME="" | |||
609 | 124 | # be searched if there is no setting found in config. | 124 | # be searched if there is no setting found in config. |
610 | 125 | DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \ | 125 | DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \ |
611 | 126 | CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \ | 126 | CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \ |
613 | 127 | OVF SmartOS Scaleway Hetzner IBMCloud Oracle" | 127 | OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale" |
614 | 128 | DI_DSLIST="" | 128 | DI_DSLIST="" |
615 | 129 | DI_MODE="" | 129 | DI_MODE="" |
616 | 130 | DI_ON_FOUND="" | 130 | DI_ON_FOUND="" |
617 | @@ -553,6 +553,11 @@ dscheck_CloudStack() { | |||
618 | 553 | return $DS_NOT_FOUND | 553 | return $DS_NOT_FOUND |
619 | 554 | } | 554 | } |
620 | 555 | 555 | ||
621 | 556 | dscheck_Exoscale() { | ||
622 | 557 | dmi_product_name_matches "Exoscale*" && return $DS_FOUND | ||
623 | 558 | return $DS_NOT_FOUND | ||
624 | 559 | } | ||
625 | 560 | |||
626 | 556 | dscheck_CloudSigma() { | 561 | dscheck_CloudSigma() { |
627 | 557 | # http://paste.ubuntu.com/23624795/ | 562 | # http://paste.ubuntu.com/23624795/ |
628 | 558 | dmi_product_name_matches "CloudSigma" && return $DS_FOUND | 563 | dmi_product_name_matches "CloudSigma" && return $DS_FOUND |
FAILED: Continuous integration, rev:7279a5570bc 87909022d9f849a c609d7acdbb3ca /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 751/
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
FAILED: Ubuntu LTS: Integration
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 751/rebuild
https:/