diff options
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | cloudinit/config/cc_ssh.py | 65 | ||||
-rw-r--r-- | cloudinit/distros/__init__.py | 18 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceAzure.py | 2 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceEc2.py | 7 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceGCE.py | 4 | ||||
-rw-r--r-- | cloudinit/sources/__init__.py | 7 | ||||
-rw-r--r-- | config/cloud.cfg | 1 | ||||
-rw-r--r-- | packages/debian/control.in | 1 | ||||
-rw-r--r-- | tests/unittests/test__init__.py | 38 | ||||
-rw-r--r-- | tests/unittests/test_datasource/test_azure.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_distros/test_generic.py | 22 |
12 files changed, 102 insertions, 69 deletions
@@ -52,6 +52,10 @@ - cc_growpart: fix specification of 'devices' list (LP: #1465436) - CloudStack: fix password setting on cloudstack > 4.5.1 (LP: #1464253) - GCE: fix determination of availability zone (LP: #1470880) + - ssh: generate ed25519 host keys (LP: #1461242) + - distro mirrors: provide datasource to mirror selection code to support + GCE regional mirrors. (LP: #1470890) + - add udev rules that identify ephemeral device on Azure (LP: #1411582) 0.7.6: - open 0.7.6 - Enable vendordata on CloudSigma datasource (LP: #1303986) diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py index ab6940fa..5bd2dec6 100644 --- a/cloudinit/config/cc_ssh.py +++ b/cloudinit/config/cc_ssh.py @@ -20,6 +20,7 @@ import glob import os +import sys # Ensure this is aliased to a name not 'distros' # since the module attribute 'distros' @@ -33,26 +34,18 @@ DISABLE_ROOT_OPTS = ("no-port-forwarding,no-agent-forwarding," "no-X11-forwarding,command=\"echo \'Please login as the user \\\"$USER\\\" " "rather than the user \\\"root\\\".\';echo;sleep 10\"") -KEY_2_FILE = { - "rsa_private": ("/etc/ssh/ssh_host_rsa_key", 0o600), - "rsa_public": ("/etc/ssh/ssh_host_rsa_key.pub", 0o644), - "dsa_private": ("/etc/ssh/ssh_host_dsa_key", 0o600), - "dsa_public": ("/etc/ssh/ssh_host_dsa_key.pub", 0o644), - "ecdsa_private": ("/etc/ssh/ssh_host_ecdsa_key", 0o600), - "ecdsa_public": ("/etc/ssh/ssh_host_ecdsa_key.pub", 0o644), -} - -PRIV_2_PUB = { - 'rsa_private': 'rsa_public', - 'dsa_private': 'dsa_public', - 'ecdsa_private': 'ecdsa_public', -} - -KEY_GEN_TPL = 'o=$(ssh-keygen -yf "%s") && echo "$o" root@localhost > "%s"' +GENERATE_KEY_NAMES = ['rsa', 'dsa', 'ecdsa', 'ed25519'] +KEY_FILE_TPL = '/etc/ssh/ssh_host_%s_key' -GENERATE_KEY_NAMES = ['rsa', 'dsa', 'ecdsa'] +CONFIG_KEY_TO_FILE = {} +PRIV_TO_PUB = {} +for k in GENERATE_KEY_NAMES: + CONFIG_KEY_TO_FILE.update({"%s_private" % k: (KEY_FILE_TPL % k, 0o600)}) + CONFIG_KEY_TO_FILE.update( + {"%s_public" % k: (KEY_FILE_TPL % k + ".pub", 0o600)}) + PRIV_TO_PUB["%s_private" % k] = "%s_public" % k -KEY_FILE_TPL = '/etc/ssh/ssh_host_%s_key' +KEY_GEN_TPL = 'o=$(ssh-keygen -yf "%s") && echo "$o" root@localhost > "%s"' def handle(_name, cfg, cloud, log, _args): @@ -69,15 +62,15 @@ def handle(_name, cfg, cloud, log, _args): if "ssh_keys" in cfg: # if there are keys in cloud-config, use them for (key, val) in cfg["ssh_keys"].items(): - if key in KEY_2_FILE: - tgt_fn = KEY_2_FILE[key][0] - tgt_perms = KEY_2_FILE[key][1] + if key in CONFIG_KEY_TO_FILE: + tgt_fn = CONFIG_KEY_TO_FILE[key][0] + tgt_perms = CONFIG_KEY_TO_FILE[key][1] util.write_file(tgt_fn, val, tgt_perms) - for (priv, pub) in PRIV_2_PUB.items(): + for (priv, pub) in PRIV_TO_PUB.items(): if pub in cfg['ssh_keys'] or priv not in cfg['ssh_keys']: continue - pair = (KEY_2_FILE[priv][0], KEY_2_FILE[pub][0]) + pair = (CONFIG_KEY_TO_FILE[priv][0], CONFIG_KEY_TO_FILE[pub][0]) cmd = ['sh', '-xc', KEY_GEN_TPL % pair] try: # TODO(harlowja): Is this guard needed? @@ -92,18 +85,28 @@ def handle(_name, cfg, cloud, log, _args): genkeys = util.get_cfg_option_list(cfg, 'ssh_genkeytypes', GENERATE_KEY_NAMES) + lang_c = os.environ.copy() + lang_c['LANG'] = 'C' for keytype in genkeys: keyfile = KEY_FILE_TPL % (keytype) + if os.path.exists(keyfile): + continue util.ensure_dir(os.path.dirname(keyfile)) - if not os.path.exists(keyfile): - cmd = ['ssh-keygen', '-t', keytype, '-N', '', '-f', keyfile] + cmd = ['ssh-keygen', '-t', keytype, '-N', '', '-f', keyfile] + + # TODO(harlowja): Is this guard needed? + with util.SeLinuxGuard("/etc/ssh", recursive=True): try: - # TODO(harlowja): Is this guard needed? - with util.SeLinuxGuard("/etc/ssh", recursive=True): - util.subp(cmd, capture=False) - except: - util.logexc(log, "Failed generating key type %s to " - "file %s", keytype, keyfile) + out, err = util.subp(cmd, capture=True, env=lang_c) + sys.stdout.write(util.decode_binary(out)) + except util.ProcessExecutionError as e: + err = util.decode_binary(e.stderr).lower() + if (e.exit_code == 1 and + err.lower().startswith("unknown key")): + log.debug("ssh-keygen: unknown key type '%s'", keytype) + else: + util.logexc(log, "Failed generating key type %s to " + "file %s", keytype, keyfile) try: (users, _groups) = ds.normalize_users_groups(cfg, cloud.distro) diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 8a947867..71884b32 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -117,12 +117,11 @@ class Distro(object): arch = self.get_primary_arch() return _get_arch_package_mirror_info(mirror_info, arch) - def get_package_mirror_info(self, arch=None, - availability_zone=None): + def get_package_mirror_info(self, arch=None, data_source=None): # This resolves the package_mirrors config option # down to a single dict of {mirror_name: mirror_url} arch_info = self._get_arch_package_mirror_info(arch) - return _get_package_mirror_info(availability_zone=availability_zone, + return _get_package_mirror_info(data_source=data_source, mirror_info=arch_info) def apply_network(self, settings, bring_up=True): @@ -556,7 +555,7 @@ class Distro(object): LOG.info("Added user '%s' to group '%s'" % (member, name)) -def _get_package_mirror_info(mirror_info, availability_zone=None, +def _get_package_mirror_info(mirror_info, data_source=None, mirror_filter=util.search_for_mirror): # given a arch specific 'mirror_info' entry (from package_mirrors) # search through the 'search' entries, and fallback appropriately @@ -572,11 +571,14 @@ def _get_package_mirror_info(mirror_info, availability_zone=None, ec2_az_re = ("^[a-z][a-z]-(%s)-[1-9][0-9]*[a-z]$" % directions_re) subst = {} - if availability_zone: - subst['availability_zone'] = availability_zone + if data_source and data_source.availability_zone: + subst['availability_zone'] = data_source.availability_zone - if availability_zone and re.match(ec2_az_re, availability_zone): - subst['ec2_region'] = "%s" % availability_zone[0:-1] + if re.match(ec2_az_re, data_source.availability_zone): + subst['ec2_region'] = "%s" % data_source.availability_zone[0:-1] + + if data_source and data_source.region: + subst['region'] = data_source.region results = {} for (name, mirror) in mirror_info.get('failsafe', {}).items(): diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py index 1193d88b..ff950deb 100644 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -433,7 +433,7 @@ def write_files(datadir, files, dirmode=None): elem.text != DEF_PASSWD_REDACTION): elem.text = DEF_PASSWD_REDACTION return ET.tostring(root) - except Exception as e: + except Exception: LOG.critical("failed to redact userpassword in {}".format(fname)) return cnt diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py index 798869b7..0032d06c 100644 --- a/cloudinit/sources/DataSourceEc2.py +++ b/cloudinit/sources/DataSourceEc2.py @@ -197,6 +197,13 @@ class DataSourceEc2(sources.DataSource): except KeyError: return None + @property + def region(self): + az = self.availability_zone + if az is not None: + return az[:-1] + return None + # Used to match classes to dependencies datasources = [ (DataSourceEc2, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), diff --git a/cloudinit/sources/DataSourceGCE.py b/cloudinit/sources/DataSourceGCE.py index 1b28a68c..7e7fc033 100644 --- a/cloudinit/sources/DataSourceGCE.py +++ b/cloudinit/sources/DataSourceGCE.py @@ -152,6 +152,10 @@ class DataSourceGCE(sources.DataSource): def availability_zone(self): return self.metadata['availability-zone'] + @property + def region(self): + return self.availability_zone.rsplit('-', 1)[0] + # Used to match classes to dependencies datasources = [ (DataSourceGCE, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py index 39eab51b..a21c08c2 100644 --- a/cloudinit/sources/__init__.py +++ b/cloudinit/sources/__init__.py @@ -157,6 +157,10 @@ class DataSource(object): return self.metadata.get('availability-zone', self.metadata.get('availability_zone')) + @property + def region(self): + return self.metadata.get('region') + def get_instance_id(self): if not self.metadata or 'instance-id' not in self.metadata: # Return a magic not really instance id string @@ -210,8 +214,7 @@ class DataSource(object): return hostname def get_package_mirror_info(self): - return self.distro.get_package_mirror_info( - availability_zone=self.availability_zone) + return self.distro.get_package_mirror_info(data_source=self) def normalize_pubkey_data(pubkey_data): diff --git a/config/cloud.cfg b/config/cloud.cfg index e96e1781..2b27f379 100644 --- a/config/cloud.cfg +++ b/config/cloud.cfg @@ -104,6 +104,7 @@ system_info: primary: - http://%(ec2_region)s.ec2.archive.ubuntu.com/ubuntu/ - http://%(availability_zone)s.clouds.archive.ubuntu.com/ubuntu/ + - http://%(region)s.clouds.archive.ubuntu.com/ubuntu/ security: [] - arches: [armhf, armel, default] failsafe: diff --git a/packages/debian/control.in b/packages/debian/control.in index 401a542f..5fe16e43 100644 --- a/packages/debian/control.in +++ b/packages/debian/control.in @@ -6,6 +6,7 @@ Maintainer: Scott Moser <smoser@ubuntu.com> Build-Depends: debhelper (>= 9), dh-python, dh-systemd, + iproute2, pyflakes, ${python}, ${test_requires}, diff --git a/tests/unittests/test__init__.py b/tests/unittests/test__init__.py index c32783a6..153f1658 100644 --- a/tests/unittests/test__init__.py +++ b/tests/unittests/test__init__.py @@ -70,8 +70,8 @@ class TestWalkerHandleHandler(TestCase): return_value=self.module_fake) as mockobj: handlers.walker_handle_handler(self.data, self.ctype, self.filename, self.payload) - mockobj.assert_called_with_once(self.expected_module_name) - self.write_file_mock.assert_called_with_once( + mockobj.assert_called_once_with(self.expected_module_name) + self.write_file_mock.assert_called_once_with( self.expected_file_fullname, self.payload, 0o600) self.assertEqual(self.data['handlercount'], 1) @@ -81,8 +81,8 @@ class TestWalkerHandleHandler(TestCase): side_effect=ImportError) as mockobj: handlers.walker_handle_handler(self.data, self.ctype, self.filename, self.payload) - mockobj.assert_called_with_once(self.expected_module_name) - self.write_file_mock.assert_called_with_once( + mockobj.assert_called_once_with(self.expected_module_name) + self.write_file_mock.assert_called_once_with( self.expected_file_fullname, self.payload, 0o600) self.assertEqual(self.data['handlercount'], 0) @@ -93,8 +93,8 @@ class TestWalkerHandleHandler(TestCase): return_value=self.module_fake) as mockobj: handlers.walker_handle_handler(self.data, self.ctype, self.filename, self.payload) - mockobj.assert_called_with_once(self.expected_module_name) - self.write_file_mock.assert_called_with_once( + mockobj.assert_called_once_with(self.expected_module_name) + self.write_file_mock.assert_called_once_with( self.expected_file_fullname, self.payload, 0o600) self.assertEqual(self.data['handlercount'], 0) @@ -122,7 +122,7 @@ class TestHandlerHandlePart(unittest.TestCase): self.frequency, self.headers) # Assert that the handle_part() method of the mock object got # called with the expected arguments. - mod_mock.handle_part.assert_called_with_once( + mod_mock.handle_part.assert_called_once_with( self.data, self.ctype, self.filename, self.payload) def test_normal_version_2(self): @@ -136,8 +136,9 @@ class TestHandlerHandlePart(unittest.TestCase): self.frequency, self.headers) # Assert that the handle_part() method of the mock object got # called with the expected arguments. - mod_mock.handle_part.assert_called_with_once( - self.data, self.ctype, self.filename, self.payload) + mod_mock.handle_part.assert_called_once_with( + self.data, self.ctype, self.filename, self.payload, + settings.PER_INSTANCE) def test_modfreq_per_always(self): """ @@ -150,7 +151,7 @@ class TestHandlerHandlePart(unittest.TestCase): self.frequency, self.headers) # Assert that the handle_part() method of the mock object got # called with the expected arguments. - mod_mock.handle_part.assert_called_with_once( + mod_mock.handle_part.assert_called_once_with( self.data, self.ctype, self.filename, self.payload) def test_no_handle_when_modfreq_once(self): @@ -159,21 +160,20 @@ class TestHandlerHandlePart(unittest.TestCase): mod_mock = mock.Mock(frequency=settings.PER_ONCE) handlers.run_part(mod_mock, self.data, self.filename, self.payload, self.frequency, self.headers) - # Assert that the handle_part() method of the mock object got - # called with the expected arguments. - mod_mock.handle_part.assert_called_with_once( - self.data, self.ctype, self.filename, self.payload) + self.assertEqual(0, mod_mock.handle_part.call_count) def test_exception_is_caught(self): """Exceptions within C{handle_part} are caught and logged.""" mod_mock = mock.Mock(frequency=settings.PER_INSTANCE, handler_version=1) - handlers.run_part(mod_mock, self.data, self.filename, self.payload, - self.frequency, self.headers) mod_mock.handle_part.side_effect = Exception - handlers.run_part(mod_mock, self.data, self.filename, self.payload, - self.frequency, self.headers) - mod_mock.handle_part.assert_called_with_once( + try: + handlers.run_part(mod_mock, self.data, self.filename, + self.payload, self.frequency, self.headers) + except Exception: + self.fail("Exception was not caught in handle_part") + + mod_mock.handle_part.assert_called_once_with( self.data, self.ctype, self.filename, self.payload) diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py index 3b7e3293..8952374f 100644 --- a/tests/unittests/test_datasource/test_azure.py +++ b/tests/unittests/test_datasource/test_azure.py @@ -174,7 +174,7 @@ class TestAzureDataSource(TestCase): def xml_notequals(self, oxml, nxml): try: self.xml_equals(oxml, nxml) - except AssertionError as e: + except AssertionError: return raise AssertionError("XML is the same") diff --git a/tests/unittests/test_distros/test_generic.py b/tests/unittests/test_distros/test_generic.py index 8e3bd78a..6ed1704c 100644 --- a/tests/unittests/test_distros/test_generic.py +++ b/tests/unittests/test_distros/test_generic.py @@ -7,6 +7,11 @@ import os import shutil import tempfile +try: + from unittest import mock +except ImportError: + import mock + unknown_arch_info = { 'arches': ['default'], 'failsafe': {'primary': 'http://fs-primary-default', @@ -144,33 +149,35 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase): def test_get_package_mirror_info_az_ec2(self): arch_mirrors = gapmi(package_mirrors, arch="amd64") + data_source_mock = mock.Mock(availability_zone="us-east-1a") - results = gpmi(arch_mirrors, availability_zone="us-east-1a", + results = gpmi(arch_mirrors, data_source=data_source_mock, mirror_filter=self.return_first) self.assertEqual(results, {'primary': 'http://us-east-1.ec2/', 'security': 'http://security-mirror1-intel'}) - results = gpmi(arch_mirrors, availability_zone="us-east-1a", + results = gpmi(arch_mirrors, data_source=data_source_mock, mirror_filter=self.return_second) self.assertEqual(results, {'primary': 'http://us-east-1a.clouds/', 'security': 'http://security-mirror2-intel'}) - results = gpmi(arch_mirrors, availability_zone="us-east-1a", + results = gpmi(arch_mirrors, data_source=data_source_mock, mirror_filter=self.return_none) self.assertEqual(results, package_mirrors[0]['failsafe']) def test_get_package_mirror_info_az_non_ec2(self): arch_mirrors = gapmi(package_mirrors, arch="amd64") + data_source_mock = mock.Mock(availability_zone="nova.cloudvendor") - results = gpmi(arch_mirrors, availability_zone="nova.cloudvendor", + results = gpmi(arch_mirrors, data_source=data_source_mock, mirror_filter=self.return_first) self.assertEqual(results, {'primary': 'http://nova.cloudvendor.clouds/', 'security': 'http://security-mirror1-intel'}) - results = gpmi(arch_mirrors, availability_zone="nova.cloudvendor", + results = gpmi(arch_mirrors, data_source=data_source_mock, mirror_filter=self.return_last) self.assertEqual(results, {'primary': 'http://nova.cloudvendor.clouds/', @@ -178,17 +185,18 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase): def test_get_package_mirror_info_none(self): arch_mirrors = gapmi(package_mirrors, arch="amd64") + data_source_mock = mock.Mock(availability_zone=None) # because both search entries here replacement based on # availability-zone, the filter will be called with an empty list and # failsafe should be taken. - results = gpmi(arch_mirrors, availability_zone=None, + results = gpmi(arch_mirrors, data_source=data_source_mock, mirror_filter=self.return_first) self.assertEqual(results, {'primary': 'http://fs-primary-intel', 'security': 'http://security-mirror1-intel'}) - results = gpmi(arch_mirrors, availability_zone=None, + results = gpmi(arch_mirrors, data_source=data_source_mock, mirror_filter=self.return_last) self.assertEqual(results, {'primary': 'http://fs-primary-intel', |