From 2fd22530737468a00bfb58eed9b6e231aa6d6652 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 30 Jul 2015 15:18:53 -0700 Subject: Ensure that when a resolve conf object is written we pass the str() version of it Instead of passing the raw object and expecting the write_file to work automatically make sure we explicitly pass the string version of it so that the write_file routine can correctly encode/decode it as needed. LP: #1479988 --- cloudinit/distros/rhel_util.py | 2 +- tests/unittests/test_distros/test_resolv.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cloudinit/distros/rhel_util.py b/cloudinit/distros/rhel_util.py index 84aad623..903d7793 100644 --- a/cloudinit/distros/rhel_util.py +++ b/cloudinit/distros/rhel_util.py @@ -86,4 +86,4 @@ def update_resolve_conf_file(fn, dns_servers, search_servers): r_conf.add_search_domain(s) except ValueError: util.logexc(LOG, "Failed at adding search domain %s", s) - util.write_file(fn, r_conf, 0o644) + util.write_file(fn, str(r_conf), 0o644) diff --git a/tests/unittests/test_distros/test_resolv.py b/tests/unittests/test_distros/test_resolv.py index faaf5b7f..9edeb6e7 100644 --- a/tests/unittests/test_distros/test_resolv.py +++ b/tests/unittests/test_distros/test_resolv.py @@ -1,6 +1,8 @@ from cloudinit.distros.parsers import resolv_conf +from cloudinit.distros import rhel_util import re +import tempfile from ..helpers import TestCase @@ -19,6 +21,10 @@ class TestResolvHelper(TestCase): rp_r = str(rp).strip() self.assertEquals(BASE_RESOLVE, rp_r) + def test_write_works(self): + with tempfile.NamedTemporaryFile() as fh: + rhel_util.update_resolve_conf_file(fh.name, [], []) + def test_local_domain(self): rp = resolv_conf.ResolvConf(BASE_RESOLVE) self.assertEquals(None, rp.local_domain) -- cgit v1.2.3 From eb5a4dda1fc221bf29c45eef47f0bfadec250943 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Wed, 6 Apr 2016 13:27:23 -0400 Subject: rh_subscription: only check subscription if configured The rh_subscription config module would attempt to connect to the RHN servers even when no config is provided. Now, instead check to make sure that valid config is provided first. That consists of username and password or a activation key. LP: #1536706 --- cloudinit/config/cc_rh_subscription.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cloudinit/config/cc_rh_subscription.py b/cloudinit/config/cc_rh_subscription.py index 6087c45c..2871984a 100644 --- a/cloudinit/config/cc_rh_subscription.py +++ b/cloudinit/config/cc_rh_subscription.py @@ -19,9 +19,14 @@ from cloudinit import util -def handle(_name, cfg, _cloud, log, _args): +def handle(name, cfg, _cloud, log, _args): sm = SubscriptionManager(cfg) sm.log = log + if not sm.is_configured(): + log.debug("Activation key not provided, config module %s disabled.", + _name) + return None + if not sm.is_registered: try: verify, verify_msg = sm._verify_keys() @@ -95,7 +100,6 @@ class SubscriptionManager(object): self.disable_repo = self.rhel_cfg.get('disable-repo') self.servicelevel = self.rhel_cfg.get('service-level') self.subman = ['subscription-manager'] - self.is_registered = self._is_registered() def log_success(self, msg): '''Simple wrapper for logging info messages. Useful for unittests''' @@ -134,7 +138,7 @@ class SubscriptionManager(object): return False, no_auto return True, None - def _is_registered(self): + def is_registered(self): ''' Checks if the system is already registered and returns True if so, else False @@ -400,3 +404,6 @@ class SubscriptionManager(object): self.log.debug("Disabled the following repos: %s" % (", ".join(disable_list)).replace('--disable=', '')) return True + + def is_configured(self): + return (self.userid and self.password) or self.activation_key -- cgit v1.2.3 From 578fed15061293ce421eec1c9c1e2e056631a734 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Wed, 6 Apr 2016 13:57:00 -0400 Subject: fix tests and hopefully actually work --- cloudinit/config/cc_rh_subscription.py | 7 +++---- tests/unittests/test_rh_subscription.py | 9 +++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cloudinit/config/cc_rh_subscription.py b/cloudinit/config/cc_rh_subscription.py index 2871984a..3a113aea 100644 --- a/cloudinit/config/cc_rh_subscription.py +++ b/cloudinit/config/cc_rh_subscription.py @@ -23,11 +23,10 @@ def handle(name, cfg, _cloud, log, _args): sm = SubscriptionManager(cfg) sm.log = log if not sm.is_configured(): - log.debug("Activation key not provided, config module %s disabled.", - _name) + log.debug("%s: module not configured.", name) return None - if not sm.is_registered: + if not sm.is_registered(): try: verify, verify_msg = sm._verify_keys() if verify is not True: @@ -406,4 +405,4 @@ class SubscriptionManager(object): return True def is_configured(self): - return (self.userid and self.password) or self.activation_key + return bool((self.userid and self.password) or self.activation_key) diff --git a/tests/unittests/test_rh_subscription.py b/tests/unittests/test_rh_subscription.py index 38d5763a..8c586ad7 100644 --- a/tests/unittests/test_rh_subscription.py +++ b/tests/unittests/test_rh_subscription.py @@ -126,7 +126,8 @@ class TestBadInput(unittest.TestCase): 'enable-repo': 'not_a_list' }} config_badkey = {'rh_subscription': - {'activation_key': 'abcdef1234', + {'activation-key': 'abcdef1234', + 'fookey': 'bar', 'org': '123', }} @@ -138,7 +139,11 @@ class TestBadInput(unittest.TestCase): ''' Attempt to register without the password key/value ''' - self.input_is_missing_data(self.config_no_password) + self.SM._sub_man_cli = mock.MagicMock( + side_effect=[util.ProcessExecutionError, (self.reg, 'bar')]) + self.handle(self.name, self.config_no_password, self.cloud_init, + self.log, self.args) + self.assertEqual(self.SM._sub_man_cli.call_count, 0) def test_no_org(self): ''' -- cgit v1.2.3 From a7de90b2ebdd097bb98fef72f970354973982e81 Mon Sep 17 00:00:00 2001 From: Stéphane Graber Date: Mon, 11 Apr 2016 00:09:44 -0400 Subject: Add support for lxd-bridge configuration --- cloudinit/config/cc_lxd.py | 111 ++++++++++++++++++++++++++++++++------ doc/examples/cloud-config-lxd.txt | 27 ++++++++++ 2 files changed, 123 insertions(+), 15 deletions(-) diff --git a/cloudinit/config/cc_lxd.py b/cloudinit/config/cc_lxd.py index 63b8fb63..7d90b606 100644 --- a/cloudinit/config/cc_lxd.py +++ b/cloudinit/config/cc_lxd.py @@ -30,9 +30,23 @@ Example config: storage_create_loop: storage_pool: trust_password: + bridge: + mode: + name: + ipv4_address: + ipv4_netmask: + ipv4_dhcp_first: + ipv4_dhcp_last: + ipv4_dhcp_leases: + ipv4_nat: + ipv6_address: + ipv6_netmask: + ipv6_nat: + domain: """ from cloudinit import util +import os def handle(name, cfg, cloud, log, args): @@ -46,22 +60,24 @@ def handle(name, cfg, cloud, log, args): type(lxd_cfg)) return + # Grab the configuration init_cfg = lxd_cfg.get('init') if not isinstance(init_cfg, dict): log.warn("lxd/init config must be a dictionary. found a '%s'", type(init_cfg)) init_cfg = {} - if not init_cfg: - log.debug("no lxd/init config. disabled.") - return + bridge_cfg = lxd_cfg.get('bridge') + if not isinstance(bridge_cfg, dict): + log.warn("lxd/bridge config must be a dictionary. found a '%s'", + type(bridge_cfg)) + bridge_cfg = {} + # Install the needed packages packages = [] - # Ensure lxd is installed if not util.which("lxd"): packages.append('lxd') - # if using zfs, get the utils if init_cfg.get("storage_backend") == "zfs" and not util.which('zfs'): packages.append('zfs') @@ -73,13 +89,78 @@ def handle(name, cfg, cloud, log, args): return # Set up lxd if init config is given - init_keys = ( - 'network_address', 'network_port', 'storage_backend', - 'storage_create_device', 'storage_create_loop', - 'storage_pool', 'trust_password') - cmd = ['lxd', 'init', '--auto'] - for k in init_keys: - if init_cfg.get(k): - cmd.extend(["--%s=%s" % - (k.replace('_', '-'), str(init_cfg[k]))]) - util.subp(cmd) + if init_cfg: + init_keys = ( + 'network_address', 'network_port', 'storage_backend', + 'storage_create_device', 'storage_create_loop', + 'storage_pool', 'trust_password') + cmd = ['lxd', 'init', '--auto'] + for k in init_keys: + if init_cfg.get(k): + cmd.extend(["--%s=%s" % + (k.replace('_', '-'), str(init_cfg[k]))]) + util.subp(cmd) + + # Set up lxd-bridge if bridge config is given + if bridge_cfg: + debconf = {} + + if bridge_cfg.get("mode") == "none": + debconf["lxd/setup-bridge"] = "false" + debconf["lxd/bridge-name"] = "" + + elif bridge_cfg.get("mode") == "existing": + debconf["lxd/setup-bridge"] = "false" + debconf["lxd/use-existing-bridge"] = "true" + debconf["lxd/bridge-name"] = bridge_cfg.get("name") + + elif bridge_cfg.get("mode") == "new": + debconf["lxd/setup-bridge"] = "true" + debconf["lxd/bridge-name"] = bridge_cfg.get("name", "lxdbr0") + if bridge_cfg.get("ipv4_address"): + debconf["lxd/bridge-ipv4"] = "true" + debconf["lxd/bridge-ipv4-address"] = \ + bridge_cfg.get("ipv4_address") + debconf["lxd/bridge-ipv4-netmask"] = \ + bridge_cfg.get("ipv4_netmask") + debconf["lxd/bridge-ipv4-dhcp-first"] = \ + bridge_cfg.get("ipv4_dhcp_first") + debconf["lxd/bridge-ipv4-dhcp-last"] = \ + bridge_cfg.get("ipv4_dhcp_last") + debconf["lxd/bridge-ipv4-dhcp-leases"] = \ + bridge_cfg.get("ipv4_dhcp_leases") + debconf["lxd/bridge-ipv4-nat"] = \ + bridge_cfg.get("ipv4_nat", "true") + + if bridge_cfg.get("ipv6_address"): + debconf["lxd/bridge-ipv6"] = "true" + debconf["lxd/bridge-ipv6-address"] = \ + bridge_cfg.get("ipv6_address") + debconf["lxd/bridge-ipv6-netmask"] = \ + bridge_cfg.get("ipv6_netmask") + debconf["lxd/bridge-ipv6-nat"] = \ + bridge_cfg.get("ipv6_nat", "false") + + else: + log.warn("invalid bridge mode \"%s\"" % bridge_cfg.get("mode")) + return + + # Update debconf database + try: + log.debug("Setting lxd debconf-set-selections") + for k, v in debconf.items(): + util.subp(['debconf-communicate'], "set %s %s\n" % (k, v)) + except: + util.logexc(log, "Failed to run debconf-communicate for lxd") + + # Remove the existing configuration file (forces re-generation) + if os.path.exists("/etc/default/lxd-bridge"): + os.remove("/etc/default/lxd-bridge") + + # Run reconfigure + try: + log.debug("Running dpkg-reconfigure for lxd") + util.subp(['dpkg-reconfigure', 'lxd', + '--frontend=noninteractive']) + except: + util.logexc(log, "Failed to run dpkg-reconfigure for lxd") diff --git a/doc/examples/cloud-config-lxd.txt b/doc/examples/cloud-config-lxd.txt index b9bb4aa5..e96f314b 100644 --- a/doc/examples/cloud-config-lxd.txt +++ b/doc/examples/cloud-config-lxd.txt @@ -12,6 +12,20 @@ # storage_create_loop: set up loop based storage with size in GB # storage_pool: name of storage pool to use or create # trust_password: password required to add new clients +# bridge: dict of options for the lxd bridge +# mode: one of "new", "existing" or "none". Defaults to "new" +# name: the name of the bridge. Defaults to "lxdbr0" +# ipv4_address: an IPv4 address (e.g. 10.0.8.1) +# ipv4_netmask: a CIDR mask value (e.g. 24) +# ipv4_dhcp_first: the first IP of the DHCP range (e.g. 10.0.8.2) +# ipv4_dhcp_last: the last IP of the DHCP range (e.g. 10.0.8.254) +# ipv4_dhcp_leases: the size of the DHCP pool (e.g. 250) +# ipv4_nat: either "true" or "false" +# ipv6_address: an IPv6 address (e.g. fd98:9e0:3744::1) +# ipv6_netmask: a CIDR mask value (e.g. 64) +# ipv6_nat: either "true" or "false" +# domain: domain name to use for the bridge + lxd: init: @@ -20,6 +34,19 @@ lxd: storage_backend: zfs storage_pool: datapool storage_create_loop: 10 + bridge: + mode: new + name: lxdbr0 + ipv4_address: 10.0.8.1 + ipv4_netmask: 24 + ipv4_dhcp_first: 10.0.8.2 + ipv4_dhcp_last: 10.0.8.3 + ipv4_dhcp_leases: 250 + ipv4_nat: true + ipv6_address: fd98:9e0:3744::1 + ipv6_netmask: 64 + ipv6_nat: true + domain: lxd # The simplist working configuration is -- cgit v1.2.3 From 072510d7251653701c23ded9be38832044fb42b8 Mon Sep 17 00:00:00 2001 From: Stéphane Graber Date: Mon, 11 Apr 2016 11:58:28 -0400 Subject: Update lxd-bridge code to do a single debconf-communicate run and split debconf logic to a function --- cloudinit/config/cc_lxd.py | 101 +++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/cloudinit/config/cc_lxd.py b/cloudinit/config/cc_lxd.py index 7d90b606..b467df20 100644 --- a/cloudinit/config/cc_lxd.py +++ b/cloudinit/config/cc_lxd.py @@ -103,53 +103,14 @@ def handle(name, cfg, cloud, log, args): # Set up lxd-bridge if bridge config is given if bridge_cfg: - debconf = {} - - if bridge_cfg.get("mode") == "none": - debconf["lxd/setup-bridge"] = "false" - debconf["lxd/bridge-name"] = "" - - elif bridge_cfg.get("mode") == "existing": - debconf["lxd/setup-bridge"] = "false" - debconf["lxd/use-existing-bridge"] = "true" - debconf["lxd/bridge-name"] = bridge_cfg.get("name") - - elif bridge_cfg.get("mode") == "new": - debconf["lxd/setup-bridge"] = "true" - debconf["lxd/bridge-name"] = bridge_cfg.get("name", "lxdbr0") - if bridge_cfg.get("ipv4_address"): - debconf["lxd/bridge-ipv4"] = "true" - debconf["lxd/bridge-ipv4-address"] = \ - bridge_cfg.get("ipv4_address") - debconf["lxd/bridge-ipv4-netmask"] = \ - bridge_cfg.get("ipv4_netmask") - debconf["lxd/bridge-ipv4-dhcp-first"] = \ - bridge_cfg.get("ipv4_dhcp_first") - debconf["lxd/bridge-ipv4-dhcp-last"] = \ - bridge_cfg.get("ipv4_dhcp_last") - debconf["lxd/bridge-ipv4-dhcp-leases"] = \ - bridge_cfg.get("ipv4_dhcp_leases") - debconf["lxd/bridge-ipv4-nat"] = \ - bridge_cfg.get("ipv4_nat", "true") - - if bridge_cfg.get("ipv6_address"): - debconf["lxd/bridge-ipv6"] = "true" - debconf["lxd/bridge-ipv6-address"] = \ - bridge_cfg.get("ipv6_address") - debconf["lxd/bridge-ipv6-netmask"] = \ - bridge_cfg.get("ipv6_netmask") - debconf["lxd/bridge-ipv6-nat"] = \ - bridge_cfg.get("ipv6_nat", "false") - - else: - log.warn("invalid bridge mode \"%s\"" % bridge_cfg.get("mode")) - return + debconf = bridge_to_debconf(bridge_cfg) # Update debconf database try: log.debug("Setting lxd debconf-set-selections") - for k, v in debconf.items(): - util.subp(['debconf-communicate'], "set %s %s\n" % (k, v)) + data = "\n".join(["set %s %s" % (k, v) + for k, v in debconf.items()]) + util.subp(['debconf-communicate'], data) except: util.logexc(log, "Failed to run debconf-communicate for lxd") @@ -158,9 +119,51 @@ def handle(name, cfg, cloud, log, args): os.remove("/etc/default/lxd-bridge") # Run reconfigure - try: - log.debug("Running dpkg-reconfigure for lxd") - util.subp(['dpkg-reconfigure', 'lxd', - '--frontend=noninteractive']) - except: - util.logexc(log, "Failed to run dpkg-reconfigure for lxd") + log.debug("Running dpkg-reconfigure for lxd") + util.subp(['dpkg-reconfigure', 'lxd', + '--frontend=noninteractive']) + + +def bridge_to_debconf(bridge_cfg): + debconf = {} + + if bridge_cfg.get("mode") == "none": + debconf["lxd/setup-bridge"] = "false" + debconf["lxd/bridge-name"] = "" + + elif bridge_cfg.get("mode") == "existing": + debconf["lxd/setup-bridge"] = "false" + debconf["lxd/use-existing-bridge"] = "true" + debconf["lxd/bridge-name"] = bridge_cfg.get("name") + + elif bridge_cfg.get("mode") == "new": + debconf["lxd/setup-bridge"] = "true" + debconf["lxd/bridge-name"] = bridge_cfg.get("name", "lxdbr0") + if bridge_cfg.get("ipv4_address"): + debconf["lxd/bridge-ipv4"] = "true" + debconf["lxd/bridge-ipv4-address"] = \ + bridge_cfg.get("ipv4_address") + debconf["lxd/bridge-ipv4-netmask"] = \ + bridge_cfg.get("ipv4_netmask") + debconf["lxd/bridge-ipv4-dhcp-first"] = \ + bridge_cfg.get("ipv4_dhcp_first") + debconf["lxd/bridge-ipv4-dhcp-last"] = \ + bridge_cfg.get("ipv4_dhcp_last") + debconf["lxd/bridge-ipv4-dhcp-leases"] = \ + bridge_cfg.get("ipv4_dhcp_leases") + debconf["lxd/bridge-ipv4-nat"] = \ + bridge_cfg.get("ipv4_nat", "true") + + if bridge_cfg.get("ipv6_address"): + debconf["lxd/bridge-ipv6"] = "true" + debconf["lxd/bridge-ipv6-address"] = \ + bridge_cfg.get("ipv6_address") + debconf["lxd/bridge-ipv6-netmask"] = \ + bridge_cfg.get("ipv6_netmask") + debconf["lxd/bridge-ipv6-nat"] = \ + bridge_cfg.get("ipv6_nat", "false") + + else: + raise Exception("invalid bridge mode \"%s\"" % bridge_cfg.get("mode")) + + return debconf -- cgit v1.2.3 From dcc2c894b9f12527ce0254fd7a0306e0d41af5bf Mon Sep 17 00:00:00 2001 From: Stéphane Graber Date: Mon, 11 Apr 2016 12:57:26 -0400 Subject: lxd-bridge: Don't require a bridge name and implement support for domain name --- cloudinit/config/cc_lxd.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cloudinit/config/cc_lxd.py b/cloudinit/config/cc_lxd.py index b467df20..0b10077a 100644 --- a/cloudinit/config/cc_lxd.py +++ b/cloudinit/config/cc_lxd.py @@ -138,7 +138,9 @@ def bridge_to_debconf(bridge_cfg): elif bridge_cfg.get("mode") == "new": debconf["lxd/setup-bridge"] = "true" - debconf["lxd/bridge-name"] = bridge_cfg.get("name", "lxdbr0") + if bridge_cfg.get("name"): + debconf["lxd/bridge-name"] = bridge_cfg.get("name") + if bridge_cfg.get("ipv4_address"): debconf["lxd/bridge-ipv4"] = "true" debconf["lxd/bridge-ipv4-address"] = \ @@ -163,6 +165,9 @@ def bridge_to_debconf(bridge_cfg): debconf["lxd/bridge-ipv6-nat"] = \ bridge_cfg.get("ipv6_nat", "false") + if bridge_cfg.get("domain"): + debconf["lxd/bridge-domain"] = bridge_cfg.get("domain") + else: raise Exception("invalid bridge mode \"%s\"" % bridge_cfg.get("mode")) -- cgit v1.2.3 From 32340db1f8b232855b635686e89608e23f2530cc Mon Sep 17 00:00:00 2001 From: Stéphane Graber Date: Mon, 11 Apr 2016 12:58:01 -0400 Subject: Add tests for lxd-bridge --- tests/unittests/test_handler/test_handler_lxd.py | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/unittests/test_handler/test_handler_lxd.py b/tests/unittests/test_handler/test_handler_lxd.py index 7ffa2a53..5f61ba6a 100644 --- a/tests/unittests/test_handler/test_handler_lxd.py +++ b/tests/unittests/test_handler/test_handler_lxd.py @@ -73,3 +73,62 @@ class TestLxd(t_help.TestCase): cc_lxd.handle('cc_lxd', {'package_update': True}, cc, LOG, []) self.assertFalse(cc.distro.install_packages.called) self.assertFalse(mock_util.subp.called) + + def test_lxd_debconf_new_full(self): + data = {"mode": "new", + "name": "testbr0", + "ipv4_address": "10.0.8.1", + "ipv4_netmask": "24", + "ipv4_dhcp_first": "10.0.8.2", + "ipv4_dhcp_last": "10.0.8.254", + "ipv4_dhcp_leases": "250", + "ipv4_nat": "true", + "ipv6_address": "fd98:9e0:3744::1", + "ipv6_netmask": "64", + "ipv6_nat": "true", + "domain": "lxd"} + self.assertEquals( + cc_lxd.bridge_to_debconf(data), + {"lxd/setup-bridge": "true", + "lxd/bridge-name": "testbr0", + "lxd/bridge-ipv4": "true", + "lxd/bridge-ipv4-address": "10.0.8.1", + "lxd/bridge-ipv4-netmask": "24", + "lxd/bridge-ipv4-dhcp-first": "10.0.8.2", + "lxd/bridge-ipv4-dhcp-last": "10.0.8.254", + "lxd/bridge-ipv4-dhcp-leases": "250", + "lxd/bridge-ipv4-nat": "true", + "lxd/bridge-ipv6": "true", + "lxd/bridge-ipv6-address": "fd98:9e0:3744::1", + "lxd/bridge-ipv6-netmask": "64", + "lxd/bridge-ipv6-nat": "true", + "lxd/bridge-domain": "lxd"}) + + def test_lxd_debconf_new_partial(self): + data = {"mode": "new", + "ipv6_address": "fd98:9e0:3744::1", + "ipv6_netmask": "64", + "ipv6_nat": "true"} + self.assertEquals( + cc_lxd.bridge_to_debconf(data), + {"lxd/setup-bridge": "true", + "lxd/bridge-ipv6": "true", + "lxd/bridge-ipv6-address": "fd98:9e0:3744::1", + "lxd/bridge-ipv6-netmask": "64", + "lxd/bridge-ipv6-nat": "true"}) + + def test_lxd_debconf_existing(self): + data = {"mode": "existing", + "name": "testbr0"} + self.assertEquals( + cc_lxd.bridge_to_debconf(data), + {"lxd/setup-bridge": "false", + "lxd/use-existing-bridge": "true", + "lxd/bridge-name": "testbr0"}) + + def test_lxd_debconf_none(self): + data = {"mode": "none"} + self.assertEquals( + cc_lxd.bridge_to_debconf(data), + {"lxd/setup-bridge": "false", + "lxd/bridge-name": ""}) -- cgit v1.2.3 From 860d98bce4b607a513dc94d96335e8f105a36294 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Mon, 11 Apr 2016 15:08:08 -0400 Subject: minor cleanups - use util.del_file rather than os.remove - raise exception if debconf-communicate is not present - add a trailing newline into debconf-communicate input --- cloudinit/config/cc_lxd.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/cloudinit/config/cc_lxd.py b/cloudinit/config/cc_lxd.py index 0b10077a..bf735648 100644 --- a/cloudinit/config/cc_lxd.py +++ b/cloudinit/config/cc_lxd.py @@ -46,7 +46,6 @@ Example config: """ from cloudinit import util -import os def handle(name, cfg, cloud, log, args): @@ -102,26 +101,29 @@ def handle(name, cfg, cloud, log, args): util.subp(cmd) # Set up lxd-bridge if bridge config is given - if bridge_cfg: + dconf_comm = "debconf-communicate" + if bridge_cfg and util.which(dconf_comm): debconf = bridge_to_debconf(bridge_cfg) # Update debconf database try: - log.debug("Setting lxd debconf-set-selections") + log.debug("Setting lxd debconf via " + dconf_comm) data = "\n".join(["set %s %s" % (k, v) - for k, v in debconf.items()]) + for k, v in debconf.items()]) + "\n" util.subp(['debconf-communicate'], data) except: - util.logexc(log, "Failed to run debconf-communicate for lxd") + util.logexc(log, "Failed to run '%s' for lxd with" % dconf_comm) # Remove the existing configuration file (forces re-generation) - if os.path.exists("/etc/default/lxd-bridge"): - os.remove("/etc/default/lxd-bridge") + util.del_file("/etc/default/lxd-bridge") # Run reconfigure log.debug("Running dpkg-reconfigure for lxd") util.subp(['dpkg-reconfigure', 'lxd', '--frontend=noninteractive']) + elif bridge_cfg: + raise RuntimeError( + "Unable to configure lxd bridge without %s." + dconf_comm) def bridge_to_debconf(bridge_cfg): -- cgit v1.2.3 From 7122f7d6fac6eb78922a474facfd9d439d1bf5b6 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 12 Apr 2016 10:38:09 -0400 Subject: chef: straighten out validation_cert and validation_key Now, validation_key is always a path to a file, as it is in chef's client.rb syntax. validation_cert is always the *content* of that file that should be written. However, if validation_cert is the string "system", then we do not write that value, but rather assume the file exists. LP: #1568940 --- cloudinit/config/cc_chef.py | 21 ++++---- doc/examples/cloud-config-chef.txt | 4 +- templates/chef_client.rb.tmpl | 2 +- tests/unittests/test_handler/test_handler_chef.py | 65 ++++++++++++++++++++++- 4 files changed, 79 insertions(+), 13 deletions(-) diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py index 28711a59..07dacb0c 100644 --- a/cloudinit/config/cc_chef.py +++ b/cloudinit/config/cc_chef.py @@ -38,8 +38,10 @@ It can be configured with the following option structure:: chef: directories: (defaulting to /etc/chef, /var/log/chef, /var/lib/chef, /var/cache/chef, /var/backups/chef, /var/run/chef) - validation_key or validation_cert: (optional string to be written to - /etc/chef/validation.pem) + validation_cert: (optional string to be written to file validation_key) + special value 'system' means set use existing file + validation_key: (optional the path for validation_cert. default + /etc/chef/validation.pem) firstboot_path: (path to write run_list and initial_attributes keys that should also be present in this configuration, defaults to /etc/chef/firstboot.json) @@ -64,6 +66,7 @@ It can be configured with the following option structure:: server_url: show_time: ssl_verify_mode: + validation_cert: validation_key: validation_name: """ @@ -105,6 +108,7 @@ CHEF_RB_TPL_DEFAULTS = { # These are not symbols... 'log_location': '/var/log/chef/client.log', 'validation_key': CHEF_VALIDATION_PEM_PATH, + 'validation_cert': None, 'client_key': "/etc/chef/client.pem", 'json_attribs': CHEF_FB_PATH, 'file_cache_path': "/var/cache/chef", @@ -201,13 +205,12 @@ def handle(name, cfg, cloud, log, _args): for d in itertools.chain(chef_dirs, REQUIRED_CHEF_DIRS): util.ensure_dir(d) - # Set the validation key based on the presence of either 'validation_key' - # or 'validation_cert'. In the case where both exist, 'validation_key' - # takes precedence - for key in ('validation_key', 'validation_cert'): - if key in chef_cfg and chef_cfg[key]: - util.write_file(CHEF_VALIDATION_PEM_PATH, chef_cfg[key]) - break + vkey_path = chef_cfg.get('validation_key', CHEF_VALIDATION_PEM_PATH) + vcert = chef_cfg.get('validation_cert') + # special value 'system' means do not overwrite the file + # but still render the template to contain 'validation_key' + if vcert and vcert != "system": + util.write_file(vkey_path, vcert) # Create the chef config from template template_fn = cloud.get_template_filename('chef_client.rb') diff --git a/doc/examples/cloud-config-chef.txt b/doc/examples/cloud-config-chef.txt index 4edad653..b886cba2 100644 --- a/doc/examples/cloud-config-chef.txt +++ b/doc/examples/cloud-config-chef.txt @@ -67,7 +67,9 @@ chef: # Default validation name is chef-validator validation_name: "yourorg-validator" - validation_key: | + # if validation_cert's value is "system" then it is expected + # that the file already exists on the system. + validation_cert: | -----BEGIN RSA PRIVATE KEY----- YOUR-ORGS-VALIDATION-KEY-HERE -----END RSA PRIVATE KEY----- diff --git a/templates/chef_client.rb.tmpl b/templates/chef_client.rb.tmpl index c4069d22..cbb6b15f 100644 --- a/templates/chef_client.rb.tmpl +++ b/templates/chef_client.rb.tmpl @@ -26,7 +26,7 @@ log_location "{{log_location}}" {% if validation_name %} validation_client_name "{{validation_name}}" {% endif %} -{% if validation_key %} +{% if validation_cert %} validation_key "{{validation_key}}" {% endif %} {% if client_key %} diff --git a/tests/unittests/test_handler/test_handler_chef.py b/tests/unittests/test_handler/test_handler_chef.py index edad88cb..7763f23b 100644 --- a/tests/unittests/test_handler/test_handler_chef.py +++ b/tests/unittests/test_handler/test_handler_chef.py @@ -75,17 +75,28 @@ class TestChef(t_help.FilesystemMockingTestCase): 'chef': { 'server_url': 'localhost', 'validation_name': 'bob', + 'validation_key': "/etc/chef/vkey.pem", + 'validation_cert': "this is my cert", }, } cc_chef.handle('chef', cfg, self.fetch_cloud('ubuntu'), LOG, []) for d in cc_chef.CHEF_DIRS: self.assertTrue(os.path.isdir(d)) c = util.load_file(cc_chef.CHEF_RB_PATH) + + # the content of these keys is not expected to be rendered to tmpl + unrendered_keys = ('validation_cert',) for k, v in cfg['chef'].items(): + if k in unrendered_keys: + continue self.assertIn(v, c) for k, v in cc_chef.CHEF_RB_TPL_DEFAULTS.items(): - if isinstance(v, six.string_types): - self.assertIn(v, c) + if k in unrendered_keys: + continue + # the value from the cfg overrides that in the default + val = cfg['chef'].get(k, v) + if isinstance(val, six.string_types): + self.assertIn(val, c) c = util.load_file(cc_chef.CHEF_FB_PATH) self.assertEqual({}, json.loads(c)) @@ -131,3 +142,53 @@ class TestChef(t_help.FilesystemMockingTestCase): c = util.load_file(cc_chef.CHEF_RB_PATH) self.assertNotIn('json_attribs', c) self.assertNotIn('Formatter.show_time', c) + + @t_help.skipIf(not os.path.isfile(CLIENT_TEMPL), + CLIENT_TEMPL + " is not available") + def test_validation_cert_and_validation_key(self): + # test validation_cert content is written to validation_key path + tpl_file = util.load_file('templates/chef_client.rb.tmpl') + self.patchUtils(self.tmp) + self.patchOS(self.tmp) + + util.write_file('/etc/cloud/templates/chef_client.rb.tmpl', tpl_file) + v_path = '/etc/chef/vkey.pem' + v_cert = 'this is my cert' + cfg = { + 'chef': { + 'server_url': 'localhost', + 'validation_name': 'bob', + 'validation_key': v_path, + 'validation_cert': v_cert + }, + } + cc_chef.handle('chef', cfg, self.fetch_cloud('ubuntu'), LOG, []) + content = util.load_file(cc_chef.CHEF_RB_PATH) + self.assertIn(v_path, content) + util.load_file(v_path) + self.assertEqual(v_cert, util.load_file(v_path)) + + def test_validation_cert_with_system(self): + # test validation_cert content is not written over system file + tpl_file = util.load_file('templates/chef_client.rb.tmpl') + self.patchUtils(self.tmp) + self.patchOS(self.tmp) + + v_path = '/etc/chef/vkey.pem' + v_cert = "system" + expected_cert = "this is the system file certificate" + cfg = { + 'chef': { + 'server_url': 'localhost', + 'validation_name': 'bob', + 'validation_key': v_path, + 'validation_cert': v_cert + }, + } + util.write_file('/etc/cloud/templates/chef_client.rb.tmpl', tpl_file) + util.write_file(v_path, expected_cert) + cc_chef.handle('chef', cfg, self.fetch_cloud('ubuntu'), LOG, []) + content = util.load_file(cc_chef.CHEF_RB_PATH) + self.assertIn(v_path, content) + util.load_file(v_path) + self.assertEqual(expected_cert, util.load_file(v_path)) -- cgit v1.2.3 From 2b3f56294576998246e13f9b07074bad7b4bf212 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 12 Apr 2016 10:46:29 -0400 Subject: provide a warning if 'system' but file does not exist --- cloudinit/config/cc_chef.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py index 07dacb0c..6ecaf8db 100644 --- a/cloudinit/config/cc_chef.py +++ b/cloudinit/config/cc_chef.py @@ -209,8 +209,13 @@ def handle(name, cfg, cloud, log, _args): vcert = chef_cfg.get('validation_cert') # special value 'system' means do not overwrite the file # but still render the template to contain 'validation_key' - if vcert and vcert != "system": - util.write_file(vkey_path, vcert) + if vcert: + if vcert != "system": + util.write_file(vkey_path, vcert) + elif not os.path.isfile(vkey_path): + log.warn("chef validation_cert provided as 'system', but " + "validation_key path '%s' does not exist.", + vkey_path) # Create the chef config from template template_fn = cloud.get_template_filename('chef_client.rb') -- cgit v1.2.3 From cc549350c6010908add773a8b74e5d1ac840bbf1 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 12 Apr 2016 11:54:31 -0400 Subject: DataSourceNoCloud: fix check_instance_id when upgraded A system that had booted, upgraded, and then rebooted would show a cloud-init stack trace as it attempted to run new code with the old pickled object. The old object would not have the seed_dirs attribute. So we check and fallback correctly if that is not present. LP: #1568150 --- cloudinit/sources/DataSourceNoCloud.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py index c2fba4d2..74d0e5ec 100644 --- a/cloudinit/sources/DataSourceNoCloud.py +++ b/cloudinit/sources/DataSourceNoCloud.py @@ -216,8 +216,11 @@ class DataSourceNoCloud(sources.DataSource): if not current: return None + # LP: #1568150 need getattr in the case that an old class object + # has been loaded from a pickled file and now executing new source. + dirs = getattr(self, 'seed_dirs', [self.seed_dir]) quick_id = _quick_read_instance_id(cmdline_id=self.cmdline_id, - dirs=self.seed_dirs) + dirs=dirs) if not quick_id: return None return quick_id == current @@ -238,6 +241,8 @@ def _quick_read_instance_id(cmdline_id, dirs=None): return fill[iid_key] for d in dirs: + if d is None: + continue try: data = util.pathprefix2dict(d, required=['meta-data']) md = util.load_yaml(data['meta-data']) -- cgit v1.2.3 From 5cfe3d6fa2d50a68f9b6c7ceb4b9d5db09687782 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 12 Apr 2016 12:57:50 -0400 Subject: SmartOS, CloudSigma: fix error when dmi data is not availble In Cloudsigma, the datasource would warn if no product id was availble. SmartOS would log exception. This fixes both of those, changing the warning to a debug message. LP: #1569469 --- cloudinit/sources/DataSourceCloudSigma.py | 2 +- cloudinit/sources/DataSourceSmartOS.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudinit/sources/DataSourceCloudSigma.py b/cloudinit/sources/DataSourceCloudSigma.py index f8f94759..d7d4e844 100644 --- a/cloudinit/sources/DataSourceCloudSigma.py +++ b/cloudinit/sources/DataSourceCloudSigma.py @@ -56,7 +56,7 @@ class DataSourceCloudSigma(sources.DataSource): LOG.debug("determining hypervisor product name via dmi data") sys_product_name = util.read_dmi_data("system-product-name") if not sys_product_name: - LOG.warn("failed to get hypervisor product name via dmi data") + LOG.debug("system-product-name not available in dmi data") return False else: LOG.debug("detected hypervisor as %s", sys_product_name) diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py index 5edab152..6cbd8dfa 100644 --- a/cloudinit/sources/DataSourceSmartOS.py +++ b/cloudinit/sources/DataSourceSmartOS.py @@ -242,7 +242,7 @@ class DataSourceSmartOS(sources.DataSource): # SDC KVM instances will provide dmi data, LX-brand does not if self.smartos_type == 'kvm': dmi_info = dmi_data() - if dmi_info is False: + if dmi_info is None: LOG.debug("No dmidata utility found") return False -- cgit v1.2.3 From 96cc3852d8126af2dba7cd778473c23bcd883ceb Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Wed, 13 Apr 2016 11:48:38 -0400 Subject: skip bridges when generating fallback networking It does not make sense to consider bridges when searching for fallback networking. If the system is configured with a bridge, then its probably for some purpose other than to get to a metadata service. Considering the bridge could make cloud-init pick the wrong device on reboot. LP: #1569974 --- cloudinit/net/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py index 40929c6e..31110292 100644 --- a/cloudinit/net/__init__.py +++ b/cloudinit/net/__init__.py @@ -646,6 +646,9 @@ def generate_fallback_config(): connected = [] possibly_connected = [] for interface in potential_interfaces: + if os.path.exists(sys_dev_path(interface, "bridge")): + # skip any bridges + continue try: carrier = int(sys_netdev_info(interface, 'carrier')) if carrier: -- cgit v1.2.3 From a551cb080388c2016bcf23981f99a4a6aa0fe198 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Wed, 13 Apr 2016 12:35:50 -0400 Subject: phone_home: allow usage of fqdn This simply allows the phone_home template to pass the systems fully qualified domain name. LP: #1566824 --- ChangeLog | 1 + cloudinit/config/cc_phone_home.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index d7c2fc36..7bfd0c0a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -104,6 +104,7 @@ - systemd: do not specify After of obsolete syslog.target (LP: #1536964) - centos: Ensure that resolve conf object is written as a str (LP: #1479988) - chef: straighten out validation_cert and validation_key (LP: #1568940) + - phone_home: allow usage of fqdn (LP: #1566824) [Ollie Armstrong] 0.7.6: - open 0.7.6 diff --git a/cloudinit/config/cc_phone_home.py b/cloudinit/config/cc_phone_home.py index 18a7ddad..3dcc9459 100644 --- a/cloudinit/config/cc_phone_home.py +++ b/cloudinit/config/cc_phone_home.py @@ -30,7 +30,8 @@ POST_LIST_ALL = [ 'pub_key_rsa', 'pub_key_ecdsa', 'instance_id', - 'hostname' + 'hostname', + 'fdqn' ] @@ -41,7 +42,8 @@ POST_LIST_ALL = [ # # phone_home: # url: http://my.foo.bar/$INSTANCE_ID/ -# post: [ pub_key_dsa, pub_key_rsa, pub_key_ecdsa, instance_id +# post: [ pub_key_dsa, pub_key_rsa, pub_key_ecdsa, instance_id, hostname, +# fqdn ] # def handle(name, cfg, cloud, log, args): if len(args) != 0: @@ -74,6 +76,7 @@ def handle(name, cfg, cloud, log, args): all_keys = {} all_keys['instance_id'] = cloud.get_instance_id() all_keys['hostname'] = cloud.get_hostname() + all_keys['fqdn'] = cloud.get_hostname(fqdn=True) pubkeys = { 'pub_key_dsa': '/etc/ssh/ssh_host_dsa_key.pub', -- cgit v1.2.3