diff options
-rw-r--r-- | ChangeLog | 2 | ||||
-rwxr-xr-x | cloud-init.py | 30 | ||||
-rw-r--r-- | cloudinit/DataSourceNoCloud.py | 7 | ||||
-rw-r--r-- | cloudinit/__init__.py | 26 | ||||
-rw-r--r-- | cloudinit/util.py | 13 | ||||
-rw-r--r-- | doc/examples/cloud-config-datasources.txt | 7 | ||||
-rw-r--r-- | doc/kernel-cmdline.txt | 48 | ||||
-rw-r--r-- | tests/unittests/test__init__.py | 51 | ||||
-rw-r--r-- | tests/unittests/test_util.py | 18 |
9 files changed, 199 insertions, 3 deletions
@@ -42,6 +42,8 @@ 'validation_cert' was not (LP: #960547) - Provide user friendly message when an invalid locale is set [Ben Howard] (LP: #859814) + - Support reading cloud-config from kernel command line parameter and + populating local file with it, which can then provide data for DataSources 0.6.2: - fix bug where update was not done unless update was explicitly set. diff --git a/cloud-init.py b/cloud-init.py index 9e0a0405..3261e3f3 100755 --- a/cloud-init.py +++ b/cloud-init.py @@ -28,6 +28,7 @@ import cloudinit.CloudConfig as CC import cloudinit.DataSource as ds import cloudinit.netinfo as netinfo import time +import traceback import logging import errno import os @@ -67,6 +68,30 @@ def main(): warn("unable to open /proc/uptime\n") uptime = "na" + cmdline_msg = None + cmdline_exc = None + if cmd == "start": + target = "%s.d/%s" % (cloudinit.system_config, + "91_kernel_cmdline_url.cfg") + if os.path.exists(target): + cmdline_msg = "cmdline: %s existed" % target + else: + cmdline=util.get_cmdline() + try: + (key, url, content) = cloudinit.get_cmdline_url( + cmdline=cmdline) + if key and content: + util.write_file(target, content, mode=0600) + cmdline_msg = ("cmdline: wrote %s from %s, %s" % + (target, key, url)) + elif key: + cmdline_msg = ("cmdline: %s, %s had no cloud-config" % + (key, url)) + except Exception: + cmdline_exc = ("cmdline: '%s' raised exception\n%s" % + (cmdline, traceback.format_exc())) + warn(cmdline_exc) + try: cfg = cloudinit.get_base_cfg(cfg_path) except Exception as e: @@ -86,6 +111,11 @@ def main(): cloudinit.logging_set_from_cfg(cfg) log = logging.getLogger() + if cmdline_exc: + log.debug(cmdline_exc) + elif cmdline_msg: + log.debug(cmdline_msg) + try: cloudinit.initfs() except Exception as e: diff --git a/cloudinit/DataSourceNoCloud.py b/cloudinit/DataSourceNoCloud.py index 62ecc088..e8c56b8f 100644 --- a/cloudinit/DataSourceNoCloud.py +++ b/cloudinit/DataSourceNoCloud.py @@ -67,6 +67,13 @@ class DataSourceNoCloud(DataSource.DataSource): found.append(self.seeddir) log.debug("using seeded cache data in %s" % self.seeddir) + # if the datasource config had a 'seedfrom' entry, then that takes + # precedence over a 'seedfrom' that was found in a filesystem + # but not over external medi + if 'seedfrom' in self.ds_cfg and self.ds_cfg['seedfrom']: + found.append("ds_config") + md["seedfrom"] = self.ds_cfg['seedfrom'] + fslist = util.find_devs_with("TYPE=vfat") fslist.extend(util.find_devs_with("TYPE=iso9660")) diff --git a/cloudinit/__init__.py b/cloudinit/__init__.py index 6d276d84..85c6fd1b 100644 --- a/cloudinit/__init__.py +++ b/cloudinit/__init__.py @@ -137,7 +137,9 @@ class CloudInit: if ds_deps != None: self.ds_deps = ds_deps + self.sysconfig = sysconfig + self.cfg = self.read_cfg() def read_cfg(self): @@ -639,3 +641,27 @@ class InternalPartHandler: def handle_part(self, data, ctype, filename, payload, frequency): return(self.handler(data, ctype, filename, payload, frequency)) + + +def get_cmdline_url(names=('cloud-config-url', 'url'), + starts="#cloud-config", cmdline=None): + + if cmdline == None: + cmdline = util.get_cmdline() + + data = util.keyval_str_to_dict(cmdline) + url = None + key = None + for key in names: + if key in data: + url = data[key] + break + if url == None: + return (None, None, None) + + contents = util.readurl(url) + + if contents.startswith(starts): + return (key, url, contents) + + return (key, url, None) diff --git a/cloudinit/util.py b/cloudinit/util.py index 9133426c..47397418 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -840,3 +840,16 @@ def wait_for_url(urls, max_wait=None, timeout=None, time.sleep(sleeptime) return False + + +def keyval_str_to_dict(kvstring): + ret = {} + for tok in kvstring.split(): + try: + (key, val) = tok.split("=", 1) + except ValueError: + key = tok + val = True + ret[key] = val + + return(ret) diff --git a/doc/examples/cloud-config-datasources.txt b/doc/examples/cloud-config-datasources.txt index 63a6cfc4..d10dde05 100644 --- a/doc/examples/cloud-config-datasources.txt +++ b/doc/examples/cloud-config-datasources.txt @@ -24,3 +24,10 @@ datasource: consumer_key: Xh234sdkljf token_key: kjfhgb3n token_secret: 24uysdfx1w4 + + NoCloud: + # default seedfrom is None + # if found, then it should contain a url with: + # <url>/user-data and <url>/meta-data + # seedfrom: http://my.example.com/i-abcde + seedfrom: None diff --git a/doc/kernel-cmdline.txt b/doc/kernel-cmdline.txt new file mode 100644 index 00000000..0b77a9af --- /dev/null +++ b/doc/kernel-cmdline.txt @@ -0,0 +1,48 @@ +In order to allow an ephemeral, or otherwise pristine image to +receive some configuration, cloud-init will read a url directed by +the kernel command line and proceed as if its data had previously existed. + +This allows for configuring a meta-data service, or some other data. + +Note, that usage of the kernel command line is somewhat of a last resort, +as it requires knowing in advance the correct command line or modifying +the boot loader to append data. + +For example, when 'cloud-init start' runs, it will check to +see if if one of 'cloud-config-url' or 'url' appear in key/value fashion +in the kernel command line as in: + root=/dev/sda ro url=http://foo.bar.zee/abcde + +Cloud-init will then read the contents of the given url. +If the content starts with '#cloud-config', it will store +that data to the local filesystem in a static filename +'/etc/cloud/cloud.cfg.d/91_kernel_cmdline_url.cfg', and consider it as +part of the config from that point forward. + +If that file exists already, it will not be overwritten, and the url parameters +completely ignored. + +Then, when the DataSource runs, it will find that config already available. + +So, in able to configure the MAAS DataSource by controlling the kernel +command line from outside the image, you can append: + url=http://your.url.here/abcdefg +or + cloud-config-url=http://your.url.here/abcdefg + +Then, have the following content at that url: + #cloud-config + datasource: + MAAS: + metadata_url: http://mass-host.localdomain/source + consumer_key: Xh234sdkljf + token_key: kjfhgb3n + token_secret: 24uysdfx1w4 + +Notes: + * Because 'url=' is so very generic, in order to avoid false positives, + cloud-init requires the content to start with '#cloud-config' in order + for it to be considered. + * The url= is un-authed http GET, and contains credentials + It could be set up to be randomly generated and also check source + address in order to be more secure diff --git a/tests/unittests/test__init__.py b/tests/unittests/test__init__.py index e157fa77..4f60f0ea 100644 --- a/tests/unittests/test__init__.py +++ b/tests/unittests/test__init__.py @@ -2,8 +2,8 @@ from mocker import MockerTestCase, ANY, ARGS, KWARGS import os from cloudinit import (partwalker_handle_handler, handler_handle_part, - handler_register) -from cloudinit.util import write_file, logexc + handler_register, get_cmdline_url) +from cloudinit.util import write_file, logexc, readurl class TestPartwalkerHandleHandler(MockerTestCase): @@ -193,3 +193,50 @@ class TestHandlerHandlePart(MockerTestCase): handler_handle_part(mod_mock, self.data, self.ctype, self.filename, self.payload, self.frequency) + + +class TestCmdlineUrl(MockerTestCase): + def test_invalid_content(self): + url = "http://example.com/foo" + key = "mykey" + payload = "0" + cmdline = "ro %s=%s bar=1" % (key, url) + + mock_readurl = self.mocker.replace(readurl, passthrough=False) + mock_readurl(url) + self.mocker.result(payload) + + self.mocker.replay() + + self.assertEqual((key, url, None), + get_cmdline_url(names=[key], starts="xxxxxx", cmdline=cmdline)) + + def test_valid_content(self): + url = "http://example.com/foo" + key = "mykey" + payload = "xcloud-config\nmydata: foo\nbar: wark\n" + cmdline = "ro %s=%s bar=1" % (key, url) + + mock_readurl = self.mocker.replace(readurl, passthrough=False) + mock_readurl(url) + self.mocker.result(payload) + + self.mocker.replay() + + self.assertEqual((key, url, payload), + get_cmdline_url(names=[key], starts="xcloud-config", + cmdline=cmdline)) + + def test_no_key_found(self): + url = "http://example.com/foo" + key = "mykey" + cmdline = "ro %s=%s bar=1" % (key, url) + + self.mocker.replace(readurl, passthrough=False) + self.mocker.replay() + + self.assertEqual((None, None, None), + get_cmdline_url(names=["does-not-appear"], + starts="#cloud-config", cmdline=cmdline)) + +# vi: ts=4 expandtab diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py index ca96bc60..e8f5885c 100644 --- a/tests/unittests/test_util.py +++ b/tests/unittests/test_util.py @@ -6,7 +6,8 @@ import os import stat from cloudinit.util import (mergedict, get_cfg_option_list_or_str, write_file, - delete_dir_contents) + delete_dir_contents, get_cmdline, + keyval_str_to_dict) class TestMergeDict(TestCase): @@ -248,3 +249,18 @@ class TestDeleteDirContents(TestCase): delete_dir_contents(self.tmp) self.assertDirEmpty(self.tmp) + + +class TestKeyValStrings(TestCase): + def test_keyval_str_to_dict(self): + expected = {'1': 'one', '2': 'one+one', 'ro': True} + cmdline = "1=one ro 2=one+one" + self.assertEqual(expected, keyval_str_to_dict(cmdline)) + + +class TestGetCmdline(TestCase): + def test_cmdline_reads_debug_env(self): + os.environ['DEBUG_PROC_CMDLINE'] = 'abcd 123' + self.assertEqual(os.environ['DEBUG_PROC_CMDLINE'], get_cmdline()) + +# vi: ts=4 expandtab |