summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog4
-rw-r--r--cloudinit/config/cc_ssh.py65
-rw-r--r--cloudinit/distros/__init__.py18
-rw-r--r--cloudinit/sources/DataSourceAzure.py2
-rw-r--r--cloudinit/sources/DataSourceEc2.py7
-rw-r--r--cloudinit/sources/DataSourceGCE.py4
-rw-r--r--cloudinit/sources/__init__.py7
-rw-r--r--config/cloud.cfg1
-rw-r--r--packages/debian/control.in1
-rw-r--r--tests/unittests/test__init__.py38
-rw-r--r--tests/unittests/test_datasource/test_azure.py2
-rw-r--r--tests/unittests/test_distros/test_generic.py22
12 files changed, 102 insertions, 69 deletions
diff --git a/ChangeLog b/ChangeLog
index b6980eb0..661e968b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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',