diff options
-rw-r--r-- | ChangeLog | 6 | ||||
-rw-r--r-- | cloudinit/config/cc_seed_random.py | 47 | ||||
-rw-r--r-- | cloudinit/distros/freebsd.py | 2 | ||||
-rw-r--r-- | cloudinit/netinfo.py | 17 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceAltCloud.py | 6 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceConfigDrive.py | 2 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceOpenStack.py | 12 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceSmartOS.py | 11 | ||||
-rw-r--r-- | cloudinit/sources/helpers/__init__.py | 1 | ||||
-rw-r--r-- | cloudinit/sources/helpers/openstack.py | 21 | ||||
-rw-r--r-- | doc/status.txt | 12 | ||||
-rw-r--r-- | tests/unittests/test_datasource/test_altcloud.py | 24 | ||||
-rw-r--r-- | tests/unittests/test_datasource/test_azure.py | 4 | ||||
-rw-r--r-- | tests/unittests/test_datasource/test_configdrive.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_datasource/test_nocloud.py | 1 | ||||
-rw-r--r-- | tests/unittests/test_datasource/test_smartos.py | 6 | ||||
-rw-r--r-- | tests/unittests/test_handler/test_handler_growpart.py | 3 | ||||
-rw-r--r-- | tests/unittests/test_handler/test_handler_seed_random.py | 67 | ||||
-rwxr-xr-x | tools/make-tarball | 8 |
19 files changed, 206 insertions, 46 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/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/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/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/doc/status.txt b/doc/status.txt index 5958fa85..60993216 100644 --- a/doc/status.txt +++ b/doc/status.txt @@ -3,7 +3,7 @@ wishing to use it to determine cloud-init status. It will manage 2 files: status.json - finished.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 @@ -33,18 +33,20 @@ status.json's format is: of each of the above stages to determine the state. } -finished.json's format is: +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/finished.json" + fin = "/run/cloud-init/result.json" if os.path.exists(fin): ret = json.load(open(fin, "r")) - if len(ret): - print "Finished with errors:" + "\n".join(ret['errors']) + if len(ret['v1']['errors']): + print "Finished with errors:" + "\n".join(ret['v1']['errors']) else: print "Finished no errors" else: 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 11d9a264..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 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" |