Merge lp:~kiril-vladimiroff/cloud-init/cloudsigma-data-source into lp:~cloud-init-dev/cloud-init/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
Reviewer Review Type Date Requested Status
cloud-init Commiters Pending
Review via email: mp+205929@code.launchpad.net

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'])