diff options
23 files changed, 383 insertions, 88 deletions
@@ -31,6 +31,12 @@ - Add GCE datasource [Vaidas Jablonskis] - Add native Openstack datasource which reads openstack metadata 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] + - Write status to /run/cloud-init/status.json for consumption by + other programs (LP: #1284439) 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/bin/cloud-init b/bin/cloud-init index 80a1df05..6ede60af 100755 --- a/bin/cloud-init +++ b/bin/cloud-init @@ -22,8 +22,11 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import argparse +import json import os import sys +import time +import tempfile import traceback # This is more just for running from the bin folder so that @@ -126,11 +129,11 @@ def run_module_section(mods, action_name, section): " under section '%s'") % (action_name, full_section_name) sys.stderr.write("%s\n" % (msg)) LOG.debug(msg) - return 0 + return [] else: LOG.debug("Ran %s modules with %s failures", len(which_ran), len(failures)) - return len(failures) + return failures def main_init(name, args): @@ -220,7 +223,7 @@ def main_init(name, args): if existing_files: LOG.debug("Exiting early due to the existence of %s files", existing_files) - return 0 + return (None, []) else: # The cache is not instance specific, so it has to be purged # but we want 'start' to benefit from a cache if @@ -249,9 +252,9 @@ def main_init(name, args): " Likely bad things to come!")) if not args.force: if args.local: - return 0 + return (None, []) else: - return 1 + return (None, ["No instance datasource found."]) # Stage 6 iid = init.instancify() LOG.debug("%s will now be targeting instance id: %s", name, iid) @@ -274,7 +277,7 @@ def main_init(name, args): init.consume_data(PER_ALWAYS) except Exception: util.logexc(LOG, "Consuming user data failed!") - return 1 + return (init.datasource, ["Consuming user data failed!"]) # Stage 8 - re-read and apply relevant cloud-config to include user-data mods = stages.Modules(init, extract_fns(args)) @@ -291,7 +294,7 @@ def main_init(name, args): logging.setupLogging(mods.cfg) # Stage 10 - return run_module_section(mods, name, name) + return (init.datasource, run_module_section(mods, name, name)) def main_modules(action_name, args): @@ -315,14 +318,12 @@ def main_modules(action_name, args): init.fetch() except sources.DataSourceNotFoundException: # There was no datasource found, theres nothing to do - util.logexc(LOG, ('Can not apply stage %s, ' - 'no datasource found!' - " Likely bad things to come!"), name) - print_exc(('Can not apply stage %s, ' - 'no datasource found!' - " Likely bad things to come!") % (name)) + msg = ('Can not apply stage %s, no datasource found! Likely bad ' + 'things to come!' % name) + util.logexc(LOG, msg) + print_exc(msg) if not args.force: - return 1 + return [(msg)] # Stage 3 mods = stages.Modules(init, extract_fns(args)) # Stage 4 @@ -419,6 +420,110 @@ def main_single(name, args): return 0 +def atomic_write_json(path, data): + tf = None + try: + tf = tempfile.NamedTemporaryFile(dir=os.path.dirname(path), + delete=False) + tf.write(json.dumps(data, indent=1) + "\n") + tf.close() + os.rename(tf.name, path) + except Exception as e: + if tf is not None: + util.del_file(tf.name) + raise e + + +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: + link_d = os.path.normpath("/run/cloud-init") + + status_path = os.path.join(data_d, "status.json") + status_link = os.path.join(link_d, "status.json") + result_path = os.path.join(data_d, "result.json") + result_link = os.path.join(link_d, "result.json") + + util.ensure_dirs((data_d, link_d,)) + + (_name, functor) = args.action + + if name == "init": + if args.local: + mode = "init-local" + else: + mode = "init" + elif name == "modules": + mode = "modules-%s" % args.mode + else: + raise ValueError("unknown name: %s" % name) + + modes = ('init', 'init-local', 'modules-config', 'modules-final') + + status = None + if mode == 'init-local': + for f in (status_link, result_link, status_path, result_path): + util.del_file(f) + else: + try: + status = json.loads(util.load_file(status_path)) + except: + pass + + if status is None: + nullstatus = { + 'errors': [], + 'start': None, + 'end': None, + } + status = {'v1': {}} + for m in modes: + status['v1'][m] = nullstatus.copy() + status['v1']['datasource'] = None + + v1 = status['v1'] + v1['stage'] = mode + v1[mode]['start'] = time.time() + + atomic_write_json(status_path, status) + util.sym_link(os.path.relpath(status_path, link_d), status_link, + force=True) + + try: + ret = functor(name, args) + if mode in ('init', 'init-local'): + (datasource, errors) = ret + if datasource is not None: + v1['datasource'] = str(datasource) + else: + errors = ret + + v1[mode]['errors'] = [str(e) for e in errors] + + except Exception as e: + v1[mode]['errors'] = [str(e)] + + v1[mode]['finished'] = time.time() + v1['stage'] = None + + atomic_write_json(status_path, status) + + if mode == "modules-final": + # write the 'finished' file + errors = [] + for m in modes: + if v1[m]['errors']: + errors.extend(v1[m].get('errors', [])) + + atomic_write_json(result_path, + {'v1': {'datasource': v1['datasource'], 'errors': errors}}) + util.sym_link(os.path.relpath(result_path, link_d), result_link, + force=True) + + return len(v1[mode]['errors']) + + def main(): parser = argparse.ArgumentParser() @@ -502,6 +607,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/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 <harlowja@yahoo-inc.com> +# Author: Dustin Kirkland <kirkland@ubuntu.com> +# Author: Scott Moser <scott.moser@canonical.com> # # 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/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py index afb502c9..d98f9578 100644 --- a/cloudinit/distros/freebsd.py +++ b/cloudinit/distros/freebsd.py @@ -79,7 +79,7 @@ class Distro(distros.Distro): return val def _read_system_hostname(self): - sys_hostname = self._read_hostname() + sys_hostname = self._read_hostname(filename=None) return ('rc.conf', sys_hostname) def _read_hostname(self, filename, default=None): diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index ac3c011f..30b6f3b3 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -52,18 +52,23 @@ def netdev_info(empty=""): fieldpost = "6" for i in range(len(toks)): - if toks[i] == "hwaddr" or toks[i] == "ether": - try: - devs[curdev]["hwaddr"] = toks[i + 1] - except IndexError: - pass + # older net-tools (ubuntu) show 'inet addr:xx.yy', + # newer (freebsd and fedora) show 'inet xx.yy' + # just skip this 'inet' entry. (LP: #1285185) + try: + if (toks[i] in ("inet", "inet6") and + toks[i + 1].startswith("addr:")): + continue + except IndexError: + pass # Couple the different items we're interested in with the correct # field since FreeBSD/CentOS/Fedora differ in the output. ifconfigfields = { "addr:": "addr", "inet": "addr", "bcast:": "bcast", "broadcast": "bcast", - "mask:": "mask", "netmask": "mask" + "mask:": "mask", "netmask": "mask", + "hwaddr": "hwaddr", "ether": "hwaddr", } for origfield, field in ifconfigfields.items(): target = "%s%s" % (field, fieldpost) diff --git a/cloudinit/sources/DataSourceAltCloud.py b/cloudinit/sources/DataSourceAltCloud.py index a834f8eb..1e913a6e 100644 --- a/cloudinit/sources/DataSourceAltCloud.py +++ b/cloudinit/sources/DataSourceAltCloud.py @@ -115,6 +115,12 @@ class DataSourceAltCloud(sources.DataSource): ''' + uname_arch = os.uname()[4] + if uname_arch.startswith("arm") or uname_arch == "aarch64": + # Disabling because dmidecode in CMD_DMI_SYSTEM crashes kvm process + LOG.debug("Disabling AltCloud datasource on arm (LP: #1243287)") + return 'UNKNOWN' + cmd = CMD_DMI_SYSTEM try: (cmd_out, _err) = util.subp(cmd) diff --git a/cloudinit/sources/DataSourceCloudSigma.py b/cloudinit/sources/DataSourceCloudSigma.py index 79ced3f4..e1c7e566 100644 --- a/cloudinit/sources/DataSourceCloudSigma.py +++ b/cloudinit/sources/DataSourceCloudSigma.py @@ -20,7 +20,6 @@ import re from cloudinit import log as logging from cloudinit import sources -from cloudinit import util from cloudinit.cs_utils import Cepko LOG = logging.getLogger(__name__) @@ -51,7 +50,9 @@ class DataSourceCloudSigma(sources.DataSource): server_context = self.cepko.all().result server_meta = server_context['meta'] except: - util.logexc(LOG, "Failed reading from the serial port") + # TODO: check for explicit "config on", and then warn + # but since no explicit config is available now, just debug. + LOG.debug("CloudSigma: Unable to read from serial port") return False dsmode = server_meta.get('cloudinit-dsmode', self.dsmode) diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py index 142e0eb8..0c35f83a 100644 --- a/cloudinit/sources/DataSourceConfigDrive.py +++ b/cloudinit/sources/DataSourceConfigDrive.py @@ -181,7 +181,7 @@ def get_previous_iid(paths): # hasn't declared itself found. fname = os.path.join(paths.get_cpath('data'), 'instance-id') try: - return util.load_file(fname) + return util.load_file(fname).rstrip("\n") except IOError: return None diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py index cbaac29f..8dc96ab6 100644 --- a/cloudinit/sources/DataSourceNoCloud.py +++ b/cloudinit/sources/DataSourceNoCloud.py @@ -90,7 +90,7 @@ class DataSourceNoCloud(sources.DataSource): found.append("ds_config") def _pp2d_callback(mp, data): - util.pathprefix2dict(mp, **data) + return util.pathprefix2dict(mp, **data) label = self.ds_cfg.get('fs_label', "cidata") if label is not None: @@ -110,7 +110,8 @@ class DataSourceNoCloud(sources.DataSource): LOG.debug("Attempting to use data from %s", dev) try: - seeded = util.mount_cb(dev, _pp2d_callback) + seeded = util.mount_cb(dev, _pp2d_callback, + pp2d_kwargs) except ValueError as e: if dev in label_list: LOG.warn("device %s with label=%s not a" @@ -123,7 +124,7 @@ class DataSourceNoCloud(sources.DataSource): # that is more likely to be what is desired. If they want # dsmode of local, then they must specify that. if 'dsmode' not in mydata['meta-data']: - mydata['meta-data'] = "net" + mydata['dsmode'] = "net" LOG.debug("Using data from %s", dev) found.append(dev) diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py index 72ec8075..0970d07b 100644 --- a/cloudinit/sources/DataSourceOpenStack.py +++ b/cloudinit/sources/DataSourceOpenStack.py @@ -88,11 +88,11 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): md_urls = [] url2base = {} for url in urls: - md_url = url_helper.combine_url(url, 'openstack', - openstack.OS_LATEST, - 'meta_data.json') - md_urls.append(md_url) - url2base[md_url] = url + for version in openstack.OS_VERSIONS + (openstack.OS_LATEST,): + md_url = url_helper.combine_url(url, 'openstack', + version, 'meta_data.json') + md_urls.append(md_url) + url2base[md_url] = url (max_wait, timeout) = self._get_url_settings() start_time = time.time() @@ -120,7 +120,7 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): read_metadata_service, args=[self.metadata_address], kwargs={'ssl_details': self.ssl_details, - 'version': openstack.OS_LATEST}) + 'version': openstack.OS_HAVANA}) except openstack.NonReadable: return False except (openstack.BrokenMetadata, IOError): diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py index 8d4a16e6..7c1eb09a 100644 --- a/cloudinit/sources/DataSourceSmartOS.py +++ b/cloudinit/sources/DataSourceSmartOS.py @@ -174,6 +174,12 @@ class DataSourceSmartOS(sources.DataSource): LOG.debug("Host does not appear to be on SmartOS") return False + uname_arch = os.uname()[4] + if uname_arch.startswith("arm") or uname_arch == "aarch64": + # Disabling because dmidcode in dmi_data() crashes kvm process + LOG.debug("Disabling SmartOS datasource on arm (LP: #1243287)") + return False + dmi_info = dmi_data() if dmi_info is False: LOG.debug("No dmidata utility found") @@ -203,7 +209,7 @@ class DataSourceSmartOS(sources.DataSource): # executed. It may be of any format that would be considered # executable in the guest instance. # - # We write 'user-script' and 'operator-script' into the + # We write 'user-script' and 'operator-script' into the # instance/data directory. The default vendor-data then handles # executing them later. data_d = os.path.join(self.paths.get_cpath(), 'instances', @@ -238,7 +244,8 @@ class DataSourceSmartOS(sources.DataSource): md['vendor-data'] = BUILTIN_VENDOR_DATA % { 'user_script': user_script, 'operator_script': operator_script, - 'per_boot_d': os.path.join(self.paths.get_cpath("scripts"), 'per-boot'), + 'per_boot_d': os.path.join(self.paths.get_cpath("scripts"), + 'per-boot'), } self.metadata = util.mergemanydict([md, self.metadata]) diff --git a/cloudinit/sources/helpers/__init__.py b/cloudinit/sources/helpers/__init__.py index 2cf99ec8..386225d5 100644 --- a/cloudinit/sources/helpers/__init__.py +++ b/cloudinit/sources/helpers/__init__.py @@ -11,4 +11,3 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py index a17148d3..0fac0335 100644 --- a/cloudinit/sources/helpers/openstack.py +++ b/cloudinit/sources/helpers/openstack.py @@ -44,12 +44,15 @@ KEY_COPIES = ( ('local-hostname', 'hostname', False), ('instance-id', 'uuid', True), ) +OS_LATEST = 'latest' +OS_FOLSOM = '2012-08-10' +OS_GRIZZLY = '2013-04-04' +OS_HAVANA = '2013-10-17' OS_VERSIONS = ( - '2012-08-10', # folsom - '2013-04-04', # grizzly - '2013-10-17', # havana + OS_FOLSOM, + OS_GRIZZLY, + OS_HAVANA, ) -OS_LATEST = 'latest' class NonReadable(IOError): @@ -176,12 +179,12 @@ class BaseReader(object): potential_version) if self._path_exists(path): if potential_version != version: - LOG.warn("Version '%s' not available, attempting to use" - " version '%s' instead", version, - potential_version) + LOG.debug("Version '%s' not available, attempting to use" + " version '%s' instead", version, + potential_version) return potential_version - LOG.warn("Version '%s' not available, attempting to use '%s'" - " instead", version, OS_LATEST) + LOG.debug("Version '%s' not available, attempting to use '%s'" + " instead", version, OS_LATEST) return OS_LATEST def read_v2(self, version=None): 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) diff --git a/doc/status.txt b/doc/status.txt new file mode 100644 index 00000000..60993216 --- /dev/null +++ b/doc/status.txt @@ -0,0 +1,53 @@ +cloud-init will keep a 'status' file up to date for other applications +wishing to use it to determine cloud-init status. + +It will manage 2 files: + status.json + result.json + +The files will be written to /var/lib/cloud/data/ . +A symlink will be created in /run/cloud-init. The link from /run is to ensure +that if the file exists, it is not stale for this boot. + +status.json's format is: + { + 'v1': { + 'init': { + errors: [] # list of strings for each error that occurred + start: float # time.time() that this stage started or None + end: float # time.time() that this stage finished or None + }, + 'init-local': { + 'errors': [], 'start': <float>, 'end' <float> # (same as 'init' above) + }, + 'modules-config': { + 'errors': [], 'start': <float>, 'end' <float> # (same as 'init' above) + }, + 'modules-final': { + 'errors': [], 'start': <float>, 'end' <float> # (same as 'init' above) + }, + 'datasource': string describing datasource found or None + 'stage': string representing stage that is currently running + ('init', 'init-local', 'modules-final', 'modules-config', None) + if None, then no stage is running. Reader must read the start/end + of each of the above stages to determine the state. + } + +result.json's format is: + { + 'v1': { + 'datasource': string describing the datasource found + 'errors': [] # list of errors reported + } + } + +Thus, to determine if cloud-init is finished: + fin = "/run/cloud-init/result.json" + if os.path.exists(fin): + ret = json.load(open(fin, "r")) + if len(ret['v1']['errors']): + print "Finished with errors:" + "\n".join(ret['v1']['errors']) + else: + print "Finished no errors" + else: + print "Not Finished" diff --git a/tests/unittests/test_datasource/test_altcloud.py b/tests/unittests/test_datasource/test_altcloud.py index bda61c7e..eaaa90e6 100644 --- a/tests/unittests/test_datasource/test_altcloud.py +++ b/tests/unittests/test_datasource/test_altcloud.py @@ -33,6 +33,8 @@ import cloudinit.sources.DataSourceAltCloud from cloudinit.sources.DataSourceAltCloud import DataSourceAltCloud from cloudinit.sources.DataSourceAltCloud import read_user_data_callback +OS_UNAME_ORIG = getattr(os, 'uname') + def _write_cloud_info_file(value): ''' @@ -104,11 +106,16 @@ class TestGetCloudType(TestCase): def setUp(self): '''Set up.''' self.paths = helpers.Paths({'cloud_dir': '/tmp'}) + # We have a different code path for arm to deal with LP1243287 + # We have to switch arch to x86_64 to avoid test failure + force_arch('x86_64') def tearDown(self): # Reset cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \ ['dmidecode', '--string', 'system-product-name'] + # Return back to original arch + force_arch() def test_rhev(self): ''' @@ -238,6 +245,9 @@ class TestGetDataNoCloudInfoFile(TestCase): self.paths = helpers.Paths({'cloud_dir': '/tmp'}) cloudinit.sources.DataSourceAltCloud.CLOUD_INFO_FILE = \ 'no such file' + # We have a different code path for arm to deal with LP1243287 + # We have to switch arch to x86_64 to avoid test failure + force_arch('x86_64') def tearDown(self): # Reset @@ -245,6 +255,8 @@ class TestGetDataNoCloudInfoFile(TestCase): '/etc/sysconfig/cloud-info' cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \ ['dmidecode', '--string', 'system-product-name'] + # Return back to original arch + force_arch() def test_rhev_no_cloud_file(self): '''Test No cloud info file module get_data() forcing RHEV.''' @@ -442,4 +454,16 @@ class TestReadUserDataCallback(TestCase): _remove_user_data_files(self.mount_dir) self.assertEquals(None, read_user_data_callback(self.mount_dir)) + +def force_arch(arch=None): + + def _os_uname(): + return ('LINUX', 'NODENAME', 'RELEASE', 'VERSION', arch) + + if arch: + setattr(os, 'uname', _os_uname) + elif arch is None: + setattr(os, 'uname', OS_UNAME_ORIG) + + # vi: ts=4 expandtab diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py index 44c537f4..ccfd672a 100644 --- a/tests/unittests/test_datasource/test_azure.py +++ b/tests/unittests/test_datasource/test_azure.py @@ -352,13 +352,13 @@ class TestAzureDataSource(MockerTestCase): ovf_env_path = os.path.join(self.waagent_d, 'ovf-env.xml') self.assertTrue(os.path.exists(ovf_env_path)) self.assertEqual(xml, load_file(ovf_env_path)) - + def test_existing_ovf_same(self): # waagent/SharedConfig left alone if found ovf-env.xml same as cached odata = {'UserData': base64.b64encode("SOMEUSERDATA")} data = {'ovfcontent': construct_valid_ovf_env(data=odata)} - populate_dir(self.waagent_d, + populate_dir(self.waagent_d, {'ovf-env.xml': data['ovfcontent'], 'otherfile': 'otherfile-content', 'SharedConfig.xml': 'mysharedconfig'}) diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py index 937b88c1..4404668e 100644 --- a/tests/unittests/test_datasource/test_configdrive.py +++ b/tests/unittests/test_datasource/test_configdrive.py @@ -166,7 +166,7 @@ class TestConfigDriveDataSource(MockerTestCase): my_mock.replay() device = cfg_ds.device_name_to_device(name) self.assertEquals(dev_name, device) - + def test_dev_ec2_map(self): populate_dir(self.tmp, CFG_DRIVE_FILES_V2) cfg_ds = ds.DataSourceConfigDrive(settings.CFG_BUILTIN, diff --git a/tests/unittests/test_datasource/test_nocloud.py b/tests/unittests/test_datasource/test_nocloud.py index af575a10..a65833eb 100644 --- a/tests/unittests/test_datasource/test_nocloud.py +++ b/tests/unittests/test_datasource/test_nocloud.py @@ -133,6 +133,7 @@ class TestNoCloudDataSource(MockerTestCase): self.assertFalse(dsrc.vendordata) self.assertTrue(ret) + class TestParseCommandLineData(MockerTestCase): def test_parse_cmdline_data_valid(self): diff --git a/tests/unittests/test_datasource/test_smartos.py b/tests/unittests/test_datasource/test_smartos.py index 19282bac..8f9fa27d 100644 --- a/tests/unittests/test_datasource/test_smartos.py +++ b/tests/unittests/test_datasource/test_smartos.py @@ -158,6 +158,11 @@ class TestSmartOSDataSource(helpers.FilesystemMockingTestCase): def _dmi_data(): return dmi_data + def _os_uname(): + # LP: #1243287. tests assume this runs, but running test on + # arm would cause them all to fail. + return ('LINUX', 'NODENAME', 'RELEASE', 'VERSION', 'x86_64') + if sys_cfg is None: sys_cfg = {} @@ -168,6 +173,7 @@ class TestSmartOSDataSource(helpers.FilesystemMockingTestCase): self.apply_patches([(mod, 'LEGACY_USER_D', self.legacy_user_d)]) self.apply_patches([(mod, 'get_serial', _get_serial)]) self.apply_patches([(mod, 'dmi_data', _dmi_data)]) + self.apply_patches([(os, 'uname', _os_uname)]) dsrc = mod.DataSourceSmartOS(sys_cfg, distro=None, paths=self.paths) return dsrc @@ -334,33 +340,6 @@ class TestSmartOSDataSource(helpers.FilesystemMockingTestCase): shebang = f.readlines()[0].strip() self.assertEquals(shebang, "#!/usr/bin/perl") - def test_scripts_removed(self): - """ - Since SmartOS requires that the user script is fetched - each boot, we want to make sure that the information - is backed-up for user-review later. - - This tests the behavior of when a script is removed. It makes - sure that a) the previous script is backed-up; and 2) that - there is no script remaining. - """ - - script_d = os.path.join(self.tmp, "instance", "data") - os.makedirs(script_d) - - test_script_f = os.path.join(script_d, 'user-script') - with open(test_script_f, 'w') as f: - f.write("TEST DATA") - - my_returns = MOCK_RETURNS.copy() - del my_returns['user-script'] - - dsrc = self._get_ds(mockdata=my_returns) - ret = dsrc.get_data() - self.assertTrue(ret) - self.assertFalse(dsrc.metadata['user-script']) - self.assertFalse(os.path.exists(test_script_f)) - def test_userdata_removed(self): """ User-data in the SmartOS world is supposed to be written to a file diff --git a/tests/unittests/test_handler/test_handler_growpart.py b/tests/unittests/test_handler/test_handler_growpart.py index 996526d3..f2ed4597 100644 --- a/tests/unittests/test_handler/test_handler_growpart.py +++ b/tests/unittests/test_handler/test_handler_growpart.py @@ -9,7 +9,6 @@ import errno import logging import os import re -import unittest # growpart: # mode: auto # off, on, auto, 'growpart' @@ -203,7 +202,7 @@ def simple_device_part_info(devpath): return x -class Bunch: +class Bunch(object): st_mode = None # fix pylint complaint def __init__(self, **kwds): 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 diff --git a/tools/make-tarball b/tools/make-tarball index 27f5f374..b7039150 100755 --- a/tools/make-tarball +++ b/tools/make-tarball @@ -27,7 +27,13 @@ else ARCHIVE_FN="$PWD/cloud-init-$VERSION~bzr$REVNO.tar.gz" fi -bzr export --format=tgz --root="cloud-init-$VERSION~bzr$REVNO" \ +export_uncommitted="" +if [ "${UNCOMMITTED:-0}" != "0" ]; then + export_uncommitted="--uncommitted" +fi + +bzr export ${export_uncommitted} \ + --format=tgz --root="cloud-init-$VERSION~bzr$REVNO" \ "--revision=${REVNO}" "${ARCHIVE_FN}" "$ROOT_DIR" echo "$ARCHIVE_FN" |