Merge lp:~kiril-vladimiroff/cloud-init/cloudsigma-data-source into lp:~cloud-init-dev/cloud-init/trunk
- cloudsigma-data-source
- Merge into trunk
Proposed by
Kiril Vladimiroff
Status: | Merged |
---|---|
Merged at revision: | 941 |
Proposed branch: | lp:~kiril-vladimiroff/cloud-init/cloudsigma-data-source |
Merge into: | lp:~cloud-init-dev/cloud-init/trunk |
Diff against target: |
400 lines (+351/-2) 7 files modified
cloudinit/cs_utils.py (+99/-0) cloudinit/settings.py (+1/-0) cloudinit/sources/DataSourceCloudSigma.py (+91/-0) doc/sources/cloudsigma/README.rst (+34/-0) requirements.txt (+2/-2) tests/unittests/test_cs_util.py (+65/-0) tests/unittests/test_datasource/test_cloudsigma.py (+59/-0) |
To merge this branch: | bzr merge lp:~kiril-vladimiroff/cloud-init/cloudsigma-data-source |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
cloud-init Commiters | Pending | ||
Review via email:
|
Commit message
Description of the change
Add support for the CloudSigma server context.
To post a comment you must log in.
- 940. By Scott Moser
-
Add 'unverified_
modules' config option and skip unverified modules Config modules are able to declare distros that they were verified
to run on by setting 'distros' as a list in the config module.Previously, if a module was configured to run and the running distro was not
listed as supported, it would run anyway, and a warning would be written.Now, we change the behavior to skip those modules.
The distro (or user) can specify that a given list of modules should run anyway
by declaring the 'unverified_modules' config variable. run_once modules will be run without this filter (ie, expecting that the user
explicitly wanted to run it). - 941. By Kiril Vladimiroff
-
Add support for the CloudSigma server context.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'cloudinit/cs_utils.py' |
2 | --- cloudinit/cs_utils.py 1970-01-01 00:00:00 +0000 |
3 | +++ cloudinit/cs_utils.py 2014-02-12 10:49:46 +0000 |
4 | @@ -0,0 +1,99 @@ |
5 | +# vi: ts=4 expandtab |
6 | +# |
7 | +# Copyright (C) 2014 CloudSigma |
8 | +# |
9 | +# Author: Kiril Vladimiroff <kiril.vladimiroff@cloudsigma.com> |
10 | +# |
11 | +# This program is free software: you can redistribute it and/or modify |
12 | +# it under the terms of the GNU General Public License version 3, as |
13 | +# published by the Free Software Foundation. |
14 | +# |
15 | +# This program is distributed in the hope that it will be useful, |
16 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
17 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
18 | +# GNU General Public License for more details. |
19 | +# |
20 | +# You should have received a copy of the GNU General Public License |
21 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
22 | +""" |
23 | +cepko implements easy-to-use communication with CloudSigma's VMs through |
24 | +a virtual serial port without bothering with formatting the messages |
25 | +properly nor parsing the output with the specific and sometimes |
26 | +confusing shell tools for that purpose. |
27 | + |
28 | +Having the server definition accessible by the VM can ve useful in various |
29 | +ways. For example it is possible to easily determine from within the VM, |
30 | +which network interfaces are connected to public and which to private network. |
31 | +Another use is to pass some data to initial VM setup scripts, like setting the |
32 | +hostname to the VM name or passing ssh public keys through server meta. |
33 | + |
34 | +For more information take a look at the Server Context section of CloudSigma |
35 | +API Docs: http://cloudsigma-docs.readthedocs.org/en/latest/server_context.html |
36 | +""" |
37 | +import json |
38 | +import platform |
39 | + |
40 | +import serial |
41 | + |
42 | +SERIAL_PORT = '/dev/ttyS1' |
43 | +if platform.system() == 'Windows': |
44 | + SERIAL_PORT = 'COM2' |
45 | + |
46 | + |
47 | +class Cepko(object): |
48 | + """ |
49 | + One instance of that object could be use for one or more |
50 | + queries to the serial port. |
51 | + """ |
52 | + request_pattern = "<\n{}\n>" |
53 | + |
54 | + def get(self, key="", request_pattern=None): |
55 | + if request_pattern is None: |
56 | + request_pattern = self.request_pattern |
57 | + return CepkoResult(request_pattern.format(key)) |
58 | + |
59 | + def all(self): |
60 | + return self.get() |
61 | + |
62 | + def meta(self, key=""): |
63 | + request_pattern = self.request_pattern.format("/meta/{}") |
64 | + return self.get(key, request_pattern) |
65 | + |
66 | + def global_context(self, key=""): |
67 | + request_pattern = self.request_pattern.format("/global_context/{}") |
68 | + return self.get(key, request_pattern) |
69 | + |
70 | + |
71 | +class CepkoResult(object): |
72 | + """ |
73 | + CepkoResult executes the request to the virtual serial port as soon |
74 | + as the instance is initialized and stores the result in both raw and |
75 | + marshalled format. |
76 | + """ |
77 | + def __init__(self, request): |
78 | + self.request = request |
79 | + self.raw_result = self._execute() |
80 | + self.result = self._marshal(self.raw_result) |
81 | + |
82 | + def _execute(self): |
83 | + connection = serial.Serial(SERIAL_PORT) |
84 | + connection.write(self.request) |
85 | + return connection.readline().strip('\x04\n') |
86 | + |
87 | + def _marshal(self, raw_result): |
88 | + try: |
89 | + return json.loads(raw_result) |
90 | + except ValueError: |
91 | + return raw_result |
92 | + |
93 | + def __len__(self): |
94 | + return self.result.__len__() |
95 | + |
96 | + def __getitem__(self, key): |
97 | + return self.result.__getitem__(key) |
98 | + |
99 | + def __contains__(self, item): |
100 | + return self.result.__contains__(item) |
101 | + |
102 | + def __iter__(self): |
103 | + return self.result.__iter__() |
104 | |
105 | === modified file 'cloudinit/settings.py' |
106 | --- cloudinit/settings.py 2014-01-16 21:53:21 +0000 |
107 | +++ cloudinit/settings.py 2014-02-12 10:49:46 +0000 |
108 | @@ -37,6 +37,7 @@ |
109 | 'OVF', |
110 | 'MAAS', |
111 | 'Ec2', |
112 | + 'CloudSigma', |
113 | 'CloudStack', |
114 | 'SmartOS', |
115 | # At the end to act as a 'catch' when none of the above work... |
116 | |
117 | === added file 'cloudinit/sources/DataSourceCloudSigma.py' |
118 | --- cloudinit/sources/DataSourceCloudSigma.py 1970-01-01 00:00:00 +0000 |
119 | +++ cloudinit/sources/DataSourceCloudSigma.py 2014-02-12 10:49:46 +0000 |
120 | @@ -0,0 +1,91 @@ |
121 | +# vi: ts=4 expandtab |
122 | +# |
123 | +# Copyright (C) 2014 CloudSigma |
124 | +# |
125 | +# Author: Kiril Vladimiroff <kiril.vladimiroff@cloudsigma.com> |
126 | +# |
127 | +# This program is free software: you can redistribute it and/or modify |
128 | +# it under the terms of the GNU General Public License version 3, as |
129 | +# published by the Free Software Foundation. |
130 | +# |
131 | +# This program is distributed in the hope that it will be useful, |
132 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
133 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
134 | +# GNU General Public License for more details. |
135 | +# |
136 | +# You should have received a copy of the GNU General Public License |
137 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
138 | +import re |
139 | + |
140 | +from cloudinit import log as logging |
141 | +from cloudinit import sources |
142 | +from cloudinit import util |
143 | +from cloudinit.cs_utils import Cepko |
144 | + |
145 | +LOG = logging.getLogger(__name__) |
146 | + |
147 | +VALID_DSMODES = ("local", "net", "disabled") |
148 | + |
149 | + |
150 | +class DataSourceCloudSigma(sources.DataSource): |
151 | + """ |
152 | + Uses cepko in order to gather the server context from the VM. |
153 | + |
154 | + For more information about CloudSigma's Server Context: |
155 | + http://cloudsigma-docs.readthedocs.org/en/latest/server_context.html |
156 | + """ |
157 | + def __init__(self, sys_cfg, distro, paths): |
158 | + self.dsmode = 'local' |
159 | + self.cepko = Cepko() |
160 | + self.ssh_public_key = '' |
161 | + sources.DataSource.__init__(self, sys_cfg, distro, paths) |
162 | + |
163 | + def get_data(self): |
164 | + """ |
165 | + Metadata is the whole server context and /meta/cloud-config is used |
166 | + as userdata. |
167 | + """ |
168 | + try: |
169 | + server_context = self.cepko.all().result |
170 | + server_meta = server_context['meta'] |
171 | + self.userdata_raw = server_meta.get('cloudinit-user-data', "") |
172 | + self.metadata = server_context |
173 | + self.ssh_public_key = server_meta['ssh_public_key'] |
174 | + |
175 | + if server_meta.get('cloudinit-dsmode') in VALID_DSMODES: |
176 | + self.dsmode = server_meta['cloudinit-dsmode'] |
177 | + except: |
178 | + util.logexc(LOG, "Failed reading from the serial port") |
179 | + return False |
180 | + return True |
181 | + |
182 | + def get_hostname(self, fqdn=False, resolve_ip=False): |
183 | + """ |
184 | + Cleans up and uses the server's name if the latter is set. Otherwise |
185 | + the first part from uuid is being used. |
186 | + """ |
187 | + if re.match(r'^[A-Za-z0-9 -_\.]+$', self.metadata['name']): |
188 | + return self.metadata['name'][:61] |
189 | + else: |
190 | + return self.metadata['uuid'].split('-')[0] |
191 | + |
192 | + def get_public_ssh_keys(self): |
193 | + return [self.ssh_public_key] |
194 | + |
195 | + def get_instance_id(self): |
196 | + return self.metadata['uuid'] |
197 | + |
198 | + |
199 | +# Used to match classes to dependencies. Since this datasource uses the serial |
200 | +# port network is not really required, so it's okay to load without it, too. |
201 | +datasources = [ |
202 | + (DataSourceCloudSigma, (sources.DEP_FILESYSTEM)), |
203 | + (DataSourceCloudSigma, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), |
204 | +] |
205 | + |
206 | + |
207 | +def get_datasource_list(depends): |
208 | + """ |
209 | + Return a list of data sources that match this set of dependencies |
210 | + """ |
211 | + return sources.list_from_depends(depends, datasources) |
212 | |
213 | === added directory 'doc/sources/cloudsigma' |
214 | === added file 'doc/sources/cloudsigma/README.rst' |
215 | --- doc/sources/cloudsigma/README.rst 1970-01-01 00:00:00 +0000 |
216 | +++ doc/sources/cloudsigma/README.rst 2014-02-12 10:49:46 +0000 |
217 | @@ -0,0 +1,34 @@ |
218 | +===================== |
219 | +CloudSigma Datasource |
220 | +===================== |
221 | + |
222 | +This datasource finds metadata and user-data from the `CloudSigma`_ cloud platform. |
223 | +Data transfer occurs through a virtual serial port of the `CloudSigma`_'s VM and the |
224 | +presence of network adapter is **NOT** a requirement, |
225 | + |
226 | + See `server context`_ in the public documentation for more information. |
227 | + |
228 | + |
229 | +Setting a hostname |
230 | +~~~~~~~~~~~~~~~~~~ |
231 | + |
232 | +By default the name of the server will be applied as a hostname on the first boot. |
233 | + |
234 | + |
235 | +Providing user-data |
236 | +~~~~~~~~~~~~~~~~~~~ |
237 | + |
238 | +You can provide user-data to the VM using the dedicated `meta field`_ in the `server context`_ |
239 | +``cloudinit-user-data``. By default *cloud-config* format is expected there and the ``#cloud-config`` |
240 | +header could be omitted. However since this is a raw-text field you could provide any of the valid |
241 | +`config formats`_. |
242 | + |
243 | +If your user-data needs an internet connection you have to create a `meta field`_ in the `server context`_ |
244 | +``cloudinit-dsmode`` and set "net" as value. If this field does not exist the default value is "local". |
245 | + |
246 | + |
247 | + |
248 | +.. _CloudSigma: http://cloudsigma.com/ |
249 | +.. _server context: http://cloudsigma-docs.readthedocs.org/en/latest/server_context.html |
250 | +.. _meta field: http://cloudsigma-docs.readthedocs.org/en/latest/meta.html |
251 | +.. _config formats: http://cloudinit.readthedocs.org/en/latest/topics/format.html |
252 | |
253 | === modified file 'requirements.txt' |
254 | --- requirements.txt 2014-01-18 07:46:19 +0000 |
255 | +++ requirements.txt 2014-02-12 10:49:46 +0000 |
256 | @@ -10,8 +10,8 @@ |
257 | # datasource is removed, this is no longer needed |
258 | oauth |
259 | |
260 | -# This one is currently used only by the SmartOS datasource. If that |
261 | -# datasource is removed, this is no longer needed |
262 | +# This one is currently used only by the CloudSigma and SmartOS datasources. |
263 | +# If these datasources are removed, this is no longer needed |
264 | pyserial |
265 | |
266 | # This is only needed for places where we need to support configs in a manner |
267 | |
268 | === added file 'tests/unittests/test_cs_util.py' |
269 | --- tests/unittests/test_cs_util.py 1970-01-01 00:00:00 +0000 |
270 | +++ tests/unittests/test_cs_util.py 2014-02-12 10:49:46 +0000 |
271 | @@ -0,0 +1,65 @@ |
272 | +from mocker import MockerTestCase |
273 | + |
274 | +from cloudinit.cs_utils import Cepko |
275 | + |
276 | + |
277 | +SERVER_CONTEXT = { |
278 | + "cpu": 1000, |
279 | + "cpus_instead_of_cores": False, |
280 | + "global_context": {"some_global_key": "some_global_val"}, |
281 | + "mem": 1073741824, |
282 | + "meta": {"ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2E.../hQ5D5 john@doe"}, |
283 | + "name": "test_server", |
284 | + "requirements": [], |
285 | + "smp": 1, |
286 | + "tags": ["much server", "very performance"], |
287 | + "uuid": "65b2fb23-8c03-4187-a3ba-8b7c919e889", |
288 | + "vnc_password": "9e84d6cb49e46379" |
289 | +} |
290 | + |
291 | + |
292 | +class CepkoMock(Cepko): |
293 | + def all(self): |
294 | + return SERVER_CONTEXT |
295 | + |
296 | + def get(self, key="", request_pattern=None): |
297 | + return SERVER_CONTEXT['tags'] |
298 | + |
299 | + |
300 | +class CepkoResultTests(MockerTestCase): |
301 | + def setUp(self): |
302 | + self.mocked = self.mocker.replace("cloudinit.cs_utils.Cepko", |
303 | + spec=CepkoMock, |
304 | + count=False, |
305 | + passthrough=False) |
306 | + self.mocked() |
307 | + self.mocker.result(CepkoMock()) |
308 | + self.mocker.replay() |
309 | + self.c = Cepko() |
310 | + |
311 | + def test_getitem(self): |
312 | + result = self.c.all() |
313 | + self.assertEqual("65b2fb23-8c03-4187-a3ba-8b7c919e889", result['uuid']) |
314 | + self.assertEqual([], result['requirements']) |
315 | + self.assertEqual("much server", result['tags'][0]) |
316 | + self.assertEqual(1, result['smp']) |
317 | + |
318 | + def test_len(self): |
319 | + self.assertEqual(len(SERVER_CONTEXT), len(self.c.all())) |
320 | + |
321 | + def test_contains(self): |
322 | + result = self.c.all() |
323 | + self.assertTrue('uuid' in result) |
324 | + self.assertFalse('uid' in result) |
325 | + self.assertTrue('meta' in result) |
326 | + self.assertFalse('ssh_public_key' in result) |
327 | + |
328 | + def test_iter(self): |
329 | + self.assertEqual(sorted(SERVER_CONTEXT.keys()), |
330 | + sorted([key for key in self.c.all()])) |
331 | + |
332 | + def test_with_list_as_result(self): |
333 | + result = self.c.get('tags') |
334 | + self.assertEqual('much server', result[0]) |
335 | + self.assertTrue('very performance' in result) |
336 | + self.assertEqual(2, len(result)) |
337 | |
338 | === added file 'tests/unittests/test_datasource/test_cloudsigma.py' |
339 | --- tests/unittests/test_datasource/test_cloudsigma.py 1970-01-01 00:00:00 +0000 |
340 | +++ tests/unittests/test_datasource/test_cloudsigma.py 2014-02-12 10:49:46 +0000 |
341 | @@ -0,0 +1,59 @@ |
342 | +# coding: utf-8 |
343 | +from unittest import TestCase |
344 | + |
345 | +from cloudinit.cs_utils import Cepko |
346 | +from cloudinit.sources import DataSourceCloudSigma |
347 | + |
348 | + |
349 | +SERVER_CONTEXT = { |
350 | + "cpu": 1000, |
351 | + "cpus_instead_of_cores": False, |
352 | + "global_context": {"some_global_key": "some_global_val"}, |
353 | + "mem": 1073741824, |
354 | + "meta": { |
355 | + "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2E.../hQ5D5 john@doe", |
356 | + "cloudinit-user-data": "#cloud-config\n\n...", |
357 | + }, |
358 | + "name": "test_server", |
359 | + "requirements": [], |
360 | + "smp": 1, |
361 | + "tags": ["much server", "very performance"], |
362 | + "uuid": "65b2fb23-8c03-4187-a3ba-8b7c919e8890", |
363 | + "vnc_password": "9e84d6cb49e46379" |
364 | +} |
365 | + |
366 | + |
367 | +class CepkoMock(Cepko): |
368 | + result = SERVER_CONTEXT |
369 | + |
370 | + def all(self): |
371 | + return self |
372 | + |
373 | + |
374 | +class DataSourceCloudSigmaTest(TestCase): |
375 | + def setUp(self): |
376 | + self.datasource = DataSourceCloudSigma.DataSourceCloudSigma("", "", "") |
377 | + self.datasource.cepko = CepkoMock() |
378 | + self.datasource.get_data() |
379 | + |
380 | + def test_get_hostname(self): |
381 | + self.assertEqual("test_server", self.datasource.get_hostname()) |
382 | + self.datasource.metadata['name'] = '' |
383 | + self.assertEqual("65b2fb23", self.datasource.get_hostname()) |
384 | + self.datasource.metadata['name'] = u'тест' |
385 | + self.assertEqual("65b2fb23", self.datasource.get_hostname()) |
386 | + |
387 | + def test_get_public_ssh_keys(self): |
388 | + self.assertEqual([SERVER_CONTEXT['meta']['ssh_public_key']], |
389 | + self.datasource.get_public_ssh_keys()) |
390 | + |
391 | + def test_get_instance_id(self): |
392 | + self.assertEqual(SERVER_CONTEXT['uuid'], |
393 | + self.datasource.get_instance_id()) |
394 | + |
395 | + def test_metadata(self): |
396 | + self.assertEqual(self.datasource.metadata, SERVER_CONTEXT) |
397 | + |
398 | + def test_user_data(self): |
399 | + self.assertEqual(self.datasource.userdata_raw, |
400 | + SERVER_CONTEXT['meta']['cloudinit-user-data']) |