initial checkin of kernel commandline cloud-config url support
This commit is contained in:
parent
55f58896d0
commit
ceeedf3a03
@ -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)
|
||||
|
@ -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)
|
||||
|
48
doc/kernel-cmdline.txt
Normal file
48
doc/kernel-cmdline.txt
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user