From d1e26fc118cdb641829fbe6b838ef46d4ab1f113 Mon Sep 17 00:00:00 2001 From: Kiril Vladimiroff Date: Wed, 19 Feb 2014 10:45:53 +0200 Subject: Read encoded with base64 user data This allows users of CloudSigma's VM to encode their user data with base64. In order to do that thet have to add the ``cloudinit-user-data`` field to the ``base64_fields``. The latter is a comma-separated field with all the meta fields whit base64 encoded values. --- cloudinit/sources/DataSourceCloudSigma.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'cloudinit') diff --git a/cloudinit/sources/DataSourceCloudSigma.py b/cloudinit/sources/DataSourceCloudSigma.py index e734d7e5..79ced3f4 100644 --- a/cloudinit/sources/DataSourceCloudSigma.py +++ b/cloudinit/sources/DataSourceCloudSigma.py @@ -15,6 +15,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from base64 import b64decode import re from cloudinit import log as logging @@ -60,7 +61,11 @@ class DataSourceCloudSigma(sources.DataSource): if dsmode == "disabled" or dsmode != self.dsmode: return False + base64_fields = server_meta.get('base64_fields', '').split(',') self.userdata_raw = server_meta.get('cloudinit-user-data', "") + if 'cloudinit-user-data' in base64_fields: + self.userdata_raw = b64decode(self.userdata_raw) + self.metadata = server_context self.ssh_public_key = server_meta['ssh_public_key'] -- cgit v1.2.3 From da13f065c9a2be372fea35db62e51086d443f8dc Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Mon, 24 Feb 2014 17:20:12 -0500 Subject: fixes from testing, force symlink --- bin/cloud-init | 48 ++++++++++++++++++++++++++---------------------- cloudinit/util.py | 4 +++- 2 files changed, 29 insertions(+), 23 deletions(-) (limited to 'cloudinit') diff --git a/bin/cloud-init b/bin/cloud-init index dc480901..78f8600d 100755 --- a/bin/cloud-init +++ b/bin/cloud-init @@ -419,7 +419,7 @@ def main_single(name, args): return 0 -def status_wrapper(args, data_d=None, link_d=None): +def status_wrapper(name, args, data_d=None, link_d=None): if data_d is None: data_d = os.path.normpath("/var/lib/cloud/data") if link_d is None: @@ -434,13 +434,15 @@ def status_wrapper(args, data_d=None, link_d=None): (_name, functor) = args.action - if args.name: + if name == "init": if args.local: mode = "init-local" else: mode = "init" - elif args.name == "modules": + elif name == "modules": mode = "modules-%s" % args.mode + else: + raise ValueError("unknown name: %s" % name) modes = ('init', 'init-local', 'modules-config', 'modules-final') @@ -457,40 +459,40 @@ def status_wrapper(args, data_d=None, link_d=None): if status is None: nullstatus = { 'errors': [], - 'state': None, 'start': None, 'end': None, } status = {'v1': {}} - for mode in modes: - status['v1'][mode] = nullstatus.copy() + for m in modes: + status['v1'][m] = nullstatus.copy() status['v1']['datasource'] = None - status['stage'] = mode v1 = status['v1'] + v1['stage'] = mode v1[mode]['start'] = time.time() util.write_file(status_path, json.dumps(status)) - util.sym_link(os.path.relpath(os.path.status_path, link_d), status_link) + util.sym_link(os.path.relpath(status_path, link_d), status_link, + force=True) try: - ret = functor(args) + ret = functor(name, args) + if mode in ('init', 'init-local'): + (datasource, errors) = ret + if datasource is not None: + v1['datasource'] = datasource + v1[mode]['errors'] = errors + else: + errors = ret + v1[mode]['errors'] = ret + except Exception as e: v1[mode]['errors'] = [str(e)] v1[mode]['finished'] = time.time() v1['stage'] = None - if mode in ('init', 'init-local'): - (datasource, errors) = ret - if datasource is not None: - v1['datasource'] = datasource - v1[mode]['errors'] = errors - else: - errors = ret - v1[mode]['errors'] = ret - util.write_file(status_path, json.dumps(status)) if mode == "modules-final": @@ -503,8 +505,8 @@ def status_wrapper(args, data_d=None, link_d=None): finished = {'datasource': v1['datasource'], 'errors': errors} util.write_file(result_path, json.dumps(finished)) - util.sym_link(os.path.relpath(os.path.result_path, link_d), - result_link) + util.sym_link(os.path.relpath(result_path, link_d), result_link, + force=True) return len(v1[mode]['errors']) @@ -540,7 +542,7 @@ def main(): default=False) # This is used so that we can know which action is selected + # the functor to use to run this subcommand - parser_init.set_defaults(action=('init', status_wrapper)) + parser_init.set_defaults(action=('init', main_init)) # These settings are used for the 'config' and 'final' stages parser_mod = subparsers.add_parser('modules', @@ -551,7 +553,7 @@ def main(): "to use (default: %(default)s)"), default='config', choices=('init', 'config', 'final')) - parser_mod.set_defaults(action=('modules', status_wrapper)) + parser_mod.set_defaults(action=('modules', main_modules)) # These settings are used when you want to query information # stored in the cloud-init data objects/directories/files @@ -592,6 +594,8 @@ def main(): signal_handler.attach_handlers() (name, functor) = args.action + if name in ("modules", "init"): + functor = status_wrapper return util.log_time(logfunc=LOG.debug, msg="cloud-init mode '%s'" % name, get_uptime=True, func=functor, args=(name, args)) diff --git a/cloudinit/util.py b/cloudinit/util.py index 87b0c853..06039ee2 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -1395,8 +1395,10 @@ def get_builtin_cfg(): return obj_copy.deepcopy(CFG_BUILTIN) -def sym_link(source, link): +def sym_link(source, link, force=False): LOG.debug("Creating symbolic link from %r => %r", link, source) + if force and os.path.exists(link): + del_file(link) os.symlink(source, link) -- cgit v1.2.3 From 2b35f6b814b7f30ceea1e8a58c928f2818bb2729 Mon Sep 17 00:00:00 2001 From: Dustin Kirkland Date: Mon, 3 Mar 2014 16:44:31 -0500 Subject: seed_random: support a 'command' to seed /dev/random This extends 'random_seed' top level entry to include a 'command' entry, that has the opportunity to then seed the random number generator. Example config: #cloud-config random_seed: command: ['dd', 'if=/dev/zero', 'of=/dev/random', 'bs=1M', 'count=10'] LP: #1286316 --- ChangeLog | 2 + cloudinit/config/cc_seed_random.py | 47 ++++++++++++--- .../test_handler/test_handler_seed_random.py | 67 ++++++++++++++++++++++ 3 files changed, 107 insertions(+), 9 deletions(-) (limited to 'cloudinit') diff --git a/ChangeLog b/ChangeLog index 76ab88c4..a45ab73b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -33,6 +33,8 @@ rather than relying on EC2 data in openstack metadata service. - SmartOS, AltCloud: disable running on arm systems due to bug (LP: #1243287, #1285686) [Oleg Strikov] + - Allow running a command to seed random, default is 'pollinate -q' + (LP: #1286316) [Dustin Kirkland] 0.7.4: - fix issue mounting 'ephemeral0' if ephemeral0 was an alias for a partitioned block device with target filesystem on ephemeral0.1. diff --git a/cloudinit/config/cc_seed_random.py b/cloudinit/config/cc_seed_random.py index 22a31f29..599280f6 100644 --- a/cloudinit/config/cc_seed_random.py +++ b/cloudinit/config/cc_seed_random.py @@ -1,8 +1,11 @@ # vi: ts=4 expandtab # # Copyright (C) 2013 Yahoo! Inc. +# Copyright (C) 2014 Canonical, Ltd # # Author: Joshua Harlow +# Author: Dustin Kirkland +# Author: Scott Moser # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, as @@ -20,9 +23,11 @@ import base64 from StringIO import StringIO from cloudinit.settings import PER_INSTANCE +from cloudinit import log as logging from cloudinit import util frequency = PER_INSTANCE +LOG = logging.getLogger(__name__) def _decode(data, encoding=None): @@ -38,24 +43,48 @@ def _decode(data, encoding=None): raise IOError("Unknown random_seed encoding: %s" % (encoding)) -def handle(name, cfg, cloud, log, _args): - if not cfg or "random_seed" not in cfg: - log.debug(("Skipping module named %s, " - "no 'random_seed' configuration found"), name) +def handle_random_seed_command(command, required): + if not command and required: + raise ValueError("no command found but required=true") + elif not command: + LOG.debug("no command provided") return - my_cfg = cfg['random_seed'] - seed_path = my_cfg.get('file', '/dev/urandom') + cmd = command[0] + if not util.which(cmd): + if required: + raise ValueError("command '%s' not found but required=true", cmd) + else: + LOG.debug("command '%s' not found for seed_command", cmd) + return + util.subp(command) + + +def handle(name, cfg, cloud, log, _args): + mycfg = cfg.get('random_seed', {}) + seed_path = mycfg.get('file', '/dev/urandom') + seed_data = mycfg.get('data', '') + seed_buf = StringIO() - seed_buf.write(_decode(my_cfg.get('data', ''), - encoding=my_cfg.get('encoding'))) + if seed_data: + seed_buf.write(_decode(seed_data, encoding=mycfg.get('encoding'))) + # 'random_seed' is set up by Azure datasource, and comes already in + # openstack meta_data.json metadata = cloud.datasource.metadata if metadata and 'random_seed' in metadata: seed_buf.write(metadata['random_seed']) seed_data = seed_buf.getvalue() if len(seed_data): - log.debug("%s: adding %s bytes of random seed entrophy to %s", name, + log.debug("%s: adding %s bytes of random seed entropy to %s", name, len(seed_data), seed_path) util.append_file(seed_path, seed_data) + + command = mycfg.get('command', ['pollinate', '-q']) + req = mycfg.get('command_required', False) + try: + handle_random_seed_command(command=command, required=req) + except ValueError as e: + log.warn("handling random command [%s] failed: %s", command, e) + raise e diff --git a/tests/unittests/test_handler/test_handler_seed_random.py b/tests/unittests/test_handler/test_handler_seed_random.py index 2b21ac02..00c50fc1 100644 --- a/tests/unittests/test_handler/test_handler_seed_random.py +++ b/tests/unittests/test_handler/test_handler_seed_random.py @@ -42,10 +42,29 @@ class TestRandomSeed(t_help.TestCase): def setUp(self): super(TestRandomSeed, self).setUp() self._seed_file = tempfile.mktemp() + self.unapply = [] + + # by default 'which' has nothing in its path + self.apply_patches([(util, 'which', self._which)]) + self.apply_patches([(util, 'subp', self._subp)]) + self.subp_called = [] + self.whichdata = {} def tearDown(self): + apply_patches([i for i in reversed(self.unapply)]) util.del_file(self._seed_file) + def apply_patches(self, patches): + ret = apply_patches(patches) + self.unapply += ret + + def _which(self, program): + return self.whichdata.get(program) + + def _subp(self, args): + self.subp_called.append(tuple(args)) + return + def _compress(self, text): contents = StringIO() gz_fh = gzip.GzipFile(mode='wb', fileobj=contents) @@ -148,3 +167,51 @@ class TestRandomSeed(t_help.TestCase): cc_seed_random.handle('test', cfg, c, LOG, []) contents = util.load_file(self._seed_file) self.assertEquals('tiny-tim-was-here-so-was-josh', contents) + + def test_seed_command_not_provided_pollinate_available(self): + c = self._get_cloud('ubuntu', {}) + self.whichdata = {'pollinate': '/usr/bin/pollinate'} + cc_seed_random.handle('test', {}, c, LOG, []) + + self.assertEquals(self.subp_called, [('pollinate', '-q')]) + + def test_seed_command_not_provided_pollinate_not_available(self): + c = self._get_cloud('ubuntu', {}) + self.whichdata = {} + cc_seed_random.handle('test', {}, c, LOG, []) + + # subp should not have been called as which would say not available + self.assertEquals(self.subp_called, list()) + + def test_unavailable_seed_command_and_required_raises_error(self): + c = self._get_cloud('ubuntu', {}) + self.whichdata = {} + self.assertRaises(ValueError, cc_seed_random.handle, + 'test', {'random_seed': {'command_required': True}}, c, LOG, []) + + def test_seed_command_and_required(self): + c = self._get_cloud('ubuntu', {}) + self.whichdata = {'foo': 'foo'} + cfg = {'random_seed': {'command_required': True, 'command': ['foo']}} + cc_seed_random.handle('test', cfg, c, LOG, []) + + self.assertEquals(self.subp_called, [('foo',)]) + + def test_seed_command_non_default(self): + c = self._get_cloud('ubuntu', {}) + self.whichdata = {'foo': 'foo'} + cfg = {'random_seed': {'command_required': True, 'command': ['foo']}} + cc_seed_random.handle('test', cfg, c, LOG, []) + + self.assertEquals(self.subp_called, [('foo',)]) + + +def apply_patches(patches): + ret = [] + for (ref, name, replace) in patches: + if replace is None: + continue + orig = getattr(ref, name) + setattr(ref, name, replace) + ret.append((ref, name, orig)) + return ret -- cgit v1.2.3