summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog2
-rwxr-xr-xcloud-init.py30
-rw-r--r--cloudinit/DataSourceNoCloud.py7
-rw-r--r--cloudinit/__init__.py26
-rw-r--r--cloudinit/util.py13
-rw-r--r--doc/examples/cloud-config-datasources.txt7
-rw-r--r--doc/kernel-cmdline.txt48
-rw-r--r--tests/unittests/test__init__.py51
-rw-r--r--tests/unittests/test_util.py18
9 files changed, 199 insertions, 3 deletions
diff --git a/ChangeLog b/ChangeLog
index 56a157a0..8794a06f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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