From a1b185d0cce5064e9b36b4db7b55564e2ab1d7a8 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 20 Oct 2016 22:53:17 -0400 Subject: Get early logging logged, including failures of cmdline url. Failures to load the kernel command line's url (cloud-config-url=) would previously get swallowed. This should make it much more obvious when that happens. With logging going to expected places at sane levels (WARN will go to stderr by default). --- tests/unittests/helpers.py | 16 ++++--- tests/unittests/test__init__.py | 92 ++++++++++++++++++++++++++--------------- 2 files changed, 70 insertions(+), 38 deletions(-) (limited to 'tests') diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index cf3b46d2..64e56d98 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -264,16 +264,22 @@ class HttprettyTestCase(TestCase): class TempDirTestCase(TestCase): # provide a tempdir per class, not per test. - def setUp(self): - super(TempDirTestCase, self).setUp() - self.tmp = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.tmp) + @classmethod + def setUpClass(cls): + cls.tmpd = tempfile.mkdtemp(prefix="ci-%s." % cls.__name__) + return TestCase.setUpClass() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.tmpd) + return TestCase.tearDownClass() def tmp_path(self, path): + # if absolute path (starts with /), then make ./path if path.startswith(os.path.sep): path = "." + path - return os.path.normpath(os.path.join(self.tmp, path)) + return os.path.normpath(os.path.join(self.tmpd, path)) def populate_dir(path, files): diff --git a/tests/unittests/test__init__.py b/tests/unittests/test__init__.py index 7b6f8c4e..e6f4c318 100644 --- a/tests/unittests/test__init__.py +++ b/tests/unittests/test__init__.py @@ -1,16 +1,18 @@ # This file is part of cloud-init. See LICENSE file for license information. +import logging import os import shutil import tempfile +from cloudinit.cmd import main from cloudinit import handlers from cloudinit import helpers from cloudinit import settings from cloudinit import url_helper from cloudinit import util -from .helpers import TestCase, ExitStack, mock +from .helpers import TestCase, TempDirTestCase, ExitStack, mock class FakeModule(handlers.Handler): @@ -170,44 +172,68 @@ class TestHandlerHandlePart(TestCase): self.data, self.ctype, self.filename, self.payload) -class TestCmdlineUrl(TestCase): - def test_invalid_content(self): - url = "http://example.com/foo" - key = "mykey" - payload = b"0" - cmdline = "ro %s=%s bar=1" % (key, url) +class TestCmdlineUrl(TempDirTestCase): + def test_parse_cmdline_url_nokey_raises_keyerror(self): + self.assertRaises( + KeyError, main.parse_cmdline_url, 'root=foo bar single') - with mock.patch('cloudinit.url_helper.readurl', - return_value=url_helper.StringResponse(payload)): - self.assertEqual( - util.get_cmdline_url(names=[key], starts="xxxxxx", - cmdline=cmdline), - (key, url, None)) + def test_parse_cmdline_url_found(self): + cmdline = 'root=foo bar single url=http://example.com arg1 -v' + self.assertEqual( + ('url', 'http://example.com'), main.parse_cmdline_url(cmdline)) - def test_valid_content(self): - url = "http://example.com/foo" - key = "mykey" - payload = b"xcloud-config\nmydata: foo\nbar: wark\n" + @mock.patch('cloudinit.cmd.main.util.read_file_or_url') + def test_invalid_content(self, m_read): + key = "cloud-config-url" + url = 'http://example.com/foo' cmdline = "ro %s=%s bar=1" % (key, url) + m_read.return_value = url_helper.StringResponse(b"unexpected blob") - with mock.patch('cloudinit.url_helper.readurl', - return_value=url_helper.StringResponse(payload)): - self.assertEqual( - util.get_cmdline_url(names=[key], starts=b"xcloud-config", - cmdline=cmdline), - (key, url, payload)) + fpath = self.tmp_path("test_valid") + lvl, msg = main.attempt_cmdline_url( + fpath, network=True, cmdline=cmdline) + self.assertEqual(logging.WARN, lvl) + self.assertIn(url, msg) + self.assertFalse(os.path.exists(fpath)) - def test_no_key_found(self): + @mock.patch('cloudinit.cmd.main.util.read_file_or_url') + def test_valid_content(self, m_read): url = "http://example.com/foo" - key = "mykey" - cmdline = "ro %s=%s bar=1" % (key, url) - - with mock.patch('cloudinit.url_helper.readurl', - return_value=url_helper.StringResponse(b'')): - self.assertEqual( - util.get_cmdline_url(names=["does-not-appear"], - starts="#cloud-config", cmdline=cmdline), - (None, None, None)) + payload = b"#cloud-config\nmydata: foo\nbar: wark\n" + cmdline = "ro %s=%s bar=1" % ('cloud-config-url', url) + + m_read.return_value = url_helper.StringResponse(payload) + fpath = self.tmp_path("test_valid") + lvl, msg = main.attempt_cmdline_url( + fpath, network=True, cmdline=cmdline) + self.assertEqual(util.load_file(fpath, decode=False), payload) + self.assertEqual(logging.INFO, lvl) + self.assertIn(url, msg) + + @mock.patch('cloudinit.cmd.main.util.read_file_or_url') + def test_no_key_found(self, m_read): + cmdline = "ro mykey=http://example.com/foo root=foo" + fpath = self.tmp_path("test_no_key_found") + lvl, msg = main.attempt_cmdline_url( + fpath, network=True, cmdline=cmdline) + + m_read.assert_not_called() + self.assertFalse(os.path.exists(fpath)) + self.assertEqual(logging.DEBUG, lvl) + + @mock.patch('cloudinit.cmd.main.util.read_file_or_url') + def test_exception_warns(self, m_read): + url = "http://example.com/foo" + cmdline = "ro cloud-config-url=%s root=LABEL=bar" % url + fpath = self.tmp_path("test_no_key_found") + m_read.side_effect = url_helper.UrlError( + cause="Unexpected Error", url="http://example.com/foo") + + lvl, msg = main.attempt_cmdline_url( + fpath, network=True, cmdline=cmdline) + self.assertEqual(logging.WARN, lvl) + self.assertIn(url, msg) + self.assertFalse(os.path.exists(fpath)) # vi: ts=4 expandtab -- cgit v1.2.3 From 4cf53f1544f8f5629330eab3efef1a18255c277a Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Tue, 17 Jan 2017 08:53:22 -0500 Subject: OpenStack: Use timeout and retries from config in get_data. This modifies get_data in DataSourceOpenStack.py to get the timeout and retries values from the data source configuration, rather than from keyword arguments. This permits get_data to use the same timeout as other methods, and allows an operator to increase the timeout in environments where the metadata service takes longer than five seconds to respond. LP: #1657130 Resolves: rhbz#1408589 --- cloudinit/sources/DataSourceOpenStack.py | 15 ++++++++++++--- tests/unittests/test_datasource/test_openstack.py | 8 ++++---- 2 files changed, 16 insertions(+), 7 deletions(-) (limited to 'tests') diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py index 2a58f1cd..e1ea21f8 100644 --- a/cloudinit/sources/DataSourceOpenStack.py +++ b/cloudinit/sources/DataSourceOpenStack.py @@ -45,6 +45,7 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): # max_wait < 0 indicates do not wait max_wait = -1 timeout = 10 + retries = 5 try: max_wait = int(self.ds_cfg.get("max_wait", max_wait)) @@ -55,7 +56,13 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): timeout = max(0, int(self.ds_cfg.get("timeout", timeout))) except Exception: util.logexc(LOG, "Failed to get timeout, using %s", timeout) - return (max_wait, timeout) + + try: + retries = int(self.ds_cfg.get("retries", retries)) + except Exception: + util.logexc(LOG, "Failed to get max wait. using %s", retries) + + return (max_wait, timeout, retries) def wait_for_metadata_service(self): urls = self.ds_cfg.get("metadata_urls", [DEF_MD_URL]) @@ -76,7 +83,7 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): md_urls.append(md_url) url2base[md_url] = url - (max_wait, timeout) = self._get_url_settings() + (max_wait, timeout, retries) = self._get_url_settings() start_time = time.time() avail_url = url_helper.wait_for_url(urls=md_urls, max_wait=max_wait, timeout=timeout) @@ -89,13 +96,15 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): self.metadata_address = url2base.get(avail_url) return bool(avail_url) - def get_data(self, retries=5, timeout=5): + def get_data(self): try: if not self.wait_for_metadata_service(): return False except IOError: return False + (max_wait, timeout, retries) = self._get_url_settings() + try: results = util.log_time(LOG.debug, 'Crawl of openstack metadata service', diff --git a/tests/unittests/test_datasource/test_openstack.py b/tests/unittests/test_datasource/test_openstack.py index e5b6fcc6..28e1833e 100644 --- a/tests/unittests/test_datasource/test_openstack.py +++ b/tests/unittests/test_datasource/test_openstack.py @@ -232,7 +232,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase): None, helpers.Paths({})) self.assertIsNone(ds_os.version) - found = ds_os.get_data(timeout=0.1, retries=0) + found = ds_os.get_data() self.assertTrue(found) self.assertEqual(2, ds_os.version) md = dict(ds_os.metadata) @@ -256,7 +256,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase): None, helpers.Paths({})) self.assertIsNone(ds_os.version) - found = ds_os.get_data(timeout=0.1, retries=0) + found = ds_os.get_data() self.assertFalse(found) self.assertIsNone(ds_os.version) @@ -275,7 +275,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase): 'timeout': 0, } self.assertIsNone(ds_os.version) - found = ds_os.get_data(timeout=0.1, retries=0) + found = ds_os.get_data() self.assertFalse(found) self.assertIsNone(ds_os.version) @@ -298,7 +298,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase): 'timeout': 0, } self.assertIsNone(ds_os.version) - found = ds_os.get_data(timeout=0.1, retries=0) + found = ds_os.get_data() self.assertFalse(found) self.assertIsNone(ds_os.version) -- cgit v1.2.3 From b71592ce0e0a9f9f9f225315015ca57b312ad30d Mon Sep 17 00:00:00 2001 From: Andrew Jorgensen Date: Tue, 1 Nov 2016 10:54:31 -0400 Subject: EC2: Do not cache security credentials on disk On EC2, instance metadata can include credentials that remain valid for as much as 6 hours. Reading these and allowing them to be pickled represents a potential vulnerability if a snapshot of the disk is taken and shared as part of an AMI. This skips security-credentials when walking the meta-data tree. LP: #1638312 Reviewed-by: Ian Weller Reviewed-by: Ben Cressey Reported-by: Kyle Barnes --- cloudinit/ec2_utils.py | 3 +++ tests/unittests/test_ec2_util.py | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) (limited to 'tests') diff --git a/cloudinit/ec2_utils.py b/cloudinit/ec2_utils.py index c656ef14..0c16ae47 100644 --- a/cloudinit/ec2_utils.py +++ b/cloudinit/ec2_utils.py @@ -82,6 +82,9 @@ class MetadataMaterializer(object): field_name = get_name(field) if not field or not field_name: continue + # Don't materialize credentials + if field_name == 'security-credentials': + continue if has_children(field): if field_name not in children: children.append(field_name) diff --git a/tests/unittests/test_ec2_util.py b/tests/unittests/test_ec2_util.py index 4a33d747..71c2009f 100644 --- a/tests/unittests/test_ec2_util.py +++ b/tests/unittests/test_ec2_util.py @@ -140,4 +140,49 @@ class TestEc2Util(helpers.HttprettyTestCase): self.assertEqual(bdm['ami'], 'sdb') self.assertEqual(bdm['ephemeral0'], 'sdc') + @hp.activate + def test_metadata_no_security_credentials(self): + base_url = 'http://169.254.169.254/%s/meta-data/' % (self.VERSION) + hp.register_uri(hp.GET, base_url, status=200, + body="\n".join(['instance-id', + 'iam/'])) + hp.register_uri(hp.GET, uh.combine_url(base_url, 'instance-id'), + status=200, body='i-0123451689abcdef0') + hp.register_uri(hp.GET, + uh.combine_url(base_url, 'iam/'), + status=200, + body="\n".join(['info/', 'security-credentials/'])) + hp.register_uri(hp.GET, + uh.combine_url(base_url, 'iam/info/'), + status=200, + body='LastUpdated') + hp.register_uri(hp.GET, + uh.combine_url(base_url, 'iam/info/LastUpdated'), + status=200, body='2016-10-27T17:29:39Z') + hp.register_uri(hp.GET, + uh.combine_url(base_url, 'iam/security-credentials/'), + status=200, + body='ReadOnly/') + hp.register_uri(hp.GET, + uh.combine_url(base_url, + 'iam/security-credentials/ReadOnly/'), + status=200, + body="\n".join(['LastUpdated', 'Expiration'])) + hp.register_uri(hp.GET, + uh.combine_url( + base_url, + 'iam/security-credentials/ReadOnly/LastUpdated'), + status=200, body='2016-10-27T17:28:17Z') + hp.register_uri(hp.GET, + uh.combine_url( + base_url, + 'iam/security-credentials/ReadOnly/Expiration'), + status=200, body='2016-10-28T00:00:34Z') + md = eu.get_instance_metadata(self.VERSION, retries=0, timeout=0.1) + self.assertEqual(md['instance-id'], 'i-0123451689abcdef0') + iam = md['iam'] + self.assertEqual(1, len(iam)) + self.assertEqual(iam['info']['LastUpdated'], '2016-10-27T17:29:39Z') + self.assertNotIn('security-credentials', iam) + # vi: ts=4 expandtab -- cgit v1.2.3 From d3fbb5df017c7a6e0eb1a146d970db260932d7e8 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Fri, 20 Jan 2017 16:19:23 -0500 Subject: reset httppretty for each test this ensures that we call httpretty.reset() before calling httppretty.register_uri(...), which ensures that we get a fresh callback with the expected version of the metadata. LP: #1658200 --- tests/unittests/test_datasource/test_gce.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'tests') diff --git a/tests/unittests/test_datasource/test_gce.py b/tests/unittests/test_datasource/test_gce.py index 4f667678..a5cced07 100644 --- a/tests/unittests/test_datasource/test_gce.py +++ b/tests/unittests/test_datasource/test_gce.py @@ -59,6 +59,8 @@ def _set_mock_metadata(gce_meta=None): else: return (404, headers, '') + # reset is needed. https://github.com/gabrielfalcao/HTTPretty/issues/316 + httpretty.reset() httpretty.register_uri(httpretty.GET, MD_URL_RE, body=_request_callback) -- cgit v1.2.3 From 853df0a0e85002582694b88db886f206f64b23c7 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Fri, 20 Jan 2017 14:32:08 -0500 Subject: Add 3 ecdsa-sha2-nistp* ssh key types now that they are standardized cloud-init adds ssh_authorized_keys to the default user and to root but for root it disables the keys with a prefix command. However, if the public_key key is of type ecdsa-sha2-nistp521, it is not parsed correctly, and the prefix command is not prepended. Resolves: rhbz#1151824 LP: #1658174 --- cloudinit/ssh_util.py | 3 +++ tests/unittests/test_sshutil.py | 24 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py index be8a49e8..b95b956f 100644 --- a/cloudinit/ssh_util.py +++ b/cloudinit/ssh_util.py @@ -22,8 +22,11 @@ DEF_SSHD_CFG = "/etc/ssh/sshd_config" VALID_KEY_TYPES = ( "dsa", "ecdsa", + "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp256-cert-v01@openssh.com", + "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp384-cert-v01@openssh.com", + "ecdsa-sha2-nistp521", "ecdsa-sha2-nistp521-cert-v01@openssh.com", "ed25519", "rsa", diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py index 55971b5e..991f45a6 100644 --- a/tests/unittests/test_sshutil.py +++ b/tests/unittests/test_sshutil.py @@ -32,6 +32,22 @@ VALID_CONTENT = { "YWpMfYdPUnE7u536WqzFmsaqJctz3gBxH9Ex7dFtrxR4qiqEr9Qtlu3xGn7Bw07" "/+i1D+ey3ONkZLN+LQ714cgj8fRS4Hj29SCmXp5Kt5/82cD/VN3NtHw==" ), + 'ecdsa-sha2-nistp256': ( + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMy/WuXq5MF" + "r5hVQ9EEKKUTF7vUaOkgxUh6bNsCs9SFMVslIm1zM/WJYwUv52LdEePjtDYiV4A" + "l2XthJ9/bs7Pc=" + ), + 'ecdsa-sha2-nistp521': ( + "AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBABOdNTkh9F" + "McK4hZRLs5LTXBEXwNr0+Yg9uvJYRFcz2ZlnjYX9tM4Z3QQFjqogU4pU+zpKLqZ" + "5VE4Jcnb1T608UywBIdXkSFZT8trGJqBv9nFWGgmTX3KP8kiBbihpuv1cGwglPl" + "Hxs50A42iP0JiT7auGtEAGsu/uMql323GTGb4171Q==" + ), + 'ecdsa-sha2-nistp384': ( + "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBAnoqFU9Gnl" + "LcsEuCJnobs/c6whzvjCgouaOO61kgXNtIxyF4Wkutg6xaGYgBBt/phb7a2TurI" + "bcIBuzJ/mP22UyUAbNnBfStAEBmYbrTf1EfiMCYUAr1XnL0UdYmZ8HFg==" + ), } TEST_OPTIONS = ( @@ -44,7 +60,13 @@ class TestAuthKeyLineParser(test_helpers.TestCase): def test_simple_parse(self): # test key line with common 3 fields (keytype, base64, comment) parser = ssh_util.AuthKeyLineParser() - for ktype in ['rsa', 'ecdsa', 'dsa']: + ecdsa_types = [ + 'ecdsa-sha2-nistp256', + 'ecdsa-sha2-nistp384', + 'ecdsa-sha2-nistp521', + ] + + for ktype in ['rsa', 'ecdsa', 'dsa'] + ecdsa_types: content = VALID_CONTENT[ktype] comment = 'user-%s@host' % ktype line = ' '.join((ktype, content, comment,)) -- cgit v1.2.3 From 8e9e47f2f06b4dcf017149699111e94ba7c1830b Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 24 Jan 2017 11:37:08 -0500 Subject: tests: No longer monkey patch httpretty for python 3.4.2 No shipping ubuntu has a python 3.4 that is less than 3.4.2. Remove this workaround to avoid unnecessary complexity. This reverts 04a60cf949. --- tests/unittests/helpers.py | 35 ----------------------- tests/unittests/test_datasource/test_gce.py | 2 +- tests/unittests/test_datasource/test_openstack.py | 3 +- tests/unittests/test_ec2_util.py | 4 +-- 4 files changed, 4 insertions(+), 40 deletions(-) (limited to 'tests') diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index 64e56d98..a0933464 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -2,7 +2,6 @@ from __future__ import print_function -import functools import os import shutil import sys @@ -29,7 +28,6 @@ PY2 = False PY26 = False PY27 = False PY3 = False -FIX_HTTPRETTY = False _PY_VER = sys.version_info _PY_MAJOR, _PY_MINOR, _PY_MICRO = _PY_VER[0:3] @@ -44,8 +42,6 @@ else: PY2 = True if (_PY_MAJOR, _PY_MINOR) >= (3, 0): PY3 = True - if _PY_MINOR == 4 and _PY_MICRO < 3: - FIX_HTTPRETTY = True # Makes the old path start @@ -216,37 +212,6 @@ class FilesystemMockingTestCase(ResourceUsingTestCase): return root -def import_httpretty(): - """Import HTTPretty and monkey patch Python 3.4 issue. - See https://github.com/gabrielfalcao/HTTPretty/pull/193 and - as well as https://github.com/gabrielfalcao/HTTPretty/issues/221. - - Lifted from - https://github.com/inveniosoftware/datacite/blob/master/tests/helpers.py - """ - if not FIX_HTTPRETTY: - import httpretty - else: - import socket - old_SocketType = socket.SocketType - - import httpretty - from httpretty import core - - def sockettype_patch(f): - @functools.wraps(f) - def inner(*args, **kwargs): - f(*args, **kwargs) - socket.SocketType = old_SocketType - socket.__dict__['SocketType'] = old_SocketType - return inner - - core.httpretty.disable = sockettype_patch( - httpretty.httpretty.disable - ) - return httpretty - - class HttprettyTestCase(TestCase): # necessary as http_proxy gets in the way of httpretty # https://github.com/gabrielfalcao/HTTPretty/issues/122 diff --git a/tests/unittests/test_datasource/test_gce.py b/tests/unittests/test_datasource/test_gce.py index a5cced07..4f83454e 100644 --- a/tests/unittests/test_datasource/test_gce.py +++ b/tests/unittests/test_datasource/test_gce.py @@ -4,6 +4,7 @@ # # This file is part of cloud-init. See LICENSE file for license information. +import httpretty import re from base64 import b64encode, b64decode @@ -15,7 +16,6 @@ from cloudinit.sources import DataSourceGCE from .. import helpers as test_helpers -httpretty = test_helpers.import_httpretty() GCE_META = { 'instance/id': '123', diff --git a/tests/unittests/test_datasource/test_openstack.py b/tests/unittests/test_datasource/test_openstack.py index 28e1833e..7bf55084 100644 --- a/tests/unittests/test_datasource/test_openstack.py +++ b/tests/unittests/test_datasource/test_openstack.py @@ -5,6 +5,7 @@ # This file is part of cloud-init. See LICENSE file for license information. import copy +import httpretty as hp import json import re @@ -20,8 +21,6 @@ from cloudinit.sources import DataSourceOpenStack as ds from cloudinit.sources.helpers import openstack from cloudinit import util -hp = test_helpers.import_httpretty() - BASE_URL = "http://169.254.169.254" PUBKEY = u'ssh-rsa AAAAB3NzaC1....sIkJhq8wdX+4I3A4cYbYP ubuntu@server-460\n' EC2_META = { diff --git a/tests/unittests/test_ec2_util.py b/tests/unittests/test_ec2_util.py index 71c2009f..65fdb519 100644 --- a/tests/unittests/test_ec2_util.py +++ b/tests/unittests/test_ec2_util.py @@ -1,12 +1,12 @@ # This file is part of cloud-init. See LICENSE file for license information. +import httpretty as hp + from . import helpers from cloudinit import ec2_utils as eu from cloudinit import url_helper as uh -hp = helpers.import_httpretty() - class TestEc2Util(helpers.HttprettyTestCase): VERSION = 'latest' -- cgit v1.2.3 From 65e01b463cee0bdb8c8b415e78abfcc3262aad89 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 19 Jan 2017 17:31:34 -0500 Subject: tests: remove executable bit on test_net, so it runs, and fix it. The test_user_data_normalize and test_net files had gotten the executable bit set, and thus are skipped by nose by default. We could set run with the --exe flag, but they should not have gotten this way. Other changes here: * replace TempDirTestCase with CiTestCase, which has some nice tmp_dir() and tmp_path() functions. Going forward the intent is to have CiTestCase be the base test case for tests. * test_net: switch to CiTestCase and fix usage that was silently broken, because of exe bit. * populate_dir: return the list of files that it writes rather than having no return value. * CiTestCase: * support tmp_path("foo") that returns a full path to 'foo' under a tmpdir. * add tmp_dir() to get a temp dir and clean up. --- tests/unittests/helpers.py | 47 +++++++++++-------- tests/unittests/test__init__.py | 12 ++--- tests/unittests/test_atomic_helper.py | 4 +- .../test_distros/test_user_data_normalize.py | 0 tests/unittests/test_net.py | 53 ++++++++-------------- 5 files changed, 55 insertions(+), 61 deletions(-) mode change 100755 => 100644 tests/unittests/test_distros/test_user_data_normalize.py mode change 100755 => 100644 tests/unittests/test_net.py (limited to 'tests') diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index a0933464..90e2431f 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -2,6 +2,7 @@ from __future__ import print_function +import functools import os import shutil import sys @@ -82,6 +83,28 @@ class TestCase(unittest2.TestCase): pass +class CiTestCase(TestCase): + """This is the preferred test case base class unless user + needs other test case classes below.""" + def tmp_dir(self, dir=None, cleanup=True): + # return a full path to a temporary directory that will be cleaned up. + if dir is None: + tmpd = tempfile.mkdtemp( + prefix="ci-%s." % self.__class__.__name__) + else: + tmpd = tempfile.mkdtemp(dir=dir) + self.addCleanup(functools.partial(shutil.rmtree, tmpd)) + return tmpd + + def tmp_path(self, path, dir=None): + # return an absolute path to 'path' under dir. + # if dir is None, one will be created with tmp_dir() + # the file is not created or modified. + if dir is None: + dir = self.tmp_dir() + return os.path.normpath(os.path.abspath(os.path.join(dir, path))) + + class ResourceUsingTestCase(TestCase): def setUp(self): super(ResourceUsingTestCase, self).setUp() @@ -227,29 +250,10 @@ class HttprettyTestCase(TestCase): super(HttprettyTestCase, self).tearDown() -class TempDirTestCase(TestCase): - # provide a tempdir per class, not per test. - @classmethod - def setUpClass(cls): - cls.tmpd = tempfile.mkdtemp(prefix="ci-%s." % cls.__name__) - return TestCase.setUpClass() - - @classmethod - def tearDownClass(cls): - shutil.rmtree(cls.tmpd) - return TestCase.tearDownClass() - - def tmp_path(self, path): - # if absolute path (starts with /), then make ./path - if path.startswith(os.path.sep): - path = "." + path - - return os.path.normpath(os.path.join(self.tmpd, path)) - - def populate_dir(path, files): if not os.path.exists(path): os.makedirs(path) + ret = [] for (name, content) in files.items(): p = os.path.join(path, name) util.ensure_dir(os.path.dirname(p)) @@ -259,6 +263,9 @@ def populate_dir(path, files): else: fp.write(content.encode('utf-8')) fp.close() + ret.append(p) + + return ret def dir2dict(startdir, prefix=None): diff --git a/tests/unittests/test__init__.py b/tests/unittests/test__init__.py index e6f4c318..781f6d54 100644 --- a/tests/unittests/test__init__.py +++ b/tests/unittests/test__init__.py @@ -12,7 +12,7 @@ from cloudinit import settings from cloudinit import url_helper from cloudinit import util -from .helpers import TestCase, TempDirTestCase, ExitStack, mock +from .helpers import TestCase, CiTestCase, ExitStack, mock class FakeModule(handlers.Handler): @@ -172,7 +172,7 @@ class TestHandlerHandlePart(TestCase): self.data, self.ctype, self.filename, self.payload) -class TestCmdlineUrl(TempDirTestCase): +class TestCmdlineUrl(CiTestCase): def test_parse_cmdline_url_nokey_raises_keyerror(self): self.assertRaises( KeyError, main.parse_cmdline_url, 'root=foo bar single') @@ -189,7 +189,7 @@ class TestCmdlineUrl(TempDirTestCase): cmdline = "ro %s=%s bar=1" % (key, url) m_read.return_value = url_helper.StringResponse(b"unexpected blob") - fpath = self.tmp_path("test_valid") + fpath = self.tmp_path("ccfile") lvl, msg = main.attempt_cmdline_url( fpath, network=True, cmdline=cmdline) self.assertEqual(logging.WARN, lvl) @@ -203,7 +203,7 @@ class TestCmdlineUrl(TempDirTestCase): cmdline = "ro %s=%s bar=1" % ('cloud-config-url', url) m_read.return_value = url_helper.StringResponse(payload) - fpath = self.tmp_path("test_valid") + fpath = self.tmp_path("ccfile") lvl, msg = main.attempt_cmdline_url( fpath, network=True, cmdline=cmdline) self.assertEqual(util.load_file(fpath, decode=False), payload) @@ -213,7 +213,7 @@ class TestCmdlineUrl(TempDirTestCase): @mock.patch('cloudinit.cmd.main.util.read_file_or_url') def test_no_key_found(self, m_read): cmdline = "ro mykey=http://example.com/foo root=foo" - fpath = self.tmp_path("test_no_key_found") + fpath = self.tmp_path("ccpath") lvl, msg = main.attempt_cmdline_url( fpath, network=True, cmdline=cmdline) @@ -225,7 +225,7 @@ class TestCmdlineUrl(TempDirTestCase): def test_exception_warns(self, m_read): url = "http://example.com/foo" cmdline = "ro cloud-config-url=%s root=LABEL=bar" % url - fpath = self.tmp_path("test_no_key_found") + fpath = self.tmp_path("ccfile") m_read.side_effect = url_helper.UrlError( cause="Unexpected Error", url="http://example.com/foo") diff --git a/tests/unittests/test_atomic_helper.py b/tests/unittests/test_atomic_helper.py index e170c7c3..515919d8 100644 --- a/tests/unittests/test_atomic_helper.py +++ b/tests/unittests/test_atomic_helper.py @@ -6,10 +6,10 @@ import stat from cloudinit import atomic_helper -from . import helpers +from .helpers import CiTestCase -class TestAtomicHelper(helpers.TempDirTestCase): +class TestAtomicHelper(CiTestCase): def test_basic_usage(self): """write_file takes bytes if no omode.""" path = self.tmp_path("test_basic_usage") diff --git a/tests/unittests/test_distros/test_user_data_normalize.py b/tests/unittests/test_distros/test_user_data_normalize.py old mode 100755 new mode 100644 diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py old mode 100755 new mode 100644 index 1090282a..2c2bde96 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -8,11 +8,10 @@ from cloudinit.net import sysconfig from cloudinit.sources.helpers import openstack from cloudinit import util +from .helpers import CiTestCase from .helpers import dir2dict from .helpers import mock from .helpers import populate_dir -from .helpers import TempDirTestCase -from .helpers import TestCase import base64 import copy @@ -20,8 +19,6 @@ import gzip import io import json import os -import shutil -import tempfile import textwrap import yaml @@ -478,7 +475,7 @@ def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path.side_effect = sys_dev_path -class TestSysConfigRendering(TestCase): +class TestSysConfigRendering(CiTestCase): @mock.patch("cloudinit.net.sys_dev_path") @mock.patch("cloudinit.net.read_sys_net") @@ -486,8 +483,7 @@ class TestSysConfigRendering(TestCase): def test_default_generation(self, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path): - tmp_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, tmp_dir) + tmp_dir = self.tmp_dir() _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path) @@ -518,9 +514,7 @@ USERCTL=no self.assertEqual(expected_content, content) def test_openstack_rendering_samples(self): - tmp_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, tmp_dir) - render_dir = os.path.join(tmp_dir, "render") + render_dir = self.tmp_dir() for os_sample in OS_SAMPLES: ex_input = os_sample['in_data'] ex_mac_addrs = os_sample['in_macs'] @@ -535,7 +529,7 @@ USERCTL=no self.assertEqual(expected_content, fh.read()) -class TestEniNetRendering(TestCase): +class TestEniNetRendering(CiTestCase): @mock.patch("cloudinit.net.sys_dev_path") @mock.patch("cloudinit.net.read_sys_net") @@ -543,8 +537,7 @@ class TestEniNetRendering(TestCase): def test_default_generation(self, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path): - tmp_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, tmp_dir) + tmp_dir = self.tmp_dir() _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path) @@ -576,7 +569,7 @@ iface eth1000 inet dhcp self.assertEqual(expected.lstrip(), contents.lstrip()) -class TestEniNetworkStateToEni(TestCase): +class TestEniNetworkStateToEni(CiTestCase): mycfg = { 'config': [{"type": "physical", "name": "eth0", "mac_address": "c0:d6:9f:2c:e8:80", @@ -607,7 +600,7 @@ class TestEniNetworkStateToEni(TestCase): self.assertNotIn("hwaddress", rendered) -class TestCmdlineConfigParsing(TestCase): +class TestCmdlineConfigParsing(CiTestCase): simple_cfg = { 'config': [{"type": "physical", "name": "eth0", "mac_address": "c0:d6:9f:2c:e8:80", @@ -665,7 +658,7 @@ class TestCmdlineConfigParsing(TestCase): self.assertEqual(found, self.simple_cfg) -class TestCmdlineReadKernelConfig(TempDirTestCase): +class TestCmdlineReadKernelConfig(CiTestCase): macs = { 'eth0': '14:02:ec:42:48:00', 'eno1': '14:02:ec:42:48:01', @@ -673,8 +666,7 @@ class TestCmdlineReadKernelConfig(TempDirTestCase): def test_ip_cmdline_read_kernel_cmdline_ip(self): content = {'net-eth0.conf': DHCP_CONTENT_1} - populate_dir(self.tmp, content) - files = [os.path.join(self.tmp, k) for k in content.keys()] + files = sorted(populate_dir(self.tmp_dir(), content)) found = cmdline.read_kernel_cmdline_config( files=files, cmdline='foo ip=dhcp', mac_addrs=self.macs) exp1 = copy.deepcopy(DHCP_EXPECTED_1) @@ -684,8 +676,7 @@ class TestCmdlineReadKernelConfig(TempDirTestCase): def test_ip_cmdline_read_kernel_cmdline_ip6(self): content = {'net6-eno1.conf': DHCP6_CONTENT_1} - populate_dir(self.tmp, content) - files = [os.path.join(self.tmp, k) for k in content.keys()] + files = sorted(populate_dir(self.tmp_dir(), content)) found = cmdline.read_kernel_cmdline_config( files=files, cmdline='foo ip6=dhcp root=/dev/sda', mac_addrs=self.macs) @@ -701,8 +692,7 @@ class TestCmdlineReadKernelConfig(TempDirTestCase): def test_ip_cmdline_read_kernel_cmdline_none(self): # if there is no ip= or ip6= on cmdline, return value should be None content = {'net6-eno1.conf': DHCP6_CONTENT_1} - populate_dir(self.tmp, content) - files = [os.path.join(self.tmp, k) for k in content.keys()] + files = sorted(populate_dir(self.tmp_dir(), content)) found = cmdline.read_kernel_cmdline_config( files=files, cmdline='foo root=/dev/sda', mac_addrs=self.macs) self.assertEqual(found, None) @@ -710,8 +700,7 @@ class TestCmdlineReadKernelConfig(TempDirTestCase): def test_ip_cmdline_both_ip_ip6(self): content = {'net-eth0.conf': DHCP_CONTENT_1, 'net6-eth0.conf': DHCP6_CONTENT_1.replace('eno1', 'eth0')} - populate_dir(self.tmp, content) - files = [os.path.join(self.tmp, k) for k in sorted(content.keys())] + files = sorted(populate_dir(self.tmp_dir(), content)) found = cmdline.read_kernel_cmdline_config( files=files, cmdline='foo ip=dhcp ip6=dhcp', mac_addrs=self.macs) @@ -725,14 +714,12 @@ class TestCmdlineReadKernelConfig(TempDirTestCase): self.assertEqual(found['config'], expected) -class TestEniRoundTrip(TestCase): - def setUp(self): - super(TestCase, self).setUp() - self.tmp_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.tmp_dir) - +class TestEniRoundTrip(CiTestCase): def _render_and_read(self, network_config=None, state=None, eni_path=None, - links_prefix=None, netrules_path=None): + links_prefix=None, netrules_path=None, dir=None): + if dir is None: + dir = self.tmp_dir() + if network_config: ns = network_state.parse_net_config_data(network_config) elif state: @@ -747,8 +734,8 @@ class TestEniRoundTrip(TestCase): config={'eni_path': eni_path, 'links_path_prefix': links_prefix, 'netrules_path': netrules_path}) - renderer.render_network_state(self.tmp_dir, ns) - return dir2dict(self.tmp_dir) + renderer.render_network_state(dir, ns) + return dir2dict(dir) def testsimple_convert_and_render(self): network_config = eni.convert_eni_data(EXAMPLE_ENI) -- cgit v1.2.3 From 2de1c247e285cce0b25ab70abdc56ccd41019c27 Mon Sep 17 00:00:00 2001 From: Ryan Harper Date: Wed, 25 Jan 2017 15:45:40 -0600 Subject: Fix eni rendering of multiple IPs per interface The iface:alias syntax for eni rendering is brittle with ipv6. Replace it with using multiple iface stanzas with the same iface name which is supported. Side-effect is that one can no longer do 'ifup $iface:$alias' but requires instead use of ip address {add|delete} instead. LP: #1657940 --- cloudinit/net/eni.py | 33 ++++++++++++++++++-------------- tests/unittests/test_net.py | 46 +++++++++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 26 deletions(-) (limited to 'tests') diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index b06ffac9..5b249f1f 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -90,8 +90,6 @@ def _iface_add_attrs(iface, index): def _iface_start_entry(iface, index, render_hwaddress=False): fullname = iface['name'] - if index != 0: - fullname += ":%s" % index control = iface['control'] if control == "auto": @@ -113,6 +111,16 @@ def _iface_start_entry(iface, index, render_hwaddress=False): return lines +def _subnet_is_ipv6(subnet): + # 'static6' or 'dhcp6' + if subnet['type'].endswith('6'): + # This is a request for DHCPv6. + return True + elif subnet['type'] == 'static' and ":" in subnet['address']: + return True + return False + + def _parse_deb_config_data(ifaces, contents, src_dir, src_path): """Parses the file contents, placing result into ifaces. @@ -354,21 +362,23 @@ class Renderer(renderer.Renderer): sections = [] subnets = iface.get('subnets', {}) if subnets: - for index, subnet in zip(range(0, len(subnets)), subnets): + for index, subnet in enumerate(subnets): iface['index'] = index iface['mode'] = subnet['type'] iface['control'] = subnet.get('control', 'auto') subnet_inet = 'inet' - if iface['mode'].endswith('6'): - # This is a request for DHCPv6. - subnet_inet += '6' - elif iface['mode'] == 'static' and ":" in subnet['address']: - # This is a static IPv6 address. + if _subnet_is_ipv6(subnet): subnet_inet += '6' iface['inet'] = subnet_inet - if iface['mode'].startswith('dhcp'): + if subnet['type'].startswith('dhcp'): iface['mode'] = 'dhcp' + # do not emit multiple 'auto $IFACE' lines as older (precise) + # ifupdown complains + if True in ["auto %s" % (iface['name']) in line + for line in sections]: + iface['control'] = 'alias' + lines = list( _iface_start_entry( iface, index, render_hwaddress=render_hwaddress) + @@ -378,11 +388,6 @@ class Renderer(renderer.Renderer): for route in subnet.get('routes', []): lines.extend(self._render_route(route, indent=" ")) - if len(subnets) > 1 and index == 0: - tmpl = " post-up ifup %s:%s\n" - for i in range(1, len(subnets)): - lines.append(tmpl % (iface['name'], i)) - sections.append(lines) else: # ifenslave docs say to auto the slave devices diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 2c2bde96..b77d277a 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -219,11 +219,9 @@ NETWORK_CONFIGS = { auto eth99 iface eth99 inet dhcp - post-up ifup eth99:1 - - auto eth99:1 - iface eth99:1 inet static + # control-alias eth99 + iface eth99 inet static address 192.168.21.3/24 dns-nameservers 8.8.8.8 8.8.4.4 dns-search barley.maas sach.maas @@ -261,6 +259,27 @@ NETWORK_CONFIGS = { - wark.maas """), }, + 'v4_and_v6': { + 'expected_eni': textwrap.dedent("""\ + auto lo + iface lo inet loopback + + auto iface0 + iface iface0 inet dhcp + + # control-alias iface0 + iface iface0 inet6 dhcp + """).rstrip(' '), + 'yaml': textwrap.dedent("""\ + version: 1 + config: + - type: 'physical' + name: 'iface0' + subnets: + - {'type': 'dhcp4'} + - {'type': 'dhcp6'} + """).rstrip(' '), + }, 'all': { 'expected_eni': ("""\ auto lo @@ -298,11 +317,9 @@ iface br0 inet static address 192.168.14.2/24 bridge_ports eth3 eth4 bridge_stp off - post-up ifup br0:1 - -auto br0:1 -iface br0:1 inet6 static +# control-alias br0 +iface br0 inet6 static address 2001:1::1/64 auto bond0.200 @@ -319,11 +336,9 @@ iface eth0.101 inet static mtu 1500 vlan-raw-device eth0 vlan_id 101 - post-up ifup eth0.101:1 - -auto eth0.101:1 -iface eth0.101:1 inet static +# control-alias eth0.101 +iface eth0.101 inet static address 192.168.2.10/24 post-up route add -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true @@ -758,6 +773,13 @@ class TestEniRoundTrip(CiTestCase): entry['expected_eni'].splitlines(), files['/etc/network/interfaces'].splitlines()) + def testsimple_render_v4_and_v6(self): + entry = NETWORK_CONFIGS['v4_and_v6'] + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) + self.assertEqual( + entry['expected_eni'].splitlines(), + files['/etc/network/interfaces'].splitlines()) + def test_routes_rendered(self): # as reported in bug 1649652 conf = [ -- cgit v1.2.3 From 1cd8cfaf1b4d0e3a97c693469d6d987d55014280 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 14 Feb 2017 11:06:36 -0500 Subject: apply the runtime configuration written by ds-identify. When the ds-identify code landed, it started writing /run/cloud.cfg but at the moment, nothing was reading that. The result is that ds-identify only worked to disable cloud-init entirely. --- cloudinit/stages.py | 9 +++++++- tests/unittests/test_data.py | 53 +++++++++++++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 17 deletions(-) (limited to 'tests') diff --git a/cloudinit/stages.py b/cloudinit/stages.py index 21763810..5bed9032 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py @@ -11,7 +11,8 @@ import sys import six from six.moves import cPickle as pickle -from cloudinit.settings import (PER_INSTANCE, FREQUENCIES, CLOUD_CONFIG) +from cloudinit.settings import ( + FREQUENCIES, CLOUD_CONFIG, PER_INSTANCE, RUN_CLOUD_CONFIG) from cloudinit import handlers @@ -834,6 +835,10 @@ class Modules(object): return self._run_modules(mostly_mods) +def read_runtime_config(): + return util.read_conf(RUN_CLOUD_CONFIG) + + def fetch_base_config(): return util.mergemanydict( [ @@ -841,6 +846,8 @@ def fetch_base_config(): util.get_builtin_cfg(), # Anything in your conf.d or 'default' cloud.cfg location. util.read_conf_with_confd(CLOUD_CONFIG), + # runtime config + read_runtime_config(), # Kernel/cmdline parameters override system config util.read_conf_from_cmdline(), ], reverse=True) diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py index 4092d9ca..4ad86bb6 100644 --- a/tests/unittests/test_data.py +++ b/tests/unittests/test_data.py @@ -564,12 +564,12 @@ class TestConvertString(helpers.TestCase): class TestFetchBaseConfig(helpers.TestCase): - - def test_only_builtin_gets_builtin2(self): + def test_only_builtin_gets_builtin(self): ret = helpers.wrap_and_call( - 'cloudinit.stages.util', - {'read_conf_with_confd': None, - 'read_conf_from_cmdline': None}, + 'cloudinit.stages', + {'util.read_conf_with_confd': None, + 'util.read_conf_from_cmdline': None, + 'read_runtime_config': {'return_value': {}}}, stages.fetch_base_config) self.assertEqual(util.get_builtin_cfg(), ret) @@ -578,9 +578,11 @@ class TestFetchBaseConfig(helpers.TestCase): test_key = sorted(builtin)[0] test_value = 'test' ret = helpers.wrap_and_call( - 'cloudinit.stages.util', - {'read_conf_with_confd': {'return_value': {test_key: test_value}}, - 'read_conf_from_cmdline': None}, + 'cloudinit.stages', + {'util.read_conf_with_confd': + {'return_value': {test_key: test_value}}, + 'util.read_conf_from_cmdline': None, + 'read_runtime_config': {'return_value': {}}}, stages.fetch_base_config) self.assertEqual(ret.get(test_key), test_value) builtin[test_key] = test_value @@ -592,25 +594,44 @@ class TestFetchBaseConfig(helpers.TestCase): test_value = 'test' cmdline = {test_key: test_value} ret = helpers.wrap_and_call( - 'cloudinit.stages.util', - {'read_conf_from_cmdline': {'return_value': cmdline}, - 'read_conf_with_confd': None}, + 'cloudinit.stages', + {'util.read_conf_from_cmdline': {'return_value': cmdline}, + 'util.read_conf_with_confd': None, + 'read_runtime_config': None}, stages.fetch_base_config) self.assertEqual(ret.get(test_key), test_value) builtin[test_key] = test_value self.assertEqual(ret, builtin) - def test_cmdline_overrides_conf_d_and_defaults(self): + def test_cmdline_overrides_confd_runtime_and_defaults(self): builtin = {'key1': 'value0', 'key3': 'other2'} conf_d = {'key1': 'value1', 'key2': 'other1'} cmdline = {'key3': 'other3', 'key2': 'other2'} + runtime = {'key3': 'runtime3'} ret = helpers.wrap_and_call( - 'cloudinit.stages.util', - {'read_conf_with_confd': {'return_value': conf_d}, - 'get_builtin_cfg': {'return_value': builtin}, - 'read_conf_from_cmdline': {'return_value': cmdline}}, + 'cloudinit.stages', + {'util.read_conf_with_confd': {'return_value': conf_d}, + 'util.get_builtin_cfg': {'return_value': builtin}, + 'read_runtime_config': {'return_value': runtime}, + 'util.read_conf_from_cmdline': {'return_value': cmdline}}, stages.fetch_base_config) self.assertEqual(ret, {'key1': 'value1', 'key2': 'other2', 'key3': 'other3'}) + def test_order_precedence_is_builtin_system_runtime_cmdline(self): + builtin = {'key1': 'builtin0', 'key3': 'builtin3'} + conf_d = {'key1': 'confd1', 'key2': 'confd2', 'keyconfd1': 'kconfd1'} + runtime = {'key1': 'runtime1', 'key2': 'runtime2'} + cmdline = {'key1': 'cmdline1'} + ret = helpers.wrap_and_call( + 'cloudinit.stages', + {'util.read_conf_with_confd': {'return_value': conf_d}, + 'util.get_builtin_cfg': {'return_value': builtin}, + 'util.read_conf_from_cmdline': {'return_value': cmdline}, + 'read_runtime_config': {'return_value': runtime}, + }, + stages.fetch_base_config) + self.assertEqual(ret, {'key1': 'cmdline1', 'key2': 'runtime2', + 'key3': 'builtin3', 'keyconfd1': 'kconfd1'}) + # vi: ts=4 expandtab -- cgit v1.2.3 From f81d6c7bde2af206d449de593b35773068270c84 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Fri, 17 Feb 2017 08:55:05 -0500 Subject: net: correct errors in cloudinit/net/sysconfig.py There were some logic errors in sysconfig.py that appear to be the result of accidentally typing "iface" where it should have been "iface_cfg". This patch corrects those problems so that the module can run successfully. LP: #1665441 Resolves: rhbz#1389530 --- cloudinit/net/sysconfig.py | 4 +-- tests/unittests/test_net.py | 87 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 9be74070..19e220ae 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -283,10 +283,10 @@ class Renderer(renderer.Renderer): cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0]) elif len(iface_subnets) > 1: for i, iface_subnet in enumerate(iface_subnets, - start=len(iface.children)): + start=len(iface_cfg.children)): iface_sub_cfg = iface_cfg.copy() iface_sub_cfg.name = "%s:%s" % (iface_name, i) - iface.children.append(iface_sub_cfg) + iface_cfg.children.append(iface_sub_cfg) cls._render_subnet(iface_sub_cfg, route_cfg, iface_subnet) @classmethod diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index b77d277a..1b6288d4 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -159,6 +159,91 @@ NETMASK0=0.0.0.0 ; Created by cloud-init on instance boot automatically, do not edit. ; nameserver 172.19.0.12 +""".lstrip()), + ('etc/udev/rules.d/70-persistent-net.rules', + "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ', + 'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))] + }, + { + 'in_data': { + "services": [{"type": "dns", "address": "172.19.0.12"}], + "networks": [{ + "network_id": "public-ipv4", + "type": "ipv4", "netmask": "255.255.252.0", + "link": "tap1a81968a-79", + "routes": [{ + "netmask": "0.0.0.0", + "network": "0.0.0.0", + "gateway": "172.19.3.254", + }], + "ip_address": "172.19.1.34", "id": "network0" + },{ + "network_id": "private-ipv4", + "type": "ipv4", "netmask": "255.255.255.0", + "link": "tap1a81968a-79", + "routes": [], + "ip_address": "10.0.0.10", "id": "network1" + }], + "links": [ + { + "ethernet_mac_address": "fa:16:3e:ed:9a:59", + "mtu": None, "type": "bridge", "id": + "tap1a81968a-79", + "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f" + }, + ], + }, + 'in_macs': { + 'fa:16:3e:ed:9a:59': 'eth0', + }, + 'out_sysconfig': [ + ('etc/sysconfig/network-scripts/ifcfg-eth0', + """ +# Created by cloud-init on instance boot automatically, do not edit. +# +BOOTPROTO=none +DEVICE=eth0 +HWADDR=fa:16:3e:ed:9a:59 +NM_CONTROLLED=no +ONBOOT=yes +TYPE=Ethernet +USERCTL=no +""".lstrip()), + ('etc/sysconfig/network-scripts/ifcfg-eth0:0', + """ +# Created by cloud-init on instance boot automatically, do not edit. +# +BOOTPROTO=static +DEFROUTE=yes +DEVICE=eth0:0 +GATEWAY=172.19.3.254 +HWADDR=fa:16:3e:ed:9a:59 +IPADDR=172.19.1.34 +NETMASK=255.255.252.0 +NM_CONTROLLED=no +ONBOOT=yes +TYPE=Ethernet +USERCTL=no +""".lstrip()), + ('etc/sysconfig/network-scripts/ifcfg-eth0:1', + """ +# Created by cloud-init on instance boot automatically, do not edit. +# +BOOTPROTO=static +DEVICE=eth0:1 +HWADDR=fa:16:3e:ed:9a:59 +IPADDR=10.0.0.10 +NETMASK=255.255.255.0 +NM_CONTROLLED=no +ONBOOT=yes +TYPE=Ethernet +USERCTL=no +""".lstrip()), + ('etc/resolv.conf', + """ +; Created by cloud-init on instance boot automatically, do not edit. +; +nameserver 172.19.0.12 """.lstrip()), ('etc/udev/rules.d/70-persistent-net.rules', "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ', @@ -529,8 +614,8 @@ USERCTL=no self.assertEqual(expected_content, content) def test_openstack_rendering_samples(self): - render_dir = self.tmp_dir() for os_sample in OS_SAMPLES: + render_dir = self.tmp_dir() ex_input = os_sample['in_data'] ex_mac_addrs = os_sample['in_macs'] network_cfg = openstack.convert_net_json( -- cgit v1.2.3 From da25385d0613b373c5746761748782ca1e157d10 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Fri, 17 Feb 2017 12:05:38 -0500 Subject: flake8: fix flake8 complaints in previous commit. --- cloudinit/net/sysconfig.py | 6 +++--- tests/unittests/test_net.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 19e220ae..6e7739fb 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -282,12 +282,12 @@ class Renderer(renderer.Renderer): if len(iface_subnets) == 1: cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0]) elif len(iface_subnets) > 1: - for i, iface_subnet in enumerate(iface_subnets, - start=len(iface_cfg.children)): + for i, isubnet in enumerate(iface_subnets, + start=len(iface_cfg.children)): iface_sub_cfg = iface_cfg.copy() iface_sub_cfg.name = "%s:%s" % (iface_name, i) iface_cfg.children.append(iface_sub_cfg) - cls._render_subnet(iface_sub_cfg, route_cfg, iface_subnet) + cls._render_subnet(iface_sub_cfg, route_cfg, isubnet) @classmethod def _render_bond_interfaces(cls, network_state, iface_contents): diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 1b6288d4..4b03ff72 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -177,7 +177,7 @@ nameserver 172.19.0.12 "gateway": "172.19.3.254", }], "ip_address": "172.19.1.34", "id": "network0" - },{ + }, { "network_id": "private-ipv4", "type": "ipv4", "netmask": "255.255.255.0", "link": "tap1a81968a-79", -- cgit v1.2.3