summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/__init__.py44
-rw-r--r--cloudinit/util.py13
-rw-r--r--doc/kernel-cmdline.txt48
-rw-r--r--tests/unittests/test__init__.py51
-rw-r--r--tests/unittests/test_util.py18
5 files changed, 171 insertions, 3 deletions
diff --git a/cloudinit/__init__.py b/cloudinit/__init__.py
index 6d276d84..0b46dde9 100644
--- a/cloudinit/__init__.py
+++ b/cloudinit/__init__.py
@@ -137,7 +137,27 @@ class CloudInit:
if ds_deps != None:
self.ds_deps = ds_deps
+
self.sysconfig = sysconfig
+
+ if DataSource.DEP_NETWORK in self.ds_deps:
+ target = "%s.d/%s" % (self.sysconfig, "91_kernel_cmdline_url.cfg")
+ if os.path.exists(target):
+ log.debug("cmdline: %s existed" % target)
+ else:
+ try:
+ (key, url, content) = get_cmdline_url()
+ if key and content:
+ util.write_file(target, content, mode=0600)
+ log.debug("cmdline: wrote %s from %s, %s" %
+ (target, key, url))
+ elif key:
+ log.debug("cmdline: %s, %s had no cloud-config" %
+ (key, url))
+ except Exception:
+ util.logexc(log)
+ log.warn("cmdline: exception occurred while reading")
+
self.cfg = self.read_cfg()
def read_cfg(self):
@@ -639,3 +659,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/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