Merge ~oddbloke/cloud-init/+git/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel

Proposed by Dan Watkins
Status: Merged
Merged at revision: 1639758c62b57cbe38afbf583472aaf67cbf6616
Proposed branch: ~oddbloke/cloud-init/+git/cloud-init:ubuntu/devel
Merge into: cloud-init:ubuntu/devel
Diff against target: 2218 lines (+1161/-150)
51 files modified
.gitignore (+1/-0)
cloudinit/cmd/clean.py (+14/-13)
cloudinit/cmd/tests/test_clean.py (+4/-2)
cloudinit/config/cc_apt_pipelining.py (+2/-2)
cloudinit/config/cc_chef.py (+3/-0)
cloudinit/config/cc_rsyslog.py (+1/-1)
cloudinit/config/schema.py (+1/-1)
cloudinit/config/tests/test_apt_pipelining.py (+28/-0)
cloudinit/distros/__init__.py (+9/-4)
cloudinit/handlers/upstart_job.py (+1/-1)
cloudinit/net/netplan.py (+2/-1)
cloudinit/net/network_state.py (+2/-2)
cloudinit/net/tests/test_dhcp.py (+1/-0)
cloudinit/netinfo.py (+5/-2)
cloudinit/safeyaml.py (+7/-0)
cloudinit/sources/DataSourceAzure.py (+9/-4)
cloudinit/sources/DataSourceEc2.py (+22/-1)
cloudinit/sources/DataSourceOVF.py (+3/-1)
cloudinit/sources/helpers/azure.py (+78/-31)
cloudinit/sources/helpers/openstack.py (+6/-6)
cloudinit/stages.py (+2/-2)
cloudinit/tests/helpers.py (+2/-21)
cloudinit/tests/test_netinfo.py (+14/-0)
cloudinit/url_helper.py (+1/-1)
cloudinit/util.py (+11/-17)
debian/changelog (+44/-0)
debian/cloud-init.lintian-overrides (+4/-0)
debian/control (+4/-5)
doc/examples/cloud-config-chef.txt (+3/-0)
doc/examples/cloud-config-disk-setup.txt (+17/-2)
doc/rtd/topics/datasources/ec2.rst (+11/-0)
doc/rtd/topics/instancedata.rst (+1/-1)
doc/rtd/topics/merging.rst (+84/-6)
templates/chef_client.rb.tmpl (+4/-1)
tests/cloud_tests/verify.py (+7/-2)
tests/data/azure/parse_certificates_fingerprints (+4/-0)
tests/data/azure/parse_certificates_pem (+152/-0)
tests/data/azure/pubkey_extract_cert (+13/-0)
tests/data/azure/pubkey_extract_ssh_key (+1/-0)
tests/data/netinfo/freebsd-ifconfig-output (+17/-0)
tests/data/netinfo/freebsd-netdev-formatted-output (+11/-0)
tests/unittests/test_datasource/test_azure.py (+2/-3)
tests/unittests/test_datasource/test_azure_helper.py (+65/-6)
tests/unittests/test_datasource/test_configdrive.py (+27/-8)
tests/unittests/test_datasource/test_ec2.py (+24/-0)
tests/unittests/test_distros/test_create_users.py (+28/-0)
tests/unittests/test_distros/test_netconfig.py (+1/-1)
tests/unittests/test_ds_identify.py (+1/-1)
tests/unittests/test_handler/test_handler_chef.py (+3/-0)
tests/unittests/test_net.py (+397/-0)
tools/cloud-init-per (+7/-1)
Reviewer Review Type Date Requested Status
Ryan Harper Approve
Server Team CI bot continuous-integration Needs Fixing
Review via email: mp+364099@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:8a0521081e045944233b49eca1b9fc09cffb033e
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/364099/+edit-commit-message

https://jenkins.ubuntu.com/server/job/cloud-init-ci/618/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/618/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:d78d4d09dfda2e53057b72f485cd26657a044649
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/364099/+edit-commit-message

https://jenkins.ubuntu.com/server/job/cloud-init-ci/619/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/619/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:8f319b3aeff1323481fcc5510a2c6515897f6272
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/364099/+edit-commit-message

https://jenkins.ubuntu.com/server/job/cloud-init-ci/620/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/620/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:1639758c62b57cbe38afbf583472aaf67cbf6616
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/364099/+edit-commit-message

https://jenkins.ubuntu.com/server/job/cloud-init-ci/621/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/621/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

This looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.gitignore b/.gitignore
index 75565ed..80c509e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ prime
11stage11stage
12*.snap12*.snap
13*.cover13*.cover
14.idea/
diff --git a/cloudinit/cmd/clean.py b/cloudinit/cmd/clean.py
index de22f7f..30e49de 100644
--- a/cloudinit/cmd/clean.py
+++ b/cloudinit/cmd/clean.py
@@ -5,12 +5,13 @@
5"""Define 'clean' utility and handler as part of cloud-init commandline."""5"""Define 'clean' utility and handler as part of cloud-init commandline."""
66
7import argparse7import argparse
8import glob
8import os9import os
9import sys10import sys
1011
11from cloudinit.stages import Init12from cloudinit.stages import Init
12from cloudinit.util import (13from cloudinit.util import (
13 ProcessExecutionError, chdir, del_dir, del_file, get_config_logfiles,14 ProcessExecutionError, del_dir, del_file, get_config_logfiles,
14 is_link, subp)15 is_link, subp)
1516
1617
@@ -61,18 +62,18 @@ def remove_artifacts(remove_logs, remove_seed=False):
6162
62 if not os.path.isdir(init.paths.cloud_dir):63 if not os.path.isdir(init.paths.cloud_dir):
63 return 0 # Artifacts dir already cleaned64 return 0 # Artifacts dir already cleaned
64 with chdir(init.paths.cloud_dir):65 seed_path = os.path.join(init.paths.cloud_dir, 'seed')
65 for path in os.listdir('.'):66 for path in glob.glob('%s/*' % init.paths.cloud_dir):
66 if path == 'seed' and not remove_seed:67 if path == seed_path and not remove_seed:
67 continue68 continue
68 try:69 try:
69 if os.path.isdir(path) and not is_link(path):70 if os.path.isdir(path) and not is_link(path):
70 del_dir(path)71 del_dir(path)
71 else:72 else:
72 del_file(path)73 del_file(path)
73 except OSError as e:74 except OSError as e:
74 error('Could not remove {0}: {1}'.format(path, str(e)))75 error('Could not remove {0}: {1}'.format(path, str(e)))
75 return 176 return 1
76 return 077 return 0
7778
7879
diff --git a/cloudinit/cmd/tests/test_clean.py b/cloudinit/cmd/tests/test_clean.py
index 5a3ec3b..f092ab3 100644
--- a/cloudinit/cmd/tests/test_clean.py
+++ b/cloudinit/cmd/tests/test_clean.py
@@ -22,7 +22,8 @@ class TestClean(CiTestCase):
22 class FakeInit(object):22 class FakeInit(object):
23 cfg = {'def_log_file': self.log1,23 cfg = {'def_log_file': self.log1,
24 'output': {'all': '|tee -a {0}'.format(self.log2)}}24 'output': {'all': '|tee -a {0}'.format(self.log2)}}
25 paths = mypaths(cloud_dir=self.artifact_dir)25 # Ensure cloud_dir has a trailing slash, to match real behaviour
26 paths = mypaths(cloud_dir='{}/'.format(self.artifact_dir))
2627
27 def __init__(self, ds_deps):28 def __init__(self, ds_deps):
28 pass29 pass
@@ -136,7 +137,8 @@ class TestClean(CiTestCase):
136 clean.remove_artifacts, remove_logs=False)137 clean.remove_artifacts, remove_logs=False)
137 self.assertEqual(1, retcode)138 self.assertEqual(1, retcode)
138 self.assertEqual(139 self.assertEqual(
139 'ERROR: Could not remove dir1: oops\n', m_stderr.getvalue())140 'ERROR: Could not remove %s/dir1: oops\n' % self.artifact_dir,
141 m_stderr.getvalue())
140142
141 def test_handle_clean_args_reboots(self):143 def test_handle_clean_args_reboots(self):
142 """handle_clean_args_reboots when reboot arg is provided."""144 """handle_clean_args_reboots when reboot arg is provided."""
diff --git a/cloudinit/config/cc_apt_pipelining.py b/cloudinit/config/cc_apt_pipelining.py
index cdf28cd..459332a 100644
--- a/cloudinit/config/cc_apt_pipelining.py
+++ b/cloudinit/config/cc_apt_pipelining.py
@@ -49,7 +49,7 @@ APT_PIPE_TPL = ("//Written by cloud-init per 'apt_pipelining'\n"
4949
50def handle(_name, cfg, _cloud, log, _args):50def handle(_name, cfg, _cloud, log, _args):
5151
52 apt_pipe_value = util.get_cfg_option_str(cfg, "apt_pipelining", False)52 apt_pipe_value = util.get_cfg_option_str(cfg, "apt_pipelining", 'os')
53 apt_pipe_value_s = str(apt_pipe_value).lower().strip()53 apt_pipe_value_s = str(apt_pipe_value).lower().strip()
5454
55 if apt_pipe_value_s == "false":55 if apt_pipe_value_s == "false":
@@ -59,7 +59,7 @@ def handle(_name, cfg, _cloud, log, _args):
59 elif apt_pipe_value_s in [str(b) for b in range(0, 6)]:59 elif apt_pipe_value_s in [str(b) for b in range(0, 6)]:
60 write_apt_snippet(apt_pipe_value_s, log, DEFAULT_FILE)60 write_apt_snippet(apt_pipe_value_s, log, DEFAULT_FILE)
61 else:61 else:
62 log.warn("Invalid option for apt_pipeling: %s", apt_pipe_value)62 log.warn("Invalid option for apt_pipelining: %s", apt_pipe_value)
6363
6464
65def write_apt_snippet(setting, log, f_name):65def write_apt_snippet(setting, log, f_name):
diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py
index 46abedd..a624030 100644
--- a/cloudinit/config/cc_chef.py
+++ b/cloudinit/config/cc_chef.py
@@ -51,6 +51,7 @@ file).
5151
52 chef:52 chef:
53 client_key:53 client_key:
54 encrypted_data_bag_secret:
54 environment:55 environment:
55 file_backup_path:56 file_backup_path:
56 file_cache_path:57 file_cache_path:
@@ -114,6 +115,7 @@ CHEF_RB_TPL_DEFAULTS = {
114 'file_backup_path': "/var/backups/chef",115 'file_backup_path': "/var/backups/chef",
115 'pid_file': "/var/run/chef/client.pid",116 'pid_file': "/var/run/chef/client.pid",
116 'show_time': True,117 'show_time': True,
118 'encrypted_data_bag_secret': None,
117}119}
118CHEF_RB_TPL_BOOL_KEYS = frozenset(['show_time'])120CHEF_RB_TPL_BOOL_KEYS = frozenset(['show_time'])
119CHEF_RB_TPL_PATH_KEYS = frozenset([121CHEF_RB_TPL_PATH_KEYS = frozenset([
@@ -124,6 +126,7 @@ CHEF_RB_TPL_PATH_KEYS = frozenset([
124 'json_attribs',126 'json_attribs',
125 'file_cache_path',127 'file_cache_path',
126 'pid_file',128 'pid_file',
129 'encrypted_data_bag_secret',
127])130])
128CHEF_RB_TPL_KEYS = list(CHEF_RB_TPL_DEFAULTS.keys())131CHEF_RB_TPL_KEYS = list(CHEF_RB_TPL_DEFAULTS.keys())
129CHEF_RB_TPL_KEYS.extend(CHEF_RB_TPL_BOOL_KEYS)132CHEF_RB_TPL_KEYS.extend(CHEF_RB_TPL_BOOL_KEYS)
diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py
index 27d2366..22b1753 100644
--- a/cloudinit/config/cc_rsyslog.py
+++ b/cloudinit/config/cc_rsyslog.py
@@ -203,7 +203,7 @@ LOG = logging.getLogger(__name__)
203COMMENT_RE = re.compile(r'[ ]*[#]+[ ]*')203COMMENT_RE = re.compile(r'[ ]*[#]+[ ]*')
204HOST_PORT_RE = re.compile(204HOST_PORT_RE = re.compile(
205 r'^(?P<proto>[@]{0,2})'205 r'^(?P<proto>[@]{0,2})'
206 r'(([[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'206 r'(([\[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'
207 r'([:](?P<port>[0-9]+))?$')207 r'([:](?P<port>[0-9]+))?$')
208208
209209
diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py
index 080a6d0..807c3ee 100644
--- a/cloudinit/config/schema.py
+++ b/cloudinit/config/schema.py
@@ -367,7 +367,7 @@ def handle_schema_args(name, args):
367 if not args.annotate:367 if not args.annotate:
368 error(str(e))368 error(str(e))
369 except RuntimeError as e:369 except RuntimeError as e:
370 error(str(e))370 error(str(e))
371 else:371 else:
372 print("Valid cloud-config file {0}".format(args.config_file))372 print("Valid cloud-config file {0}".format(args.config_file))
373 if args.doc:373 if args.doc:
diff --git a/cloudinit/config/tests/test_apt_pipelining.py b/cloudinit/config/tests/test_apt_pipelining.py
374new file mode 100644374new file mode 100644
index 0000000..2a6bb10
--- /dev/null
+++ b/cloudinit/config/tests/test_apt_pipelining.py
@@ -0,0 +1,28 @@
1# This file is part of cloud-init. See LICENSE file for license information.
2
3"""Tests cc_apt_pipelining handler"""
4
5import cloudinit.config.cc_apt_pipelining as cc_apt_pipelining
6
7from cloudinit.tests.helpers import CiTestCase, mock
8
9
10class TestAptPipelining(CiTestCase):
11
12 @mock.patch('cloudinit.config.cc_apt_pipelining.util.write_file')
13 def test_not_disabled_by_default(self, m_write_file):
14 """ensure that default behaviour is to not disable pipelining"""
15 cc_apt_pipelining.handle('foo', {}, None, mock.MagicMock(), None)
16 self.assertEqual(0, m_write_file.call_count)
17
18 @mock.patch('cloudinit.config.cc_apt_pipelining.util.write_file')
19 def test_false_disables_pipelining(self, m_write_file):
20 """ensure that pipelining can be disabled with correct config"""
21 cc_apt_pipelining.handle(
22 'foo', {'apt_pipelining': 'false'}, None, mock.MagicMock(), None)
23 self.assertEqual(1, m_write_file.call_count)
24 args, _ = m_write_file.call_args
25 self.assertEqual(cc_apt_pipelining.DEFAULT_FILE, args[0])
26 self.assertIn('Pipeline-Depth "0"', args[1])
27
28# vi: ts=4 expandtab
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index ef618c2..20c994d 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -577,11 +577,16 @@ class Distro(object):
577 """577 """
578 Lock the password of a user, i.e., disable password logins578 Lock the password of a user, i.e., disable password logins
579 """579 """
580 # passwd must use short '-l' due to SLES11 lacking long form '--lock'
581 lock_tools = (['passwd', '-l', name], ['usermod', '--lock', name])
580 try:582 try:
581 # Need to use the short option name '-l' instead of '--lock'583 cmd = next(l for l in lock_tools if util.which(l[0]))
582 # (which would be more descriptive) since SLES 11 doesn't know584 except StopIteration:
583 # about long names.585 raise RuntimeError((
584 util.subp(['passwd', '-l', name])586 "Unable to lock user account '%s'. No tools available. "
587 " Tried: %s.") % (name, [c[0] for c in lock_tools]))
588 try:
589 util.subp(cmd)
585 except Exception as e:590 except Exception as e:
586 util.logexc(LOG, 'Failed to disable password for user %s', name)591 util.logexc(LOG, 'Failed to disable password for user %s', name)
587 raise e592 raise e
diff --git a/cloudinit/handlers/upstart_job.py b/cloudinit/handlers/upstart_job.py
index 83fb072..003cad6 100644
--- a/cloudinit/handlers/upstart_job.py
+++ b/cloudinit/handlers/upstart_job.py
@@ -89,7 +89,7 @@ def _has_suitable_upstart():
89 util.subp(["dpkg", "--compare-versions", dpkg_ver, "ge", good])89 util.subp(["dpkg", "--compare-versions", dpkg_ver, "ge", good])
90 return True90 return True
91 except util.ProcessExecutionError as e:91 except util.ProcessExecutionError as e:
92 if e.exit_code is 1:92 if e.exit_code == 1:
93 pass93 pass
94 else:94 else:
95 util.logexc(LOG, "dpkg --compare-versions failed [%s]",95 util.logexc(LOG, "dpkg --compare-versions failed [%s]",
diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
index 21517fd..e54a34e 100644
--- a/cloudinit/net/netplan.py
+++ b/cloudinit/net/netplan.py
@@ -361,7 +361,8 @@ class Renderer(renderer.Renderer):
361 if section:361 if section:
362 dump = util.yaml_dumps({name: section},362 dump = util.yaml_dumps({name: section},
363 explicit_start=False,363 explicit_start=False,
364 explicit_end=False)364 explicit_end=False,
365 noalias=True)
365 txt = util.indent(dump, ' ' * 4)366 txt = util.indent(dump, ' ' * 4)
366 return [txt]367 return [txt]
367 return []368 return []
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index f76e508..539b76d 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -706,9 +706,9 @@ class NetworkStateInterpreter(object):
706 """Common ipconfig extraction from v2 to v1 subnets array."""706 """Common ipconfig extraction from v2 to v1 subnets array."""
707707
708 subnets = []708 subnets = []
709 if 'dhcp4' in cfg:709 if cfg.get('dhcp4'):
710 subnets.append({'type': 'dhcp4'})710 subnets.append({'type': 'dhcp4'})
711 if 'dhcp6' in cfg:711 if cfg.get('dhcp6'):
712 self.use_ipv6 = True712 self.use_ipv6 = True
713 subnets.append({'type': 'dhcp6'})713 subnets.append({'type': 'dhcp6'})
714714
diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
index 79e8842..5139024 100644
--- a/cloudinit/net/tests/test_dhcp.py
+++ b/cloudinit/net/tests/test_dhcp.py
@@ -117,6 +117,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
117 self.assertEqual('eth9', call[0][1])117 self.assertEqual('eth9', call[0][1])
118 self.assertIn('/var/tmp/cloud-init/cloud-init-dhcp-', call[0][2])118 self.assertIn('/var/tmp/cloud-init/cloud-init-dhcp-', call[0][2])
119119
120 @mock.patch('time.sleep', mock.MagicMock())
120 @mock.patch('cloudinit.net.dhcp.os.kill')121 @mock.patch('cloudinit.net.dhcp.os.kill')
121 @mock.patch('cloudinit.net.dhcp.util.subp')122 @mock.patch('cloudinit.net.dhcp.util.subp')
122 def test_dhcp_discovery_run_in_sandbox_warns_invalid_pid(self, m_subp,123 def test_dhcp_discovery_run_in_sandbox_warns_invalid_pid(self, m_subp,
diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
index 9ff929c..e91cd26 100644
--- a/cloudinit/netinfo.py
+++ b/cloudinit/netinfo.py
@@ -141,6 +141,9 @@ def _netdev_info_ifconfig(ifconfig_data):
141 res = re.match(r'.*<(\S+)>', toks[i + 1])141 res = re.match(r'.*<(\S+)>', toks[i + 1])
142 if res:142 if res:
143 devs[curdev]['ipv6'][-1]['scope6'] = res.group(1)143 devs[curdev]['ipv6'][-1]['scope6'] = res.group(1)
144 else:
145 devs[curdev]['ipv6'][-1]['scope6'] = toks[i + 1]
146
144 return devs147 return devs
145148
146149
@@ -389,8 +392,8 @@ def netdev_pformat():
389 addr.get('scope', empty), data["hwaddr"]))392 addr.get('scope', empty), data["hwaddr"]))
390 for addr in data.get('ipv6'):393 for addr in data.get('ipv6'):
391 tbl.add_row(394 tbl.add_row(
392 (dev, data["up"], addr["ip"], empty, addr["scope6"],395 (dev, data["up"], addr["ip"], empty,
393 data["hwaddr"]))396 addr.get("scope6", empty), data["hwaddr"]))
394 if len(data.get('ipv6')) + len(data.get('ipv4')) == 0:397 if len(data.get('ipv6')) + len(data.get('ipv4')) == 0:
395 tbl.add_row((dev, data["up"], empty, empty, empty,398 tbl.add_row((dev, data["up"], empty, empty, empty,
396 data["hwaddr"]))399 data["hwaddr"]))
diff --git a/cloudinit/safeyaml.py b/cloudinit/safeyaml.py
index 7bcf9dd..3bd5e03 100644
--- a/cloudinit/safeyaml.py
+++ b/cloudinit/safeyaml.py
@@ -17,6 +17,13 @@ _CustomSafeLoader.add_constructor(
17 _CustomSafeLoader.construct_python_unicode)17 _CustomSafeLoader.construct_python_unicode)
1818
1919
20class NoAliasSafeDumper(yaml.dumper.SafeDumper):
21 """A class which avoids constructing anchors/aliases on yaml dump"""
22
23 def ignore_aliases(self, data):
24 return True
25
26
20def load(blob):27def load(blob):
21 return(yaml.load(blob, Loader=_CustomSafeLoader))28 return(yaml.load(blob, Loader=_CustomSafeLoader))
2229
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index a4f998b..eccbee5 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -627,9 +627,11 @@ class DataSourceAzure(sources.DataSource):
627 if self.ds_cfg['agent_command'] == AGENT_START_BUILTIN:627 if self.ds_cfg['agent_command'] == AGENT_START_BUILTIN:
628 self.bounce_network_with_azure_hostname()628 self.bounce_network_with_azure_hostname()
629629
630 pubkey_info = self.cfg.get('_pubkeys', None)
630 metadata_func = partial(get_metadata_from_fabric,631 metadata_func = partial(get_metadata_from_fabric,
631 fallback_lease_file=self.632 fallback_lease_file=self.
632 dhclient_lease_file)633 dhclient_lease_file,
634 pubkey_info=pubkey_info)
633 else:635 else:
634 metadata_func = self.get_metadata_from_agent636 metadata_func = self.get_metadata_from_agent
635637
@@ -642,6 +644,7 @@ class DataSourceAzure(sources.DataSource):
642 "Error communicating with Azure fabric; You may experience."644 "Error communicating with Azure fabric; You may experience."
643 "connectivity issues.", exc_info=True)645 "connectivity issues.", exc_info=True)
644 return False646 return False
647
645 util.del_file(REPORTED_READY_MARKER_FILE)648 util.del_file(REPORTED_READY_MARKER_FILE)
646 util.del_file(REPROVISION_MARKER_FILE)649 util.del_file(REPROVISION_MARKER_FILE)
647 return fabric_data650 return fabric_data
@@ -909,13 +912,15 @@ def find_child(node, filter_func):
909def load_azure_ovf_pubkeys(sshnode):912def load_azure_ovf_pubkeys(sshnode):
910 # This parses a 'SSH' node formatted like below, and returns913 # This parses a 'SSH' node formatted like below, and returns
911 # an array of dicts.914 # an array of dicts.
912 # [{'fp': '6BE7A7C3C8A8F4B123CCA5D0C2F1BE4CA7B63ED7',915 # [{'fingerprint': '6BE7A7C3C8A8F4B123CCA5D0C2F1BE4CA7B63ED7',
913 # 'path': 'where/to/go'}]916 # 'path': '/where/to/go'}]
914 #917 #
915 # <SSH><PublicKeys>918 # <SSH><PublicKeys>
916 # <PublicKey><Fingerprint>ABC</FingerPrint><Path>/ABC</Path>919 # <PublicKey><Fingerprint>ABC</FingerPrint><Path>/x/y/z</Path>
917 # ...920 # ...
918 # </PublicKeys></SSH>921 # </PublicKeys></SSH>
922 # Under some circumstances, there may be a <Value> element along with the
923 # Fingerprint and Path. Pass those along if they appear.
919 results = find_child(sshnode, lambda n: n.localName == "PublicKeys")924 results = find_child(sshnode, lambda n: n.localName == "PublicKeys")
920 if len(results) == 0:925 if len(results) == 0:
921 return []926 return []
diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
index 9ccf2cd..4f2f6cc 100644
--- a/cloudinit/sources/DataSourceEc2.py
+++ b/cloudinit/sources/DataSourceEc2.py
@@ -19,6 +19,7 @@ from cloudinit import sources
19from cloudinit import url_helper as uhelp19from cloudinit import url_helper as uhelp
20from cloudinit import util20from cloudinit import util
21from cloudinit import warnings21from cloudinit import warnings
22from cloudinit.event import EventType
2223
23LOG = logging.getLogger(__name__)24LOG = logging.getLogger(__name__)
2425
@@ -107,6 +108,19 @@ class DataSourceEc2(sources.DataSource):
107 'dynamic', {}).get('instance-identity', {}).get('document', {})108 'dynamic', {}).get('instance-identity', {}).get('document', {})
108 return True109 return True
109110
111 def is_classic_instance(self):
112 """Report if this instance type is Ec2 Classic (non-vpc)."""
113 if not self.metadata:
114 # Can return False on inconclusive as we are also called in
115 # network_config where metadata will be present.
116 # Secondary call site is in packaging postinst script.
117 return False
118 ifaces_md = self.metadata.get('network', {}).get('interfaces', {})
119 for _mac, mac_data in ifaces_md.get('macs', {}).items():
120 if 'vpc-id' in mac_data:
121 return False
122 return True
123
110 @property124 @property
111 def launch_index(self):125 def launch_index(self):
112 if not self.metadata:126 if not self.metadata:
@@ -320,6 +334,13 @@ class DataSourceEc2(sources.DataSource):
320 if isinstance(net_md, dict):334 if isinstance(net_md, dict):
321 result = convert_ec2_metadata_network_config(335 result = convert_ec2_metadata_network_config(
322 net_md, macs_to_nics=macs_to_nics, fallback_nic=iface)336 net_md, macs_to_nics=macs_to_nics, fallback_nic=iface)
337 # RELEASE_BLOCKER: Xenial debian/postinst needs to add
338 # EventType.BOOT on upgrade path for classic.
339
340 # Non-VPC (aka Classic) Ec2 instances need to rewrite the
341 # network config file every boot due to MAC address change.
342 if self.is_classic_instance():
343 self.update_events['network'].add(EventType.BOOT)
323 else:344 else:
324 LOG.warning("Metadata 'network' key not valid: %s.", net_md)345 LOG.warning("Metadata 'network' key not valid: %s.", net_md)
325 self._network_config = result346 self._network_config = result
@@ -442,7 +463,7 @@ def identify_aws(data):
442 if (data['uuid'].startswith('ec2') and463 if (data['uuid'].startswith('ec2') and
443 (data['uuid_source'] == 'hypervisor' or464 (data['uuid_source'] == 'hypervisor' or
444 data['uuid'] == data['serial'])):465 data['uuid'] == data['serial'])):
445 return CloudNames.AWS466 return CloudNames.AWS
446467
447 return None468 return None
448469
diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py
index 3a3fcdf..70e7a5c 100644
--- a/cloudinit/sources/DataSourceOVF.py
+++ b/cloudinit/sources/DataSourceOVF.py
@@ -15,6 +15,8 @@ import os
15import re15import re
16import time16import time
1717
18import six
19
18from cloudinit import log as logging20from cloudinit import log as logging
19from cloudinit import sources21from cloudinit import sources
20from cloudinit import util22from cloudinit import util
@@ -434,7 +436,7 @@ def maybe_cdrom_device(devname):
434 """436 """
435 if not devname:437 if not devname:
436 return False438 return False
437 elif not isinstance(devname, util.string_types):439 elif not isinstance(devname, six.string_types):
438 raise ValueError("Unexpected input for devname: %s" % devname)440 raise ValueError("Unexpected input for devname: %s" % devname)
439441
440 # resolve '..' and multi '/' elements442 # resolve '..' and multi '/' elements
diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py
index e5696b1..2829dd2 100644
--- a/cloudinit/sources/helpers/azure.py
+++ b/cloudinit/sources/helpers/azure.py
@@ -138,9 +138,36 @@ class OpenSSLManager(object):
138 self.certificate = certificate138 self.certificate = certificate
139 LOG.debug('New certificate generated.')139 LOG.debug('New certificate generated.')
140140
141 def parse_certificates(self, certificates_xml):141 @staticmethod
142 tag = ElementTree.fromstring(certificates_xml).find(142 def _run_x509_action(action, cert):
143 './/Data')143 cmd = ['openssl', 'x509', '-noout', action]
144 result, _ = util.subp(cmd, data=cert)
145 return result
146
147 def _get_ssh_key_from_cert(self, certificate):
148 pub_key = self._run_x509_action('-pubkey', certificate)
149 keygen_cmd = ['ssh-keygen', '-i', '-m', 'PKCS8', '-f', '/dev/stdin']
150 ssh_key, _ = util.subp(keygen_cmd, data=pub_key)
151 return ssh_key
152
153 def _get_fingerprint_from_cert(self, certificate):
154 """openssl x509 formats fingerprints as so:
155 'SHA1 Fingerprint=07:3E:19:D1:4D:1C:79:92:24:C6:A0:FD:8D:DA:\
156 B6:A8:BF:27:D4:73\n'
157
158 Azure control plane passes that fingerprint as so:
159 '073E19D14D1C799224C6A0FD8DDAB6A8BF27D473'
160 """
161 raw_fp = self._run_x509_action('-fingerprint', certificate)
162 eq = raw_fp.find('=')
163 octets = raw_fp[eq+1:-1].split(':')
164 return ''.join(octets)
165
166 def _decrypt_certs_from_xml(self, certificates_xml):
167 """Decrypt the certificates XML document using the our private key;
168 return the list of certs and private keys contained in the doc.
169 """
170 tag = ElementTree.fromstring(certificates_xml).find('.//Data')
144 certificates_content = tag.text171 certificates_content = tag.text
145 lines = [172 lines = [
146 b'MIME-Version: 1.0',173 b'MIME-Version: 1.0',
@@ -151,32 +178,30 @@ class OpenSSLManager(object):
151 certificates_content.encode('utf-8'),178 certificates_content.encode('utf-8'),
152 ]179 ]
153 with cd(self.tmpdir):180 with cd(self.tmpdir):
154 with open('Certificates.p7m', 'wb') as f:
155 f.write(b'\n'.join(lines))
156 out, _ = util.subp(181 out, _ = util.subp(
157 'openssl cms -decrypt -in Certificates.p7m -inkey'182 'openssl cms -decrypt -in /dev/stdin -inkey'
158 ' {private_key} -recip {certificate} | openssl pkcs12 -nodes'183 ' {private_key} -recip {certificate} | openssl pkcs12 -nodes'
159 ' -password pass:'.format(**self.certificate_names),184 ' -password pass:'.format(**self.certificate_names),
160 shell=True)185 shell=True, data=b'\n'.join(lines))
161 private_keys, certificates = [], []186 return out
187
188 def parse_certificates(self, certificates_xml):
189 """Given the Certificates XML document, return a dictionary of
190 fingerprints and associated SSH keys derived from the certs."""
191 out = self._decrypt_certs_from_xml(certificates_xml)
162 current = []192 current = []
193 keys = {}
163 for line in out.splitlines():194 for line in out.splitlines():
164 current.append(line)195 current.append(line)
165 if re.match(r'[-]+END .*?KEY[-]+$', line):196 if re.match(r'[-]+END .*?KEY[-]+$', line):
166 private_keys.append('\n'.join(current))197 # ignore private_keys
167 current = []198 current = []
168 elif re.match(r'[-]+END .*?CERTIFICATE[-]+$', line):199 elif re.match(r'[-]+END .*?CERTIFICATE[-]+$', line):
169 certificates.append('\n'.join(current))200 certificate = '\n'.join(current)
201 ssh_key = self._get_ssh_key_from_cert(certificate)
202 fingerprint = self._get_fingerprint_from_cert(certificate)
203 keys[fingerprint] = ssh_key
170 current = []204 current = []
171 keys = []
172 for certificate in certificates:
173 with cd(self.tmpdir):
174 public_key, _ = util.subp(
175 'openssl x509 -noout -pubkey |'
176 'ssh-keygen -i -m PKCS8 -f /dev/stdin',
177 data=certificate,
178 shell=True)
179 keys.append(public_key)
180 return keys205 return keys
181206
182207
@@ -206,7 +231,6 @@ class WALinuxAgentShim(object):
206 self.dhcpoptions = dhcp_options231 self.dhcpoptions = dhcp_options
207 self._endpoint = None232 self._endpoint = None
208 self.openssl_manager = None233 self.openssl_manager = None
209 self.values = {}
210 self.lease_file = fallback_lease_file234 self.lease_file = fallback_lease_file
211235
212 def clean_up(self):236 def clean_up(self):
@@ -328,8 +352,9 @@ class WALinuxAgentShim(object):
328 LOG.debug('Azure endpoint found at %s', endpoint_ip_address)352 LOG.debug('Azure endpoint found at %s', endpoint_ip_address)
329 return endpoint_ip_address353 return endpoint_ip_address
330354
331 def register_with_azure_and_fetch_data(self):355 def register_with_azure_and_fetch_data(self, pubkey_info=None):
332 self.openssl_manager = OpenSSLManager()356 if self.openssl_manager is None:
357 self.openssl_manager = OpenSSLManager()
333 http_client = AzureEndpointHttpClient(self.openssl_manager.certificate)358 http_client = AzureEndpointHttpClient(self.openssl_manager.certificate)
334 LOG.info('Registering with Azure...')359 LOG.info('Registering with Azure...')
335 attempts = 0360 attempts = 0
@@ -347,16 +372,37 @@ class WALinuxAgentShim(object):
347 attempts += 1372 attempts += 1
348 LOG.debug('Successfully fetched GoalState XML.')373 LOG.debug('Successfully fetched GoalState XML.')
349 goal_state = GoalState(response.contents, http_client)374 goal_state = GoalState(response.contents, http_client)
350 public_keys = []375 ssh_keys = []
351 if goal_state.certificates_xml is not None:376 if goal_state.certificates_xml is not None and pubkey_info is not None:
352 LOG.debug('Certificate XML found; parsing out public keys.')377 LOG.debug('Certificate XML found; parsing out public keys.')
353 public_keys = self.openssl_manager.parse_certificates(378 keys_by_fingerprint = self.openssl_manager.parse_certificates(
354 goal_state.certificates_xml)379 goal_state.certificates_xml)
355 data = {380 ssh_keys = self._filter_pubkeys(keys_by_fingerprint, pubkey_info)
356 'public-keys': public_keys,
357 }
358 self._report_ready(goal_state, http_client)381 self._report_ready(goal_state, http_client)
359 return data382 return {'public-keys': ssh_keys}
383
384 def _filter_pubkeys(self, keys_by_fingerprint, pubkey_info):
385 """cloud-init expects a straightforward array of keys to be dropped
386 into the user's authorized_keys file. Azure control plane exposes
387 multiple public keys to the VM via wireserver. Select just the
388 user's key(s) and return them, ignoring any other certs.
389 """
390 keys = []
391 for pubkey in pubkey_info:
392 if 'value' in pubkey and pubkey['value']:
393 keys.append(pubkey['value'])
394 elif 'fingerprint' in pubkey and pubkey['fingerprint']:
395 fingerprint = pubkey['fingerprint']
396 if fingerprint in keys_by_fingerprint:
397 keys.append(keys_by_fingerprint[fingerprint])
398 else:
399 LOG.warning("ovf-env.xml specified PublicKey fingerprint "
400 "%s not found in goalstate XML", fingerprint)
401 else:
402 LOG.warning("ovf-env.xml specified PublicKey with neither "
403 "value nor fingerprint: %s", pubkey)
404
405 return keys
360406
361 def _report_ready(self, goal_state, http_client):407 def _report_ready(self, goal_state, http_client):
362 LOG.debug('Reporting ready to Azure fabric.')408 LOG.debug('Reporting ready to Azure fabric.')
@@ -373,11 +419,12 @@ class WALinuxAgentShim(object):
373 LOG.info('Reported ready to Azure fabric.')419 LOG.info('Reported ready to Azure fabric.')
374420
375421
376def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None):422def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None,
423 pubkey_info=None):
377 shim = WALinuxAgentShim(fallback_lease_file=fallback_lease_file,424 shim = WALinuxAgentShim(fallback_lease_file=fallback_lease_file,
378 dhcp_options=dhcp_opts)425 dhcp_options=dhcp_opts)
379 try:426 try:
380 return shim.register_with_azure_and_fetch_data()427 return shim.register_with_azure_and_fetch_data(pubkey_info=pubkey_info)
381 finally:428 finally:
382 shim.clean_up()429 shim.clean_up()
383430
diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py
index 9c29cea..8f06911 100644
--- a/cloudinit/sources/helpers/openstack.py
+++ b/cloudinit/sources/helpers/openstack.py
@@ -67,7 +67,7 @@ OS_VERSIONS = (
67 OS_ROCKY,67 OS_ROCKY,
68)68)
6969
70PHYSICAL_TYPES = (70KNOWN_PHYSICAL_TYPES = (
71 None,71 None,
72 'bgpovs', # not present in OpenStack upstream but used on OVH cloud.72 'bgpovs', # not present in OpenStack upstream but used on OVH cloud.
73 'bridge',73 'bridge',
@@ -600,9 +600,7 @@ def convert_net_json(network_json=None, known_macs=None):
600 subnet['ipv6'] = True600 subnet['ipv6'] = True
601 subnets.append(subnet)601 subnets.append(subnet)
602 cfg.update({'subnets': subnets})602 cfg.update({'subnets': subnets})
603 if link['type'] in PHYSICAL_TYPES:603 if link['type'] in ['bond']:
604 cfg.update({'type': 'physical', 'mac_address': link_mac_addr})
605 elif link['type'] in ['bond']:
606 params = {}604 params = {}
607 if link_mac_addr:605 if link_mac_addr:
608 params['mac_address'] = link_mac_addr606 params['mac_address'] = link_mac_addr
@@ -641,8 +639,10 @@ def convert_net_json(network_json=None, known_macs=None):
641 curinfo.update({'mac': link['vlan_mac_address'],639 curinfo.update({'mac': link['vlan_mac_address'],
642 'name': name})640 'name': name})
643 else:641 else:
644 raise ValueError(642 if link['type'] not in KNOWN_PHYSICAL_TYPES:
645 'Unknown network_data link type: %s' % link['type'])643 LOG.warning('Unknown network_data link type (%s); treating as'
644 ' physical', link['type'])
645 cfg.update({'type': 'physical', 'mac_address': link_mac_addr})
646646
647 config.append(cfg)647 config.append(cfg)
648 link_id_info[curinfo['id']] = curinfo648 link_id_info[curinfo['id']] = curinfo
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index 8a06412..da7d349 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -548,11 +548,11 @@ class Init(object):
548 with events.ReportEventStack("consume-user-data",548 with events.ReportEventStack("consume-user-data",
549 "reading and applying user-data",549 "reading and applying user-data",
550 parent=self.reporter):550 parent=self.reporter):
551 self._consume_userdata(frequency)551 self._consume_userdata(frequency)
552 with events.ReportEventStack("consume-vendor-data",552 with events.ReportEventStack("consume-vendor-data",
553 "reading and applying vendor-data",553 "reading and applying vendor-data",
554 parent=self.reporter):554 parent=self.reporter):
555 self._consume_vendordata(frequency)555 self._consume_vendordata(frequency)
556556
557 # Perform post-consumption adjustments so that557 # Perform post-consumption adjustments so that
558 # modules that run during the init stage reflect558 # modules that run during the init stage reflect
diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
index 2eb7b0c..f41180f 100644
--- a/cloudinit/tests/helpers.py
+++ b/cloudinit/tests/helpers.py
@@ -41,26 +41,6 @@ _real_subp = util.subp
41SkipTest = unittest2.SkipTest41SkipTest = unittest2.SkipTest
42skipIf = unittest2.skipIf42skipIf = unittest2.skipIf
4343
44# Used for detecting different python versions
45PY2 = False
46PY26 = False
47PY27 = False
48PY3 = False
49
50_PY_VER = sys.version_info
51_PY_MAJOR, _PY_MINOR, _PY_MICRO = _PY_VER[0:3]
52if (_PY_MAJOR, _PY_MINOR) <= (2, 6):
53 if (_PY_MAJOR, _PY_MINOR) == (2, 6):
54 PY26 = True
55 if (_PY_MAJOR, _PY_MINOR) >= (2, 0):
56 PY2 = True
57else:
58 if (_PY_MAJOR, _PY_MINOR) == (2, 7):
59 PY27 = True
60 PY2 = True
61 if (_PY_MAJOR, _PY_MINOR) >= (3, 0):
62 PY3 = True
63
6444
65# Makes the old path start45# Makes the old path start
66# with new base instead of whatever46# with new base instead of whatever
@@ -207,6 +187,7 @@ class CiTestCase(TestCase):
207 if self.with_logs:187 if self.with_logs:
208 # Remove the handler we setup188 # Remove the handler we setup
209 logging.getLogger().handlers = self.old_handlers189 logging.getLogger().handlers = self.old_handlers
190 logging.getLogger().level = None
210 util.subp = _real_subp191 util.subp = _real_subp
211 super(CiTestCase, self).tearDown()192 super(CiTestCase, self).tearDown()
212193
@@ -356,7 +337,7 @@ class FilesystemMockingTestCase(ResourceUsingTestCase):
356337
357 def patchOpen(self, new_root):338 def patchOpen(self, new_root):
358 trap_func = retarget_many_wrapper(new_root, 1, open)339 trap_func = retarget_many_wrapper(new_root, 1, open)
359 name = 'builtins.open' if PY3 else '__builtin__.open'340 name = 'builtins.open' if six.PY3 else '__builtin__.open'
360 self.patched_funcs.enter_context(mock.patch(name, trap_func))341 self.patched_funcs.enter_context(mock.patch(name, trap_func))
361342
362 def patchStdoutAndStderr(self, stdout=None, stderr=None):343 def patchStdoutAndStderr(self, stdout=None, stderr=None):
diff --git a/cloudinit/tests/test_netinfo.py b/cloudinit/tests/test_netinfo.py
index d76e768..1c8a791 100644
--- a/cloudinit/tests/test_netinfo.py
+++ b/cloudinit/tests/test_netinfo.py
@@ -11,6 +11,7 @@ from cloudinit.tests.helpers import CiTestCase, mock, readResource
11# Example ifconfig and route output11# Example ifconfig and route output
12SAMPLE_OLD_IFCONFIG_OUT = readResource("netinfo/old-ifconfig-output")12SAMPLE_OLD_IFCONFIG_OUT = readResource("netinfo/old-ifconfig-output")
13SAMPLE_NEW_IFCONFIG_OUT = readResource("netinfo/new-ifconfig-output")13SAMPLE_NEW_IFCONFIG_OUT = readResource("netinfo/new-ifconfig-output")
14SAMPLE_FREEBSD_IFCONFIG_OUT = readResource("netinfo/freebsd-ifconfig-output")
14SAMPLE_IPADDRSHOW_OUT = readResource("netinfo/sample-ipaddrshow-output")15SAMPLE_IPADDRSHOW_OUT = readResource("netinfo/sample-ipaddrshow-output")
15SAMPLE_ROUTE_OUT_V4 = readResource("netinfo/sample-route-output-v4")16SAMPLE_ROUTE_OUT_V4 = readResource("netinfo/sample-route-output-v4")
16SAMPLE_ROUTE_OUT_V6 = readResource("netinfo/sample-route-output-v6")17SAMPLE_ROUTE_OUT_V6 = readResource("netinfo/sample-route-output-v6")
@@ -18,6 +19,7 @@ SAMPLE_IPROUTE_OUT_V4 = readResource("netinfo/sample-iproute-output-v4")
18SAMPLE_IPROUTE_OUT_V6 = readResource("netinfo/sample-iproute-output-v6")19SAMPLE_IPROUTE_OUT_V6 = readResource("netinfo/sample-iproute-output-v6")
19NETDEV_FORMATTED_OUT = readResource("netinfo/netdev-formatted-output")20NETDEV_FORMATTED_OUT = readResource("netinfo/netdev-formatted-output")
20ROUTE_FORMATTED_OUT = readResource("netinfo/route-formatted-output")21ROUTE_FORMATTED_OUT = readResource("netinfo/route-formatted-output")
22FREEBSD_NETDEV_OUT = readResource("netinfo/freebsd-netdev-formatted-output")
2123
2224
23class TestNetInfo(CiTestCase):25class TestNetInfo(CiTestCase):
@@ -45,6 +47,18 @@ class TestNetInfo(CiTestCase):
4547
46 @mock.patch('cloudinit.netinfo.util.which')48 @mock.patch('cloudinit.netinfo.util.which')
47 @mock.patch('cloudinit.netinfo.util.subp')49 @mock.patch('cloudinit.netinfo.util.subp')
50 def test_netdev_freebsd_nettools_pformat(self, m_subp, m_which):
51 """netdev_pformat properly rendering netdev new nettools info."""
52 m_subp.return_value = (SAMPLE_FREEBSD_IFCONFIG_OUT, '')
53 m_which.side_effect = lambda x: x if x == 'ifconfig' else None
54 content = netdev_pformat()
55 print()
56 print(content)
57 print()
58 self.assertEqual(FREEBSD_NETDEV_OUT, content)
59
60 @mock.patch('cloudinit.netinfo.util.which')
61 @mock.patch('cloudinit.netinfo.util.subp')
48 def test_netdev_iproute_pformat(self, m_subp, m_which):62 def test_netdev_iproute_pformat(self, m_subp, m_which):
49 """netdev_pformat properly rendering ip route info."""63 """netdev_pformat properly rendering ip route info."""
50 m_subp.return_value = (SAMPLE_IPADDRSHOW_OUT, '')64 m_subp.return_value = (SAMPLE_IPADDRSHOW_OUT, '')
diff --git a/cloudinit/url_helper.py b/cloudinit/url_helper.py
index 396d69a..0af0d9e 100644
--- a/cloudinit/url_helper.py
+++ b/cloudinit/url_helper.py
@@ -521,7 +521,7 @@ class OauthUrlHelper(object):
521 if extra_exception_cb:521 if extra_exception_cb:
522 ret = extra_exception_cb(msg, exception)522 ret = extra_exception_cb(msg, exception)
523 finally:523 finally:
524 self.exception_cb(msg, exception)524 self.exception_cb(msg, exception)
525 return ret525 return ret
526526
527 def _headers_cb(self, extra_headers_cb, url):527 def _headers_cb(self, extra_headers_cb, url):
diff --git a/cloudinit/util.py b/cloudinit/util.py
index a8a232b..a192091 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -51,11 +51,6 @@ from cloudinit import version
5151
52from cloudinit.settings import (CFG_BUILTIN)52from cloudinit.settings import (CFG_BUILTIN)
5353
54try:
55 string_types = (basestring,)
56except NameError:
57 string_types = (str,)
58
59_DNS_REDIRECT_IP = None54_DNS_REDIRECT_IP = None
60LOG = logging.getLogger(__name__)55LOG = logging.getLogger(__name__)
6156
@@ -77,7 +72,6 @@ CONTAINER_TESTS = (['systemd-detect-virt', '--quiet', '--container'],
77PROC_CMDLINE = None72PROC_CMDLINE = None
7873
79_LSB_RELEASE = {}74_LSB_RELEASE = {}
80PY26 = sys.version_info[0:2] == (2, 6)
8175
8276
83def get_architecture(target=None):77def get_architecture(target=None):
@@ -125,7 +119,7 @@ def target_path(target, path=None):
125 # return 'path' inside target, accepting target as None119 # return 'path' inside target, accepting target as None
126 if target in (None, ""):120 if target in (None, ""):
127 target = "/"121 target = "/"
128 elif not isinstance(target, string_types):122 elif not isinstance(target, six.string_types):
129 raise ValueError("Unexpected input for target: %s" % target)123 raise ValueError("Unexpected input for target: %s" % target)
130 else:124 else:
131 target = os.path.abspath(target)125 target = os.path.abspath(target)
@@ -1596,14 +1590,17 @@ def json_dumps(data):
1596 separators=(',', ': '), default=json_serialize_default)1590 separators=(',', ': '), default=json_serialize_default)
15971591
15981592
1599def yaml_dumps(obj, explicit_start=True, explicit_end=True):1593def yaml_dumps(obj, explicit_start=True, explicit_end=True, noalias=False):
1600 """Return data in nicely formatted yaml."""1594 """Return data in nicely formatted yaml."""
1601 return yaml.safe_dump(obj,1595
1602 line_break="\n",1596 return yaml.dump(obj,
1603 indent=4,1597 line_break="\n",
1604 explicit_start=explicit_start,1598 indent=4,
1605 explicit_end=explicit_end,1599 explicit_start=explicit_start,
1606 default_flow_style=False)1600 explicit_end=explicit_end,
1601 default_flow_style=False,
1602 Dumper=(safeyaml.NoAliasSafeDumper
1603 if noalias else yaml.dumper.Dumper))
16071604
16081605
1609def ensure_dir(path, mode=None):1606def ensure_dir(path, mode=None):
@@ -2817,9 +2814,6 @@ def load_shell_content(content, add_empty=False, empty_val=None):
2817 variables. Set their value to empty_val."""2814 variables. Set their value to empty_val."""
28182815
2819 def _shlex_split(blob):2816 def _shlex_split(blob):
2820 if PY26 and isinstance(blob, six.text_type):
2821 # Older versions don't support unicode input
2822 blob = blob.encode("utf8")
2823 return shlex.split(blob, comments=True)2817 return shlex.split(blob, comments=True)
28242818
2825 data = {}2819 data = {}
diff --git a/debian/changelog b/debian/changelog
index 70f1879..61f09d7 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,47 @@
1cloud-init (18.5-44-g7c07af28-0ubuntu1) disco; urgency=medium
2
3 * New upstream snapshot.
4 - Support locking user with usermod if passwd is not available.
5 [Scott Moser]
6 - Example for Microsoft Azure data disk added. [Anton Olifir]
7 - clean: correctly determine the path for excluding seed directory
8 (LP: #1818571)
9 - helpers/openstack: Treat unknown link types as physical (LP: #1639263)
10 - drop Python 2.6 support and our NIH version detection
11 - tip-pylint: Fix assignment-from-return-none errors
12 - net: append type:dhcp[46] only if dhcp[46] is True in v2 netconfig
13 [Kurt Stieger] (LP: #1818032)
14 - cc_apt_pipelining: stop disabling pipelining by default (LP: #1794982)
15 - tests: fix some slow tests and some leaking state
16 - util: don't determine string_types ourselves
17 - cc_rsyslog: Escape possible nested set (LP: #1816967)
18 - Enable encrypted_data_bag_secret support for Chef
19 [Eric Williams] (LP: #1817082)
20 - azure: Filter list of ssh keys pulled from fabric [Jason Zions (MSFT)]
21 - doc: update merging doc with fixes and some additional details/examples
22 - tests: integration test failure summary to use traceback if empty error
23 - This is to fix https://bugs.launchpad.net/cloud-init/+bug/1812676
24 [Vitaly Kuznetsov]
25 - EC2: Rewrite network config on AWS Classic instances every boot
26 [Guilherme G. Piccoli] (LP: #1802073)
27 - netinfo: Adjust ifconfig output parsing for FreeBSD ipv6 entries
28 (LP: #1779672)
29 - netplan: Don't render yaml aliases when dumping netplan (LP: #1815051)
30 - add PyCharm IDE .idea/ path to .gitignore [Dominic Schlegel]
31 - correct grammar issue in instance metadata documentation
32 [Dominic Schlegel] (LP: #1802188)
33 - clean: cloud-init clean should not trace when run from within cloud_dir
34 (LP: #1795508)
35 - Resolve flake8 comparison and pycodestyle over-ident issues
36 [Paride Legovini]
37 * Update netplan dependency package (LP: #1813667)
38 * Fix build-depends-on-obsolete-package for dh-systemd
39 * Change Priority from extra to optional
40 * Override lintian warnings about WantedBy=cloud-init.target
41 * Change Maintainer to Ubuntu Developers
42
43 -- Daniel Watkins <oddbloke@ubuntu.com> Thu, 07 Mar 2019 10:32:26 -0500
44
1cloud-init (18.5-21-g8ee294d5-0ubuntu1) disco; urgency=medium45cloud-init (18.5-21-g8ee294d5-0ubuntu1) disco; urgency=medium
246
3 * New upstream snapshot.47 * New upstream snapshot.
diff --git a/debian/cloud-init.lintian-overrides b/debian/cloud-init.lintian-overrides
4new file mode 10064448new file mode 100644
index 0000000..58fac0d
--- /dev/null
+++ b/debian/cloud-init.lintian-overrides
@@ -0,0 +1,4 @@
1cloud-init binary: systemd-service-file-refers-to-unusual-wantedby-target lib/systemd/system/cloud-config.service cloud-init.target
2cloud-init binary: systemd-service-file-refers-to-unusual-wantedby-target lib/systemd/system/cloud-final.service cloud-init.target
3cloud-init binary: systemd-service-file-refers-to-unusual-wantedby-target lib/systemd/system/cloud-init-local.service cloud-init.target
4cloud-init binary: systemd-service-file-refers-to-unusual-wantedby-target lib/systemd/system/cloud-init.service cloud-init.target
diff --git a/debian/control b/debian/control
index 282304a..b1e2e8f 100644
--- a/debian/control
+++ b/debian/control
@@ -1,10 +1,9 @@
1Source: cloud-init1Source: cloud-init
2Section: admin2Section: admin
3Priority: extra3Priority: optional
4Maintainer: Scott Moser <smoser@ubuntu.com>4Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
5Build-Depends: debhelper (>= 9),5Build-Depends: debhelper (>= 9.20160709),
6 dh-python,6 dh-python,
7 dh-systemd,
8 iproute2,7 iproute2,
9 pep8,8 pep8,
10 po-debconf,9 po-debconf,
@@ -36,7 +35,7 @@ Architecture: all
36Depends: cloud-guest-utils | cloud-utils,35Depends: cloud-guest-utils | cloud-utils,
37 isc-dhcp-client,36 isc-dhcp-client,
38 iproute2,37 iproute2,
39 nplan | ifupdown,38 netplan.io | ifupdown,
40 procps,39 procps,
41 python3,40 python3,
42 python3-requests,41 python3-requests,
diff --git a/doc/examples/cloud-config-chef.txt b/doc/examples/cloud-config-chef.txt
index defc5a5..2320e01 100644
--- a/doc/examples/cloud-config-chef.txt
+++ b/doc/examples/cloud-config-chef.txt
@@ -98,6 +98,9 @@ chef:
98 # to the install script98 # to the install script
99 omnibus_version: "12.3.0"99 omnibus_version: "12.3.0"
100100
101 # If encrypted data bags are used, the client needs to have a secrets file
102 # configured to decrypt them
103 encrypted_data_bag_secret: "/etc/chef/encrypted_data_bag_secret"
101104
102# Capture all subprocess output into a logfile105# Capture all subprocess output into a logfile
103# Useful for troubleshooting cloud-init issues106# Useful for troubleshooting cloud-init issues
diff --git a/doc/examples/cloud-config-disk-setup.txt b/doc/examples/cloud-config-disk-setup.txt
index 43a62a2..89d9ff5 100644
--- a/doc/examples/cloud-config-disk-setup.txt
+++ b/doc/examples/cloud-config-disk-setup.txt
@@ -17,7 +17,7 @@ fs_setup:
17 device: ephemeral017 device: ephemeral0
18 partition: auto18 partition: auto
1919
20# Default disk definitions for Windows Azure20# Default disk definitions for Microsoft Azure
21# ------------------------------------------21# ------------------------------------------
2222
23device_aliases: {'ephemeral0': '/dev/sdb'}23device_aliases: {'ephemeral0': '/dev/sdb'}
@@ -34,6 +34,21 @@ fs_setup:
34 replace_fs: ntfs34 replace_fs: ntfs
3535
3636
37# Data disks definitions for Microsoft Azure
38# ------------------------------------------
39
40disk_setup:
41 /dev/disk/azure/scsi1/lun0:
42 table_type: gpt
43 layout: True
44 overwrite: True
45
46fs_setup:
47 - device: /dev/disk/azure/scsi1/lun0
48 partition: 1
49 filesystem: ext4
50
51
37# Default disk definitions for SmartOS52# Default disk definitions for SmartOS
38# ------------------------------------53# ------------------------------------
3954
@@ -242,7 +257,7 @@ fs_setup:
242#257#
243# "false": If an existing file system exists, skip the creation.258# "false": If an existing file system exists, skip the creation.
244#259#
245# <REPLACE_FS>: This is a special directive, used for Windows Azure that260# <REPLACE_FS>: This is a special directive, used for Microsoft Azure that
246# instructs cloud-init to replace a file system of <FS_TYPE>. NOTE:261# instructs cloud-init to replace a file system of <FS_TYPE>. NOTE:
247# unless you define a label, this requires the use of the 'any' partition262# unless you define a label, this requires the use of the 'any' partition
248# directive.263# directive.
diff --git a/doc/rtd/topics/datasources/ec2.rst b/doc/rtd/topics/datasources/ec2.rst
index 64c325d..76beca9 100644
--- a/doc/rtd/topics/datasources/ec2.rst
+++ b/doc/rtd/topics/datasources/ec2.rst
@@ -90,4 +90,15 @@ An example configuration with the default values is provided below:
90 max_wait: 12090 max_wait: 120
91 timeout: 5091 timeout: 50
9292
93Notes
94-----
95 * There are 2 types of EC2 instances network-wise: VPC ones (Virtual Private
96 Cloud) and Classic ones (also known as non-VPC). One major difference
97 between them is that Classic instances have their MAC address changed on
98 stop/restart operations, so cloud-init will recreate the network config
99 file for EC2 Classic instances every boot. On VPC instances this file is
100 generated only in the first boot of the instance.
101 The check for the instance type is performed by is_classic_instance()
102 method.
103
93.. vi: textwidth=78104.. vi: textwidth=78
diff --git a/doc/rtd/topics/instancedata.rst b/doc/rtd/topics/instancedata.rst
index 5d2dc94..231a008 100644
--- a/doc/rtd/topics/instancedata.rst
+++ b/doc/rtd/topics/instancedata.rst
@@ -4,7 +4,7 @@
4Instance Metadata4Instance Metadata
5*****************5*****************
66
7What is a instance data?7What is instance data?
8========================8========================
99
10Instance data is the collection of all configuration data that cloud-init10Instance data is the collection of all configuration data that cloud-init
diff --git a/doc/rtd/topics/merging.rst b/doc/rtd/topics/merging.rst
index c75ca59..5f7ca18 100644
--- a/doc/rtd/topics/merging.rst
+++ b/doc/rtd/topics/merging.rst
@@ -21,12 +21,12 @@ For example.
21.. code-block:: yaml21.. code-block:: yaml
2222
23 #cloud-config (1)23 #cloud-config (1)
24 run_cmd:24 runcmd:
25 - bash125 - bash1
26 - bash226 - bash2
2727
28 #cloud-config (2)28 #cloud-config (2)
29 run_cmd:29 runcmd:
30 - bash330 - bash3
31 - bash431 - bash4
3232
@@ -36,7 +36,7 @@ cloud-config object that contains the following.
36.. code-block:: yaml36.. code-block:: yaml
3737
38 #cloud-config (merged)38 #cloud-config (merged)
39 run_cmd:39 runcmd:
40 - bash340 - bash3
41 - bash441 - bash4
4242
@@ -45,7 +45,7 @@ Typically this is not what users want; instead they would likely prefer:
45.. code-block:: yaml45.. code-block:: yaml
4646
47 #cloud-config (merged)47 #cloud-config (merged)
48 run_cmd:48 runcmd:
49 - bash149 - bash1
50 - bash250 - bash2
51 - bash351 - bash3
@@ -55,6 +55,45 @@ This way makes it easier to combine the various cloud-config objects you have
55into a more useful list, thus reducing duplication necessary to accomplish the55into a more useful list, thus reducing duplication necessary to accomplish the
56same result with the previous method.56same result with the previous method.
5757
58
59Built-in Mergers
60================
61
62Cloud-init provides merging for the following built-in types:
63
64- Dict
65- List
66- String
67
68The ``Dict`` merger has the following options which control what is done with
69values contained within the config.
70
71- ``allow_delete``: Existing values not present in the new value can be deleted, defaults to False
72- ``no_replace``: Do not replace an existing value if one is already present, enabled by default.
73- ``replace``: Overwrite existing values with new ones.
74
75The ``List`` merger has the following options which control what is done with
76the values contained within the config.
77
78- ``append``: Add new value to the end of the list, defaults to False.
79- ``prepend``: Add new values to the start of the list, defaults to False.
80- ``no_replace``: Do not replace an existing value if one is already present, enabled by default.
81- ``replace``: Overwrite existing values with new ones.
82
83The ``Str`` merger has the following options which control what is done with
84the values contained within the config.
85
86- ``append``: Add new value to the end of the string, defaults to False.
87
88Common options for all merge types which control how recursive merging is
89done on other types.
90
91- ``recurse_dict``: If True merge the new values of the dictionary, defaults to True.
92- ``recurse_list``: If True merge the new values of the list, defaults to False.
93- ``recurse_array``: Alias for ``recurse_list``.
94- ``recurse_str``: If True merge the new values of the string, defaults to False.
95
96
58Customizability97Customizability
59===============98===============
6099
@@ -164,8 +203,8 @@ string format (i.e. the second option above), for example:
164203
165.. code-block:: python204.. code-block:: python
166205
167 {'merge_how': [{'name': 'list', 'settings': ['extend']},206 {'merge_how': [{'name': 'list', 'settings': ['append']},
168 {'name': 'dict', 'settings': []},207 {'name': 'dict', 'settings': ['no_replace', 'recurse_list']},
169 {'name': 'str', 'settings': ['append']}]}208 {'name': 'str', 'settings': ['append']}]}
170209
171This would be the equivalent format for default string format but in dictionary210This would be the equivalent format for default string format but in dictionary
@@ -201,4 +240,43 @@ Note, however, that merge algorithms are not used *across* types of
201configuration. As was the case before merging was implemented,240configuration. As was the case before merging was implemented,
202user-data will overwrite conf.d configuration without merging.241user-data will overwrite conf.d configuration without merging.
203242
243Example cloud-config
244====================
245
246A common request is to include multiple ``runcmd`` directives in different
247files and merge all of the commands together. To achieve this, we must modify
248the default merging to allow for dictionaries to join list values.
249
250
251The first config
252
253.. code-block:: yaml
254
255 #cloud-config
256 merge_how:
257 - name: list
258 settings: [append]
259 - name: dict
260 settings: [no_replace, recurse_list]
261
262 runcmd:
263 - bash1
264 - bash2
265
266The second config
267
268.. code-block:: yaml
269
270 #cloud-config
271 merge_how:
272 - name: list
273 settings: [append]
274 - name: dict
275 settings: [no_replace, recurse_list]
276
277 runcmd:
278 - bash3
279 - bash4
280
281
204.. vi: textwidth=78282.. vi: textwidth=78
diff --git a/templates/chef_client.rb.tmpl b/templates/chef_client.rb.tmpl
index cbb6b15..99978d3 100644
--- a/templates/chef_client.rb.tmpl
+++ b/templates/chef_client.rb.tmpl
@@ -1,6 +1,6 @@
1## template:jinja1## template:jinja
2{#2{#
3This file is only utilized if the module 'cc_chef' is enabled in 3This file is only utilized if the module 'cc_chef' is enabled in
4cloud-config. Specifically, in order to enable it4cloud-config. Specifically, in order to enable it
5you need to add the following to config:5you need to add the following to config:
6 chef:6 chef:
@@ -56,3 +56,6 @@ pid_file "{{pid_file}}"
56{% if show_time %}56{% if show_time %}
57Chef::Log::Formatter.show_time = true57Chef::Log::Formatter.show_time = true
58{% endif %}58{% endif %}
59{% if encrypted_data_bag_secret %}
60encrypted_data_bag_secret "{{encrypted_data_bag_secret}}"
61{% endif %}
diff --git a/tests/cloud_tests/verify.py b/tests/cloud_tests/verify.py
index 9911ecf..7018f4d 100644
--- a/tests/cloud_tests/verify.py
+++ b/tests/cloud_tests/verify.py
@@ -61,12 +61,17 @@ def format_test_failures(test_result):
61 if not test_result['failures']:61 if not test_result['failures']:
62 return ''62 return ''
63 failure_hdr = ' test failures:'63 failure_hdr = ' test failures:'
64 failure_fmt = ' * {module}.{class}.{function}\n {error}'64 failure_fmt = ' * {module}.{class}.{function}\n '
65 output = []65 output = []
66 for failure in test_result['failures']:66 for failure in test_result['failures']:
67 if not output:67 if not output:
68 output = [failure_hdr]68 output = [failure_hdr]
69 output.append(failure_fmt.format(**failure))69 msg = failure_fmt.format(**failure)
70 if failure.get('error'):
71 msg += failure['error']
72 else:
73 msg += failure.get('traceback', '')
74 output.append(msg)
70 return '\n'.join(output)75 return '\n'.join(output)
7176
7277
diff --git a/tests/data/azure/parse_certificates_fingerprints b/tests/data/azure/parse_certificates_fingerprints
73new file mode 10064478new file mode 100644
index 0000000..f7293c5
--- /dev/null
+++ b/tests/data/azure/parse_certificates_fingerprints
@@ -0,0 +1,4 @@
1ECEDEB3B8488D31AF3BC4CCED493F64B7D27D7B1
2073E19D14D1C799224C6A0FD8DDAB6A8BF27D473
34C16E7FAD6297D74A9B25EB8F0A12808CEBE293E
4929130695289B450FE45DCD5F6EF0CDE69865867
diff --git a/tests/data/azure/parse_certificates_pem b/tests/data/azure/parse_certificates_pem
0new file mode 1006445new file mode 100644
index 0000000..3521ea3
--- /dev/null
+++ b/tests/data/azure/parse_certificates_pem
@@ -0,0 +1,152 @@
1Bag Attributes
2 localKeyID: 01 00 00 00
3 Microsoft CSP Name: Microsoft Enhanced Cryptographic Provider v1.0
4Key Attributes
5 X509v3 Key Usage: 10
6-----BEGIN PRIVATE KEY-----
7MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDlEe5fUqwdrQTP
8W2oVlGK2f31q/8ULT8KmOTyUvL0RPdJQ69vvHOc5Q2CKg2eviHC2LWhF8WmpnZj6
961RL0GeFGizwvU8Moebw5p3oqdcgoGpHVtxf+mr4QcWF58/Fwez0dA4hcsimVNBz
10eNpBBUIKNBMTBG+4d6hcQBUAGKUdGRcCGEyTqXLU0MgHjxC9JgVqWJl+X2LcAGj5
117J+tGYGTLzKJmeCeGVNN5ZtJ0T85MYHCKQk1/FElK+Kq5akovXffQHjlnCPcx0NJ
1247NBjlPaFp2gjnAChn79bT4iCjOFZ9avWpqRpeU517UCnY7djOr3fuod/MSQyh3L
13Wuem1tWBAgMBAAECggEBAM4ZXQRs6Kjmo95BHGiAEnSqrlgX+dycjcBq3QPh8KZT
14nifqnf48XhnackENy7tWIjr3DctoUq4mOp8AHt77ijhqfaa4XSg7fwKeK9NLBGC5
15lAXNtAey0o2894/sKrd+LMkgphoYIUnuI4LRaGV56potkj/ZDP/GwTcG/R4SDnTn
16C1Nb05PNTAPQtPZrgPo7TdM6gGsTnFbVrYHQLyg2Sq/osHfF15YohB01esRLCAwb
17EF8JkRC4hWIZoV7BsyQ39232zAJQGGla7+wKFs3kObwh3VnFkQpT94KZnNiZuEfG
18x5pW4Pn3gXgNsftscXsaNe/M9mYZqo//Qw7NvUIvAvECgYEA9AVveyK0HOA06fhh
19+3hUWdvw7Pbrl+e06jO9+bT1RjQMbHKyI60DZyVGuAySN86iChJRoJr5c6xj+iXU
20cR6BVJDjGH5t1tyiK2aYf6hEpK9/j8Z54UiVQ486zPP0PGfT2TO4lBLK+8AUmoaH
21gk21ul8QeVCeCJa/o+xEoRFvzcUCgYEA8FCbbvInrUtNY+9eKaUYoNodsgBVjm5X
22I0YPUL9D4d+1nvupHSV2NVmQl0w1RaJwrNTafrl5LkqjhQbmuWNta6QgfZzSA3LB
23lWXo1Mm0azKdcD3qMGbvn0Q3zU+yGNEgmB/Yju3/NtgYRG6tc+FCWRbPbiCnZWT8
24v3C2Y0XggI0CgYEA2/jCZBgGkTkzue5kNVJlh5OS/aog+pCvL6hxCtarfBuTT3ed
25Sje+p46cz3DVpmUpATc+Si8py7KNdYQAm/BJ2be6X+woi9Xcgo87zWgcaPCjZzId
260I2jsIE/Gl6XvpRCDrxnGWRPgt3GNP4szbPLrDPiH9oie8+Y9eYYf7G+PZkCgYEA
27nRSzZOPYV4f/QDF4pVQLMykfe/iH9B/fyWjEHg3He19VQmRReIHCMMEoqBziPXAe
28onpHj8oAkeer1wpZyhhZr6CKtFDLXgGm09bXSC/IRMHC81klORovyzU2HHfZfCtG
29WOmIDnU2+0xpIGIP8sztJ3qnf97MTJSkOSadsWo9gwkCgYEAh5AQmJQmck88Dff2
30qIfJIX8d+BDw47BFJ89OmMFjGV8TNB+JO+AV4Vkodg4hxKpLqTFZTTUFgoYfy5u1
311/BhAjpmCDCrzubCFhx+8VEoM2+2+MmnuQoMAm9+/mD/IidwRaARgXgvEmp7sfdt
32RyWd+p2lYvFkC/jORQtDMY4uW1o=
33-----END PRIVATE KEY-----
34Bag Attributes
35 localKeyID: 02 00 00 00
36 Microsoft CSP Name: Microsoft Strong Cryptographic Provider
37Key Attributes
38 X509v3 Key Usage: 10
39-----BEGIN PRIVATE KEY-----
40MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDlQhPrZwVQYFV4
41FBc0H1iTXYaznMpwZvEITKtXWACzTdguUderEVOkXW3HTi5HvC2rMayt0nqo3zcd
42x1eGiqdjpZQ/wMrkz9wNEM/nNMsXntEwxk0jCVNKB/jz6vf+BOtrSI01SritAGZW
43dpKoTUyztT8C2mA3X6D8g3m4Dd07ltnzxaDqAQIU5jBHh3f/Q14tlPNZWUIiqVTC
44gDxgAe7MDmfs9h3CInTBX1XM5J4UsLTL23/padgeSvP5YF5qr1+0c7Tdftxr2lwA
45N3rLkisf5EiLAToVyJJlgP/exo2I8DaIKe7DZzD3Y1CrurOpkcMKYu5kM1Htlbua
46tDkAa2oDAgMBAAECggEAOvdueS9DyiMlCKAeQb1IQosdQOh0l0ma+FgEABC2CWhd
470LgjQTBRM6cGO+urcq7/jhdWQ1UuUG4tVn71z7itCi/F/Enhxc2C22d2GhFVpWsn
48giSXJYpZ/mIjkdVfWNo6FRuRmmHwMys1p0qTOS+8qUJWhSzW75csqJZGgeUrAI61
49LBV5F0SGR7dR2xZfy7PeDs9xpD0QivDt5DpsZWPaPvw4QlhdLgw6/YU1h9vtm6ci
50xLjnPRLZ7JMpcQHO8dUDl6FiEI7yQ11BDm253VQAVMddYRPQABn7SpEF8kD/aZVh
512Clvz61Rz80SKjPUthMPLWMCRp7zB0xDMzt3/1i+tQKBgQD6Ar1/oD3eFnRnpi4u
52n/hdHJtMuXWNfUA4dspNjP6WGOid9sgIeUUdif1XyVJ+afITzvgpWc7nUWIqG2bQ
53WxJ/4q2rjUdvjNXTy1voVungR2jD5WLQ9DKeaTR0yCliWlx4JgdPG7qGI5MMwsr+
54R/PUoUUhGeEX+o/sCSieO3iUrQKBgQDqwBEMvIdhAv/CK2sG3fsKYX8rFT55ZNX3
55Tix9DbUGY3wQColNuI8U1nDlxE9U6VOfT9RPqKelBLCgbzB23kdEJnjSlnqlTxrx
56E+Hkndyf2ckdJAR3XNxoQ6SRLJNBsgoBj/z5tlfZE9/Jc+uh0mYy3e6g6XCVPBcz
57MgoIc+ofbwKBgQCGQhZ1hR30N+bHCozeaPW9OvGDIE0qcEqeh9xYDRFilXnF6pK9
58SjJ9jG7KR8jPLiHb1VebDSl5O1EV/6UU2vNyTc6pw7LLCryBgkGW4aWy1WZDXNnW
59EG1meGS9GghvUss5kmJ2bxOZmV0Mi0brisQ8OWagQf+JGvtS7BAt+Q3l+QKBgAb9
608YQPmXiqPjPqVyW9Ntz4SnFeEJ5NApJ7IZgX8GxgSjGwHqbR+HEGchZl4ncE/Bii
61qBA3Vcb0fM5KgYcI19aPzsl28fA6ivLjRLcqfIfGVNcpW3iyq13vpdctHLW4N9QU
62FdTaOYOds+ysJziKq8CYG6NvUIshXw+HTgUybqbBAoGBAIIOqcmmtgOClAwipA17
63dAHsI9Sjk+J0+d4JU6o+5TsmhUfUKIjXf5+xqJkJcQZMEe5GhxcCuYkgFicvh4Hz
64kv2H/EU35LcJTqC6KTKZOWIbGcn1cqsvwm3GQJffYDiO8fRZSwCaif2J3F2lfH4Y
65R/fA67HXFSTT+OncdRpY1NOn
66-----END PRIVATE KEY-----
67Bag Attributes: <Empty Attributes>
68subject=/CN=CRP/OU=AzureRT/O=Microsoft Corporation/L=Redmond/ST=WA/C=US
69issuer=/CN=Root Agency
70-----BEGIN CERTIFICATE-----
71MIIB+TCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtSb290
72IEFnZW5jeTAeFw0xOTAyMTUxOTA0MDRaFw0yOTAyMTUxOTE0MDRaMGwxDDAKBgNV
73BAMMA0NSUDEQMA4GA1UECwwHQXp1cmVSVDEeMBwGA1UECgwVTWljcm9zb2Z0IENv
74cnBvcmF0aW9uMRAwDgYDVQQHDAdSZWRtb25kMQswCQYDVQQIDAJXQTELMAkGA1UE
75BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIlPjJXzrRih4C
76k/XsoI01oqo7IUxH3dA2F7vHGXQoIpKCp8Qe6Z6cFfdD8Uj+s+B1BX6hngwzIwjN
77jE/23X3SALVzJVWzX4Y/IEjbgsuao6sOyNyB18wIU9YzZkVGj68fmMlUw3LnhPbe
78eWkufZaJCaLyhQOwlRMbOcn48D6Ys8fccOyXNzpq3rH1OzeQpxS2M8zaJYP4/VZ/
79sf6KRpI7bP+QwyFvNKfhcaO9/gj4kMo9lVGjvDU20FW6g8UVNJCV9N4GO6mOcyqo
80OhuhVfjCNGgW7N1qi0TIVn0/MQM4l4dcT2R7Z/bV9fhMJLjGsy5A4TLAdRrhKUHT
81bzi9HyDvAgMBAAEwDQYJKoZIhvcNAQEFBQADAQA=
82-----END CERTIFICATE-----
83Bag Attributes
84 localKeyID: 01 00 00 00
85subject=/C=US/ST=WASHINGTON/L=Seattle/O=Microsoft/OU=Azure/CN=AnhVo/emailAddress=redacted@microsoft.com
86issuer=/C=US/ST=WASHINGTON/L=Seattle/O=Microsoft/OU=Azure/CN=AnhVo/emailAddress=redacted@microsoft.com
87-----BEGIN CERTIFICATE-----
88MIID7TCCAtWgAwIBAgIJALQS3yMg3R41MA0GCSqGSIb3DQEBCwUAMIGMMQswCQYD
89VQQGEwJVUzETMBEGA1UECAwKV0FTSElOR1RPTjEQMA4GA1UEBwwHU2VhdHRsZTES
90MBAGA1UECgwJTWljcm9zb2Z0MQ4wDAYDVQQLDAVBenVyZTEOMAwGA1UEAwwFQW5o
91Vm8xIjAgBgkqhkiG9w0BCQEWE2FuaHZvQG1pY3Jvc29mdC5jb20wHhcNMTkwMjE0
92MjMxMjQwWhcNMjExMTEwMjMxMjQwWjCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgM
93CldBU0hJTkdUT04xEDAOBgNVBAcMB1NlYXR0bGUxEjAQBgNVBAoMCU1pY3Jvc29m
94dDEOMAwGA1UECwwFQXp1cmUxDjAMBgNVBAMMBUFuaFZvMSIwIAYJKoZIhvcNAQkB
95FhNhbmh2b0BtaWNyb3NvZnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
96CgKCAQEA5RHuX1KsHa0Ez1tqFZRitn99av/FC0/Cpjk8lLy9ET3SUOvb7xznOUNg
97ioNnr4hwti1oRfFpqZ2Y+utUS9BnhRos8L1PDKHm8Oad6KnXIKBqR1bcX/pq+EHF
98hefPxcHs9HQOIXLIplTQc3jaQQVCCjQTEwRvuHeoXEAVABilHRkXAhhMk6ly1NDI
99B48QvSYFaliZfl9i3ABo+eyfrRmBky8yiZngnhlTTeWbSdE/OTGBwikJNfxRJSvi
100quWpKL1330B45Zwj3MdDSeOzQY5T2hadoI5wAoZ+/W0+IgozhWfWr1qakaXlOde1
101Ap2O3Yzq937qHfzEkMody1rnptbVgQIDAQABo1AwTjAdBgNVHQ4EFgQUPvdgLiv3
102pAk4r0QTPZU3PFOZJvgwHwYDVR0jBBgwFoAUPvdgLiv3pAk4r0QTPZU3PFOZJvgw
103DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAVUHZT+h9+uCPLTEl5IDg
104kqd9WpzXA7PJd/V+7DeDDTkEd06FIKTWZLfxLVVDjQJnQqubQb//e0zGu1qKbXnX
105R7xqWabGU4eyPeUFWddmt1OHhxKLU3HbJNJJdL6XKiQtpGGUQt/mqNQ/DEr6hhNF
106im5I79iA8H/dXA2gyZrj5Rxea4mtsaYO0mfp1NrFtJpAh2Djy4B1lBXBIv4DWG9e
107mMEwzcLCOZj2cOMA6+mdLMUjYCvIRtnn5MKUHyZX5EmX79wsqMTvVpddlVLB9Kgz
108Qnvft9+SBWh9+F3ip7BsL6Q4Q9v8eHRbnP0ya7ddlgh64uwf9VOfZZdKCnwqudJP
1093g==
110-----END CERTIFICATE-----
111Bag Attributes
112 localKeyID: 02 00 00 00
113subject=/CN=/subscriptions/redacted/resourcegroups/redacted/providers/Microsoft.Compute/virtualMachines/redacted
114issuer=/CN=Microsoft.ManagedIdentity
115-----BEGIN CERTIFICATE-----
116MIIDnTCCAoWgAwIBAgIUB2lauSRccvFkoJybUfIwOUqBN7MwDQYJKoZIhvcNAQEL
117BQAwJDEiMCAGA1UEAxMZTWljcm9zb2Z0Lk1hbmFnZWRJZGVudGl0eTAeFw0xOTAy
118MTUxOTA5MDBaFw0xOTA4MTQxOTA5MDBaMIGUMYGRMIGOBgNVBAMTgYYvc3Vic2Ny
119aXB0aW9ucy8yN2I3NTBjZC1lZDQzLTQyZmQtOTA0NC04ZDc1ZTEyNGFlNTUvcmVz
120b3VyY2Vncm91cHMvYW5oZXh0cmFzc2gvcHJvdmlkZXJzL01pY3Jvc29mdC5Db21w
121dXRlL3ZpcnR1YWxNYWNoaW5lcy9hbmh0ZXN0Y2VydDCCASIwDQYJKoZIhvcNAQEB
122BQADggEPADCCAQoCggEBAOVCE+tnBVBgVXgUFzQfWJNdhrOcynBm8QhMq1dYALNN
1232C5R16sRU6RdbcdOLke8LasxrK3SeqjfNx3HV4aKp2OllD/AyuTP3A0Qz+c0yxee
1240TDGTSMJU0oH+PPq9/4E62tIjTVKuK0AZlZ2kqhNTLO1PwLaYDdfoPyDebgN3TuW
1252fPFoOoBAhTmMEeHd/9DXi2U81lZQiKpVMKAPGAB7swOZ+z2HcIidMFfVczknhSw
126tMvbf+lp2B5K8/lgXmqvX7RztN1+3GvaXAA3esuSKx/kSIsBOhXIkmWA/97GjYjw
127Nogp7sNnMPdjUKu6s6mRwwpi7mQzUe2Vu5q0OQBragMCAwEAAaNWMFQwDgYDVR0P
128AQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYD
129VR0jBBgwFoAUOJvzEsriQWdJBndPrK+Me1bCPjYwDQYJKoZIhvcNAQELBQADggEB
130AFGP/g8o7Hv/to11M0UqfzJuW/AyH9RZtSRcNQFLZUndwweQ6fap8lFsA4REUdqe
1317Quqp5JNNY1XzKLWXMPoheIDH1A8FFXdsAroArzlNs9tO3TlIHE8A7HxEVZEmR4b
1327ZiixmkQPS2RkjEoV/GM6fheBrzuFn7X5kVZyE6cC5sfcebn8xhk3ZcXI0VmpdT0
133jFBsf5IvFCIXXLLhJI4KXc8VMoKFU1jT9na/jyaoGmfwovKj4ib8s2aiXGAp7Y38
134UCmY+bJapWom6Piy5Jzi/p/kzMVdJcSa+GqpuFxBoQYEVs2XYVl7cGu/wPM+NToC
135pkSoWwF1QAnHn0eokR9E1rU=
136-----END CERTIFICATE-----
137Bag Attributes: <Empty Attributes>
138subject=/CN=CRP/OU=AzureRT/O=Microsoft Corporation/L=Redmond/ST=WA/C=US
139issuer=/CN=Root Agency
140-----BEGIN CERTIFICATE-----
141MIIB+TCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtSb290
142IEFnZW5jeTAeFw0xOTAyMTUxOTA0MDRaFw0yOTAyMTUxOTE0MDRaMGwxDDAKBgNV
143BAMMA0NSUDEQMA4GA1UECwwHQXp1cmVSVDEeMBwGA1UECgwVTWljcm9zb2Z0IENv
144cnBvcmF0aW9uMRAwDgYDVQQHDAdSZWRtb25kMQswCQYDVQQIDAJXQTELMAkGA1UE
145BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHU9IDclbKVYVb
146Yuv0+zViX+wTwlKspslmy/uf3hkWLh7pyzyrq70S7qtSW2EGixUPxZS/R8pOLHoi
147nlKF9ILgj0gVTCJsSwnWpXRg3rhZwIVoYMHN50BHS1SqVD0lsWNMXmo76LoJcjmW
148vwIznvj5C/gnhU+K7+c3m7AlCyU2wjwpBAEYj7PQs6l/wTqpEiaqC5NytNBd7qp+
149lYYysVrpa1PFL0Nj4MMZARIfjkiJtL9qDhy9YZeJRQ6q/Fhz0kjvkZnfxixfKF4y
150WzOfhBrAtpF6oOnuYKk3hxjh9KjTTX4/U8zdLojalX09iyHyEjwJKGlGEpzh1aY7
151t5btUyvpAgMBAAEwDQYJKoZIhvcNAQEFBQADAQA=
152-----END CERTIFICATE-----
diff --git a/tests/data/azure/pubkey_extract_cert b/tests/data/azure/pubkey_extract_cert
0new file mode 100644153new file mode 100644
index 0000000..ce9b852
--- /dev/null
+++ b/tests/data/azure/pubkey_extract_cert
@@ -0,0 +1,13 @@
1-----BEGIN CERTIFICATE-----
2MIIB+TCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtSb290
3IEFnZW5jeTAeFw0xOTAyMTUxOTA0MDRaFw0yOTAyMTUxOTE0MDRaMGwxDDAKBgNV
4BAMMA0NSUDEQMA4GA1UECwwHQXp1cmVSVDEeMBwGA1UECgwVTWljcm9zb2Z0IENv
5cnBvcmF0aW9uMRAwDgYDVQQHDAdSZWRtb25kMQswCQYDVQQIDAJXQTELMAkGA1UE
6BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHU9IDclbKVYVb
7Yuv0+zViX+wTwlKspslmy/uf3hkWLh7pyzyrq70S7qtSW2EGixUPxZS/R8pOLHoi
8nlKF9ILgj0gVTCJsSwnWpXRg3rhZwIVoYMHN50BHS1SqVD0lsWNMXmo76LoJcjmW
9vwIznvj5C/gnhU+K7+c3m7AlCyU2wjwpBAEYj7PQs6l/wTqpEiaqC5NytNBd7qp+
10lYYysVrpa1PFL0Nj4MMZARIfjkiJtL9qDhy9YZeJRQ6q/Fhz0kjvkZnfxixfKF4y
11WzOfhBrAtpF6oOnuYKk3hxjh9KjTTX4/U8zdLojalX09iyHyEjwJKGlGEpzh1aY7
12t5btUyvpAgMBAAEwDQYJKoZIhvcNAQEFBQADAQA=
13-----END CERTIFICATE-----
diff --git a/tests/data/azure/pubkey_extract_ssh_key b/tests/data/azure/pubkey_extract_ssh_key
0new file mode 10064414new file mode 100644
index 0000000..54d749e
--- /dev/null
+++ b/tests/data/azure/pubkey_extract_ssh_key
@@ -0,0 +1 @@
1ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHU9IDclbKVYVbYuv0+zViX+wTwlKspslmy/uf3hkWLh7pyzyrq70S7qtSW2EGixUPxZS/R8pOLHoinlKF9ILgj0gVTCJsSwnWpXRg3rhZwIVoYMHN50BHS1SqVD0lsWNMXmo76LoJcjmWvwIznvj5C/gnhU+K7+c3m7AlCyU2wjwpBAEYj7PQs6l/wTqpEiaqC5NytNBd7qp+lYYysVrpa1PFL0Nj4MMZARIfjkiJtL9qDhy9YZeJRQ6q/Fhz0kjvkZnfxixfKF4yWzOfhBrAtpF6oOnuYKk3hxjh9KjTTX4/U8zdLojalX09iyHyEjwJKGlGEpzh1aY7t5btUyvp
diff --git a/tests/data/netinfo/freebsd-ifconfig-output b/tests/data/netinfo/freebsd-ifconfig-output
0new file mode 1006442new file mode 100644
index 0000000..3de15a5
--- /dev/null
+++ b/tests/data/netinfo/freebsd-ifconfig-output
@@ -0,0 +1,17 @@
1vtnet0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
2 options=6c07bb<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,JUMBO_MTU,VLAN_HWCSUM,TSO4,TSO6,LRO,VLAN_HWTSO,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
3 ether fa:16:3e:14:1f:99
4 hwaddr fa:16:3e:14:1f:99
5 inet 10.1.80.61 netmask 0xfffff000 broadcast 10.1.95.255
6 nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
7 media: Ethernet 10Gbase-T <full-duplex>
8 status: active
9pflog0: flags=0<> metric 0 mtu 33160
10pfsync0: flags=0<> metric 0 mtu 1500
11 syncpeer: 0.0.0.0 maxupd: 128 defer: off
12lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
13 options=600003<RXCSUM,TXCSUM,RXCSUM_IPV6,TXCSUM_IPV6>
14 inet6 ::1 prefixlen 128
15 inet6 fe80::1%lo0 prefixlen 64 scopeid 0x4
16 inet 127.0.0.1 netmask 0xff000000
17 nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
diff --git a/tests/data/netinfo/freebsd-netdev-formatted-output b/tests/data/netinfo/freebsd-netdev-formatted-output
0new file mode 10064418new file mode 100644
index 0000000..a9d2ac1
--- /dev/null
+++ b/tests/data/netinfo/freebsd-netdev-formatted-output
@@ -0,0 +1,11 @@
1+++++++++++++++++++++++++++++++Net device info+++++++++++++++++++++++++++++++
2+---------+-------+----------------+------------+-------+-------------------+
3| Device | Up | Address | Mask | Scope | Hw-Address |
4+---------+-------+----------------+------------+-------+-------------------+
5| lo0 | True | 127.0.0.1 | 0xff000000 | . | . |
6| lo0 | True | ::1/128 | . | . | . |
7| lo0 | True | fe80::1%lo0/64 | . | 0x4 | . |
8| pflog0 | False | . | . | . | . |
9| pfsync0 | False | . | . | . | . |
10| vtnet0 | True | 10.1.80.61 | 0xfffff000 | . | fa:16:3e:14:1f:99 |
11+---------+-------+----------------+------------+-------+-------------------+
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index 417d86a..6b05b8f 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -11,7 +11,7 @@ from cloudinit.util import (b64e, decode_binary, load_file, write_file,
11from cloudinit.version import version_string as vs11from cloudinit.version import version_string as vs
12from cloudinit.tests.helpers import (12from cloudinit.tests.helpers import (
13 HttprettyTestCase, CiTestCase, populate_dir, mock, wrap_and_call,13 HttprettyTestCase, CiTestCase, populate_dir, mock, wrap_and_call,
14 ExitStack, PY26, SkipTest)14 ExitStack)
1515
16import crypt16import crypt
17import httpretty17import httpretty
@@ -221,8 +221,6 @@ class TestAzureDataSource(CiTestCase):
221221
222 def setUp(self):222 def setUp(self):
223 super(TestAzureDataSource, self).setUp()223 super(TestAzureDataSource, self).setUp()
224 if PY26:
225 raise SkipTest("Does not work on python 2.6")
226 self.tmp = self.tmp_dir()224 self.tmp = self.tmp_dir()
227225
228 # patch cloud_dir, so our 'seed_dir' is guaranteed empty226 # patch cloud_dir, so our 'seed_dir' is guaranteed empty
@@ -1692,6 +1690,7 @@ class TestPreprovisioningPollIMDS(CiTestCase):
1692 self.paths = helpers.Paths({'cloud_dir': self.tmp})1690 self.paths = helpers.Paths({'cloud_dir': self.tmp})
1693 dsaz.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d1691 dsaz.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d
16941692
1693 @mock.patch('time.sleep', mock.MagicMock())
1695 @mock.patch(MOCKPATH + 'EphemeralDHCPv4')1694 @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
1696 def test_poll_imds_re_dhcp_on_timeout(self, m_dhcpv4, report_ready_func,1695 def test_poll_imds_re_dhcp_on_timeout(self, m_dhcpv4, report_ready_func,
1697 fake_resp, m_media_switch, m_dhcp,1696 fake_resp, m_media_switch, m_dhcp,
diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py
index 26b2b93..0255616 100644
--- a/tests/unittests/test_datasource/test_azure_helper.py
+++ b/tests/unittests/test_datasource/test_azure_helper.py
@@ -1,11 +1,13 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3import os3import os
4import unittest2
4from textwrap import dedent5from textwrap import dedent
56
6from cloudinit.sources.helpers import azure as azure_helper7from cloudinit.sources.helpers import azure as azure_helper
7from cloudinit.tests.helpers import CiTestCase, ExitStack, mock, populate_dir8from cloudinit.tests.helpers import CiTestCase, ExitStack, mock, populate_dir
89
10from cloudinit.util import load_file
9from cloudinit.sources.helpers.azure import WALinuxAgentShim as wa_shim11from cloudinit.sources.helpers.azure import WALinuxAgentShim as wa_shim
1012
11GOAL_STATE_TEMPLATE = """\13GOAL_STATE_TEMPLATE = """\
@@ -289,6 +291,50 @@ class TestOpenSSLManager(CiTestCase):
289 self.assertEqual([mock.call(manager.tmpdir)], del_dir.call_args_list)291 self.assertEqual([mock.call(manager.tmpdir)], del_dir.call_args_list)
290292
291293
294class TestOpenSSLManagerActions(CiTestCase):
295
296 def setUp(self):
297 super(TestOpenSSLManagerActions, self).setUp()
298
299 self.allowed_subp = True
300
301 def _data_file(self, name):
302 path = 'tests/data/azure'
303 return os.path.join(path, name)
304
305 @unittest2.skip("todo move to cloud_test")
306 def test_pubkey_extract(self):
307 cert = load_file(self._data_file('pubkey_extract_cert'))
308 good_key = load_file(self._data_file('pubkey_extract_ssh_key'))
309 sslmgr = azure_helper.OpenSSLManager()
310 key = sslmgr._get_ssh_key_from_cert(cert)
311 self.assertEqual(good_key, key)
312
313 good_fingerprint = '073E19D14D1C799224C6A0FD8DDAB6A8BF27D473'
314 fingerprint = sslmgr._get_fingerprint_from_cert(cert)
315 self.assertEqual(good_fingerprint, fingerprint)
316
317 @unittest2.skip("todo move to cloud_test")
318 @mock.patch.object(azure_helper.OpenSSLManager, '_decrypt_certs_from_xml')
319 def test_parse_certificates(self, mock_decrypt_certs):
320 """Azure control plane puts private keys as well as certificates
321 into the Certificates XML object. Make sure only the public keys
322 from certs are extracted and that fingerprints are converted to
323 the form specified in the ovf-env.xml file.
324 """
325 cert_contents = load_file(self._data_file('parse_certificates_pem'))
326 fingerprints = load_file(self._data_file(
327 'parse_certificates_fingerprints')
328 ).splitlines()
329 mock_decrypt_certs.return_value = cert_contents
330 sslmgr = azure_helper.OpenSSLManager()
331 keys_by_fp = sslmgr.parse_certificates('')
332 for fp in keys_by_fp.keys():
333 self.assertIn(fp, fingerprints)
334 for fp in fingerprints:
335 self.assertIn(fp, keys_by_fp)
336
337
292class TestWALinuxAgentShim(CiTestCase):338class TestWALinuxAgentShim(CiTestCase):
293339
294 def setUp(self):340 def setUp(self):
@@ -329,18 +375,31 @@ class TestWALinuxAgentShim(CiTestCase):
329375
330 def test_certificates_used_to_determine_public_keys(self):376 def test_certificates_used_to_determine_public_keys(self):
331 shim = wa_shim()377 shim = wa_shim()
332 data = shim.register_with_azure_and_fetch_data()378 """if register_with_azure_and_fetch_data() isn't passed some info about
379 the user's public keys, there's no point in even trying to parse
380 the certificates
381 """
382 mypk = [{'fingerprint': 'fp1', 'path': 'path1'},
383 {'fingerprint': 'fp3', 'path': 'path3', 'value': ''}]
384 certs = {'fp1': 'expected-key',
385 'fp2': 'should-not-be-found',
386 'fp3': 'expected-no-value-key',
387 }
388 sslmgr = self.OpenSSLManager.return_value
389 sslmgr.parse_certificates.return_value = certs
390 data = shim.register_with_azure_and_fetch_data(pubkey_info=mypk)
333 self.assertEqual(391 self.assertEqual(
334 [mock.call(self.GoalState.return_value.certificates_xml)],392 [mock.call(self.GoalState.return_value.certificates_xml)],
335 self.OpenSSLManager.return_value.parse_certificates.call_args_list)393 sslmgr.parse_certificates.call_args_list)
336 self.assertEqual(394 self.assertIn('expected-key', data['public-keys'])
337 self.OpenSSLManager.return_value.parse_certificates.return_value,395 self.assertIn('expected-no-value-key', data['public-keys'])
338 data['public-keys'])396 self.assertNotIn('should-not-be-found', data['public-keys'])
339397
340 def test_absent_certificates_produces_empty_public_keys(self):398 def test_absent_certificates_produces_empty_public_keys(self):
399 mypk = [{'fingerprint': 'fp1', 'path': 'path1'}]
341 self.GoalState.return_value.certificates_xml = None400 self.GoalState.return_value.certificates_xml = None
342 shim = wa_shim()401 shim = wa_shim()
343 data = shim.register_with_azure_and_fetch_data()402 data = shim.register_with_azure_and_fetch_data(pubkey_info=mypk)
344 self.assertEqual([], data['public-keys'])403 self.assertEqual([], data['public-keys'])
345404
346 def test_correct_url_used_for_report_ready(self):405 def test_correct_url_used_for_report_ready(self):
diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py
index dcdabea..520c50f 100644
--- a/tests/unittests/test_datasource/test_configdrive.py
+++ b/tests/unittests/test_datasource/test_configdrive.py
@@ -268,8 +268,7 @@ class TestConfigDriveDataSource(CiTestCase):
268 exists_mock = mocks.enter_context(268 exists_mock = mocks.enter_context(
269 mock.patch.object(os.path, 'exists',269 mock.patch.object(os.path, 'exists',
270 side_effect=exists_side_effect()))270 side_effect=exists_side_effect()))
271 device = cfg_ds.device_name_to_device(name)271 self.assertEqual(dev_name, cfg_ds.device_name_to_device(name))
272 self.assertEqual(dev_name, device)
273272
274 find_mock.assert_called_once_with(mock.ANY)273 find_mock.assert_called_once_with(mock.ANY)
275 self.assertEqual(exists_mock.call_count, 2)274 self.assertEqual(exists_mock.call_count, 2)
@@ -296,8 +295,7 @@ class TestConfigDriveDataSource(CiTestCase):
296 exists_mock = mocks.enter_context(295 exists_mock = mocks.enter_context(
297 mock.patch.object(os.path, 'exists',296 mock.patch.object(os.path, 'exists',
298 return_value=True))297 return_value=True))
299 device = cfg_ds.device_name_to_device(name)298 self.assertEqual(dev_name, cfg_ds.device_name_to_device(name))
300 self.assertEqual(dev_name, device)
301299
302 find_mock.assert_called_once_with(mock.ANY)300 find_mock.assert_called_once_with(mock.ANY)
303 exists_mock.assert_called_once_with(mock.ANY)301 exists_mock.assert_called_once_with(mock.ANY)
@@ -331,8 +329,7 @@ class TestConfigDriveDataSource(CiTestCase):
331 yield True329 yield True
332 with mock.patch.object(os.path, 'exists',330 with mock.patch.object(os.path, 'exists',
333 side_effect=exists_side_effect()):331 side_effect=exists_side_effect()):
334 device = cfg_ds.device_name_to_device(name)332 self.assertEqual(dev_name, cfg_ds.device_name_to_device(name))
335 self.assertEqual(dev_name, device)
336 # We don't assert the call count for os.path.exists() because333 # We don't assert the call count for os.path.exists() because
337 # not all of the entries in name_tests results in two calls to334 # not all of the entries in name_tests results in two calls to
338 # that function. Specifically, 'root2k' doesn't seem to call335 # that function. Specifically, 'root2k' doesn't seem to call
@@ -359,8 +356,7 @@ class TestConfigDriveDataSource(CiTestCase):
359 }356 }
360 for name, dev_name in name_tests.items():357 for name, dev_name in name_tests.items():
361 with mock.patch.object(os.path, 'exists', return_value=True):358 with mock.patch.object(os.path, 'exists', return_value=True):
362 device = cfg_ds.device_name_to_device(name)359 self.assertEqual(dev_name, cfg_ds.device_name_to_device(name))
363 self.assertEqual(dev_name, device)
364360
365 def test_dir_valid(self):361 def test_dir_valid(self):
366 """Verify a dir is read as such."""362 """Verify a dir is read as such."""
@@ -604,6 +600,9 @@ class TestNetJson(CiTestCase):
604600
605601
606class TestConvertNetworkData(CiTestCase):602class TestConvertNetworkData(CiTestCase):
603
604 with_logs = True
605
607 def setUp(self):606 def setUp(self):
608 super(TestConvertNetworkData, self).setUp()607 super(TestConvertNetworkData, self).setUp()
609 self.tmp = self.tmp_dir()608 self.tmp = self.tmp_dir()
@@ -730,6 +729,26 @@ class TestConvertNetworkData(CiTestCase):
730 'enp0s2': 'fa:16:3e:d4:57:ad'}729 'enp0s2': 'fa:16:3e:d4:57:ad'}
731 self.assertEqual(expected, config_name2mac)730 self.assertEqual(expected, config_name2mac)
732731
732 def test_unknown_device_types_accepted(self):
733 # If we don't recognise a link, we should treat it as physical for a
734 # best-effort boot
735 my_netdata = deepcopy(NETWORK_DATA)
736 my_netdata['links'][0]['type'] = 'my-special-link-type'
737
738 ncfg = openstack.convert_net_json(my_netdata, known_macs=KNOWN_MACS)
739 config_name2mac = {}
740 for n in ncfg['config']:
741 if n['type'] == 'physical':
742 config_name2mac[n['name']] = n['mac_address']
743
744 expected = {'nic0': 'fa:16:3e:05:30:fe', 'enp0s1': 'fa:16:3e:69:b0:58',
745 'enp0s2': 'fa:16:3e:d4:57:ad'}
746 self.assertEqual(expected, config_name2mac)
747
748 # We should, however, warn the user that we don't recognise the type
749 self.assertIn('Unknown network_data link type (my-special-link-type)',
750 self.logs.getvalue())
751
733752
734def cfg_ds_from_dir(base_d, files=None):753def cfg_ds_from_dir(base_d, files=None):
735 run = os.path.join(base_d, "run")754 run = os.path.join(base_d, "run")
diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py
index 1a5956d..20d59bf 100644
--- a/tests/unittests/test_datasource/test_ec2.py
+++ b/tests/unittests/test_datasource/test_ec2.py
@@ -401,6 +401,30 @@ class TestEc2(test_helpers.HttprettyTestCase):
401 ds.metadata = DEFAULT_METADATA401 ds.metadata = DEFAULT_METADATA
402 self.assertEqual('my-identity-id', ds.get_instance_id())402 self.assertEqual('my-identity-id', ds.get_instance_id())
403403
404 def test_classic_instance_true(self):
405 """If no vpc-id in metadata, is_classic_instance must return true."""
406 md_copy = copy.deepcopy(DEFAULT_METADATA)
407 ifaces_md = md_copy.get('network', {}).get('interfaces', {})
408 for _mac, mac_data in ifaces_md.get('macs', {}).items():
409 if 'vpc-id' in mac_data:
410 del mac_data['vpc-id']
411
412 ds = self._setup_ds(
413 platform_data=self.valid_platform_data,
414 sys_cfg={'datasource': {'Ec2': {'strict_id': False}}},
415 md={'md': md_copy})
416 self.assertTrue(ds.get_data())
417 self.assertTrue(ds.is_classic_instance())
418
419 def test_classic_instance_false(self):
420 """If vpc-id in metadata, is_classic_instance must return false."""
421 ds = self._setup_ds(
422 platform_data=self.valid_platform_data,
423 sys_cfg={'datasource': {'Ec2': {'strict_id': False}}},
424 md={'md': DEFAULT_METADATA})
425 self.assertTrue(ds.get_data())
426 self.assertFalse(ds.is_classic_instance())
427
404 @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')428 @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
405 def test_valid_platform_with_strict_true(self, m_dhcp):429 def test_valid_platform_with_strict_true(self, m_dhcp):
406 """Valid platform data should return true with strict_id true."""430 """Valid platform data should return true with strict_id true."""
diff --git a/tests/unittests/test_distros/test_create_users.py b/tests/unittests/test_distros/test_create_users.py
index c3f258d..4062495 100644
--- a/tests/unittests/test_distros/test_create_users.py
+++ b/tests/unittests/test_distros/test_create_users.py
@@ -240,4 +240,32 @@ class TestCreateUser(CiTestCase):
240 [mock.call(set(['auth1']), user), # not disabled240 [mock.call(set(['auth1']), user), # not disabled
241 mock.call(set(['key1']), 'foouser', options=disable_prefix)])241 mock.call(set(['key1']), 'foouser', options=disable_prefix)])
242242
243 @mock.patch("cloudinit.distros.util.which")
244 def test_lock_with_usermod_if_no_passwd(self, m_which, m_subp,
245 m_is_snappy):
246 """Lock uses usermod --lock if no 'passwd' cmd available."""
247 m_which.side_effect = lambda m: m in ('usermod',)
248 self.dist.lock_passwd("bob")
249 self.assertEqual(
250 [mock.call(['usermod', '--lock', 'bob'])],
251 m_subp.call_args_list)
252
253 @mock.patch("cloudinit.distros.util.which")
254 def test_lock_with_passwd_if_available(self, m_which, m_subp,
255 m_is_snappy):
256 """Lock with only passwd will use passwd."""
257 m_which.side_effect = lambda m: m in ('passwd',)
258 self.dist.lock_passwd("bob")
259 self.assertEqual(
260 [mock.call(['passwd', '-l', 'bob'])],
261 m_subp.call_args_list)
262
263 @mock.patch("cloudinit.distros.util.which")
264 def test_lock_raises_runtime_if_no_commands(self, m_which, m_subp,
265 m_is_snappy):
266 """Lock with no commands available raises RuntimeError."""
267 m_which.return_value = None
268 with self.assertRaises(RuntimeError):
269 self.dist.lock_passwd("bob")
270
243# vi: ts=4 expandtab271# vi: ts=4 expandtab
diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
index e986b59..e453040 100644
--- a/tests/unittests/test_distros/test_netconfig.py
+++ b/tests/unittests/test_distros/test_netconfig.py
@@ -407,7 +407,7 @@ class TestNetCfgDistroUbuntuNetplan(TestNetCfgDistroBase):
407 self.assertEqual(0o644, get_mode(cfgpath, tmpd))407 self.assertEqual(0o644, get_mode(cfgpath, tmpd))
408408
409 def netplan_path(self):409 def netplan_path(self):
410 return '/etc/netplan/50-cloud-init.yaml'410 return '/etc/netplan/50-cloud-init.yaml'
411411
412 def test_apply_network_config_v1_to_netplan_ub(self):412 def test_apply_network_config_v1_to_netplan_ub(self):
413 expected_cfgs = {413 expected_cfgs = {
diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
index 756b4fb..d00c1b4 100644
--- a/tests/unittests/test_ds_identify.py
+++ b/tests/unittests/test_ds_identify.py
@@ -441,7 +441,7 @@ class TestDsIdentify(DsIdentifyBase):
441 nova does not identify itself on platforms other than intel.441 nova does not identify itself on platforms other than intel.
442 https://bugs.launchpad.net/cloud-init/+bugs?field.tag=dsid-nova"""442 https://bugs.launchpad.net/cloud-init/+bugs?field.tag=dsid-nova"""
443443
444 data = VALID_CFG['OpenStack'].copy()444 data = copy.deepcopy(VALID_CFG['OpenStack'])
445 del data['files'][P_PRODUCT_NAME]445 del data['files'][P_PRODUCT_NAME]
446 data.update({'policy_dmi': POLICY_FOUND_OR_MAYBE,446 data.update({'policy_dmi': POLICY_FOUND_OR_MAYBE,
447 'policy_no_dmi': POLICY_FOUND_OR_MAYBE})447 'policy_no_dmi': POLICY_FOUND_OR_MAYBE})
diff --git a/tests/unittests/test_handler/test_handler_chef.py b/tests/unittests/test_handler/test_handler_chef.py
index b16532e..f431126 100644
--- a/tests/unittests/test_handler/test_handler_chef.py
+++ b/tests/unittests/test_handler/test_handler_chef.py
@@ -145,6 +145,7 @@ class TestChef(FilesystemMockingTestCase):
145 file_backup_path "/var/backups/chef"145 file_backup_path "/var/backups/chef"
146 pid_file "/var/run/chef/client.pid"146 pid_file "/var/run/chef/client.pid"
147 Chef::Log::Formatter.show_time = true147 Chef::Log::Formatter.show_time = true
148 encrypted_data_bag_secret "/etc/chef/encrypted_data_bag_secret"
148 """149 """
149 tpl_file = util.load_file('templates/chef_client.rb.tmpl')150 tpl_file = util.load_file('templates/chef_client.rb.tmpl')
150 self.patchUtils(self.tmp)151 self.patchUtils(self.tmp)
@@ -157,6 +158,8 @@ class TestChef(FilesystemMockingTestCase):
157 'validation_name': 'bob',158 'validation_name': 'bob',
158 'validation_key': "/etc/chef/vkey.pem",159 'validation_key': "/etc/chef/vkey.pem",
159 'validation_cert': "this is my cert",160 'validation_cert': "this is my cert",
161 'encrypted_data_bag_secret':
162 '/etc/chef/encrypted_data_bag_secret'
160 },163 },
161 }164 }
162 cc_chef.handle('chef', cfg, self.fetch_cloud('ubuntu'), LOG, [])165 cc_chef.handle('chef', cfg, self.fetch_cloud('ubuntu'), LOG, [])
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index e041e97..e3b9e02 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -19,6 +19,7 @@ import gzip
19import io19import io
20import json20import json
21import os21import os
22import re
22import textwrap23import textwrap
23import yaml24import yaml
2425
@@ -103,6 +104,326 @@ STATIC_EXPECTED_1 = {
103 'address': '10.0.0.2'}],104 'address': '10.0.0.2'}],
104}105}
105106
107V1_NAMESERVER_ALIAS = """
108config:
109- id: eno1
110 mac_address: 08:94:ef:51:ae:e0
111 mtu: 1500
112 name: eno1
113 subnets:
114 - type: manual
115 type: physical
116- id: eno2
117 mac_address: 08:94:ef:51:ae:e1
118 mtu: 1500
119 name: eno2
120 subnets:
121 - type: manual
122 type: physical
123- id: eno3
124 mac_address: 08:94:ef:51:ae:de
125 mtu: 1500
126 name: eno3
127 subnets:
128 - type: manual
129 type: physical
130- bond_interfaces:
131 - eno1
132 - eno3
133 id: bondM
134 mac_address: 08:94:ef:51:ae:e0
135 mtu: 1500
136 name: bondM
137 params:
138 bond-downdelay: 0
139 bond-lacp-rate: fast
140 bond-miimon: 100
141 bond-mode: 802.3ad
142 bond-updelay: 0
143 bond-xmit-hash-policy: layer3+4
144 subnets:
145 - address: 10.101.10.47/23
146 gateway: 10.101.11.254
147 type: static
148 type: bond
149- id: eno4
150 mac_address: 08:94:ef:51:ae:df
151 mtu: 1500
152 name: eno4
153 subnets:
154 - type: manual
155 type: physical
156- id: enp0s20f0u1u6
157 mac_address: 0a:94:ef:51:a4:b9
158 mtu: 1500
159 name: enp0s20f0u1u6
160 subnets:
161 - type: manual
162 type: physical
163- id: enp216s0f0
164 mac_address: 68:05:ca:81:7c:e8
165 mtu: 9000
166 name: enp216s0f0
167 subnets:
168 - type: manual
169 type: physical
170- id: enp216s0f1
171 mac_address: 68:05:ca:81:7c:e9
172 mtu: 9000
173 name: enp216s0f1
174 subnets:
175 - type: manual
176 type: physical
177- id: enp47s0f0
178 mac_address: 68:05:ca:64:d3:6c
179 mtu: 9000
180 name: enp47s0f0
181 subnets:
182 - type: manual
183 type: physical
184- bond_interfaces:
185 - enp216s0f0
186 - enp47s0f0
187 id: bond0
188 mac_address: 68:05:ca:64:d3:6c
189 mtu: 9000
190 name: bond0
191 params:
192 bond-downdelay: 0
193 bond-lacp-rate: fast
194 bond-miimon: 100
195 bond-mode: 802.3ad
196 bond-updelay: 0
197 bond-xmit-hash-policy: layer3+4
198 subnets:
199 - type: manual
200 type: bond
201- id: bond0.3502
202 mtu: 9000
203 name: bond0.3502
204 subnets:
205 - address: 172.20.80.4/25
206 type: static
207 type: vlan
208 vlan_id: 3502
209 vlan_link: bond0
210- id: bond0.3503
211 mtu: 9000
212 name: bond0.3503
213 subnets:
214 - address: 172.20.80.129/25
215 type: static
216 type: vlan
217 vlan_id: 3503
218 vlan_link: bond0
219- id: enp47s0f1
220 mac_address: 68:05:ca:64:d3:6d
221 mtu: 9000
222 name: enp47s0f1
223 subnets:
224 - type: manual
225 type: physical
226- bond_interfaces:
227 - enp216s0f1
228 - enp47s0f1
229 id: bond1
230 mac_address: 68:05:ca:64:d3:6d
231 mtu: 9000
232 name: bond1
233 params:
234 bond-downdelay: 0
235 bond-lacp-rate: fast
236 bond-miimon: 100
237 bond-mode: 802.3ad
238 bond-updelay: 0
239 bond-xmit-hash-policy: layer3+4
240 subnets:
241 - address: 10.101.8.65/26
242 routes:
243 - destination: 213.119.192.0/24
244 gateway: 10.101.8.126
245 metric: 0
246 type: static
247 type: bond
248- address:
249 - 10.101.10.1
250 - 10.101.10.2
251 - 10.101.10.3
252 - 10.101.10.5
253 search:
254 - foo.bar
255 - maas
256 type: nameserver
257version: 1
258"""
259
260NETPLAN_NO_ALIAS = """
261network:
262 version: 2
263 ethernets:
264 eno1:
265 match:
266 macaddress: 08:94:ef:51:ae:e0
267 mtu: 1500
268 set-name: eno1
269 eno2:
270 match:
271 macaddress: 08:94:ef:51:ae:e1
272 mtu: 1500
273 set-name: eno2
274 eno3:
275 match:
276 macaddress: 08:94:ef:51:ae:de
277 mtu: 1500
278 set-name: eno3
279 eno4:
280 match:
281 macaddress: 08:94:ef:51:ae:df
282 mtu: 1500
283 set-name: eno4
284 enp0s20f0u1u6:
285 match:
286 macaddress: 0a:94:ef:51:a4:b9
287 mtu: 1500
288 set-name: enp0s20f0u1u6
289 enp216s0f0:
290 match:
291 macaddress: 68:05:ca:81:7c:e8
292 mtu: 9000
293 set-name: enp216s0f0
294 enp216s0f1:
295 match:
296 macaddress: 68:05:ca:81:7c:e9
297 mtu: 9000
298 set-name: enp216s0f1
299 enp47s0f0:
300 match:
301 macaddress: 68:05:ca:64:d3:6c
302 mtu: 9000
303 set-name: enp47s0f0
304 enp47s0f1:
305 match:
306 macaddress: 68:05:ca:64:d3:6d
307 mtu: 9000
308 set-name: enp47s0f1
309 bonds:
310 bond0:
311 interfaces:
312 - enp216s0f0
313 - enp47s0f0
314 macaddress: 68:05:ca:64:d3:6c
315 mtu: 9000
316 parameters:
317 down-delay: 0
318 lacp-rate: fast
319 mii-monitor-interval: 100
320 mode: 802.3ad
321 transmit-hash-policy: layer3+4
322 up-delay: 0
323 bond1:
324 addresses:
325 - 10.101.8.65/26
326 interfaces:
327 - enp216s0f1
328 - enp47s0f1
329 macaddress: 68:05:ca:64:d3:6d
330 mtu: 9000
331 nameservers:
332 addresses:
333 - 10.101.10.1
334 - 10.101.10.2
335 - 10.101.10.3
336 - 10.101.10.5
337 search:
338 - foo.bar
339 - maas
340 parameters:
341 down-delay: 0
342 lacp-rate: fast
343 mii-monitor-interval: 100
344 mode: 802.3ad
345 transmit-hash-policy: layer3+4
346 up-delay: 0
347 routes:
348 - metric: 0
349 to: 213.119.192.0/24
350 via: 10.101.8.126
351 bondM:
352 addresses:
353 - 10.101.10.47/23
354 gateway4: 10.101.11.254
355 interfaces:
356 - eno1
357 - eno3
358 macaddress: 08:94:ef:51:ae:e0
359 mtu: 1500
360 nameservers:
361 addresses:
362 - 10.101.10.1
363 - 10.101.10.2
364 - 10.101.10.3
365 - 10.101.10.5
366 search:
367 - foo.bar
368 - maas
369 parameters:
370 down-delay: 0
371 lacp-rate: fast
372 mii-monitor-interval: 100
373 mode: 802.3ad
374 transmit-hash-policy: layer3+4
375 up-delay: 0
376 vlans:
377 bond0.3502:
378 addresses:
379 - 172.20.80.4/25
380 id: 3502
381 link: bond0
382 mtu: 9000
383 nameservers:
384 addresses:
385 - 10.101.10.1
386 - 10.101.10.2
387 - 10.101.10.3
388 - 10.101.10.5
389 search:
390 - foo.bar
391 - maas
392 bond0.3503:
393 addresses:
394 - 172.20.80.129/25
395 id: 3503
396 link: bond0
397 mtu: 9000
398 nameservers:
399 addresses:
400 - 10.101.10.1
401 - 10.101.10.2
402 - 10.101.10.3
403 - 10.101.10.5
404 search:
405 - foo.bar
406 - maas
407"""
408
409NETPLAN_DHCP_FALSE = """
410version: 2
411ethernets:
412 ens3:
413 match:
414 macaddress: 52:54:00:ab:cd:ef
415 dhcp4: false
416 dhcp6: false
417 addresses:
418 - 192.168.42.100/24
419 - 2001:db8::100/32
420 gateway4: 192.168.42.1
421 gateway6: 2001:db8::1
422 nameservers:
423 search: [example.com]
424 addresses: [192.168.42.53, 1.1.1.1]
425"""
426
106# Examples (and expected outputs for various renderers).427# Examples (and expected outputs for various renderers).
107OS_SAMPLES = [428OS_SAMPLES = [
108 {429 {
@@ -2286,6 +2607,50 @@ USERCTL=no
2286 config = sysconfig.ConfigObj(nm_cfg)2607 config = sysconfig.ConfigObj(nm_cfg)
2287 self.assertIn('ifcfg-rh', config['main']['plugins'])2608 self.assertIn('ifcfg-rh', config['main']['plugins'])
22882609
2610 def test_netplan_dhcp_false_disable_dhcp_in_state(self):
2611 """netplan config with dhcp[46]: False should not add dhcp in state"""
2612 net_config = yaml.load(NETPLAN_DHCP_FALSE)
2613 ns = network_state.parse_net_config_data(net_config,
2614 skip_broken=False)
2615
2616 dhcp_found = [snet for iface in ns.iter_interfaces()
2617 for snet in iface['subnets'] if 'dhcp' in snet['type']]
2618
2619 self.assertEqual([], dhcp_found)
2620
2621 def test_netplan_dhcp_false_no_dhcp_in_sysconfig(self):
2622 """netplan cfg with dhcp[46]: False should not have bootproto=dhcp"""
2623
2624 entry = {
2625 'yaml': NETPLAN_DHCP_FALSE,
2626 'expected_sysconfig': {
2627 'ifcfg-ens3': textwrap.dedent("""\
2628 BOOTPROTO=none
2629 DEFROUTE=yes
2630 DEVICE=ens3
2631 DNS1=192.168.42.53
2632 DNS2=1.1.1.1
2633 DOMAIN=example.com
2634 GATEWAY=192.168.42.1
2635 HWADDR=52:54:00:ab:cd:ef
2636 IPADDR=192.168.42.100
2637 IPV6ADDR=2001:db8::100/32
2638 IPV6INIT=yes
2639 IPV6_DEFAULTGW=2001:db8::1
2640 NETMASK=255.255.255.0
2641 NM_CONTROLLED=no
2642 ONBOOT=yes
2643 STARTMODE=auto
2644 TYPE=Ethernet
2645 USERCTL=no
2646 """),
2647 }
2648 }
2649
2650 found = self._render_and_read(network_config=yaml.load(entry['yaml']))
2651 self._compare_files_to_expected(entry['expected_sysconfig'], found)
2652 self._assert_headers(found)
2653
22892654
2290class TestOpenSuseSysConfigRendering(CiTestCase):2655class TestOpenSuseSysConfigRendering(CiTestCase):
22912656
@@ -3065,6 +3430,38 @@ class TestNetplanRoundTrip(CiTestCase):
3065 entry['expected_netplan'].splitlines(),3430 entry['expected_netplan'].splitlines(),
3066 files['/etc/netplan/50-cloud-init.yaml'].splitlines())3431 files['/etc/netplan/50-cloud-init.yaml'].splitlines())
30673432
3433 def test_render_output_has_yaml_no_aliases(self):
3434 entry = {
3435 'yaml': V1_NAMESERVER_ALIAS,
3436 'expected_netplan': NETPLAN_NO_ALIAS,
3437 }
3438 network_config = yaml.load(entry['yaml'])
3439 ns = network_state.parse_net_config_data(network_config)
3440 files = self._render_and_read(state=ns)
3441 # check for alias
3442 content = files['/etc/netplan/50-cloud-init.yaml']
3443
3444 # test load the yaml to ensure we don't render something not loadable
3445 # this allows single aliases, but not duplicate ones
3446 parsed = yaml.load(files['/etc/netplan/50-cloud-init.yaml'])
3447 self.assertNotEqual(None, parsed)
3448
3449 # now look for any alias, avoid rendering them entirely
3450 # generate the first anchor string using the template
3451 # as of this writing, looks like "&id001"
3452 anchor = r'&' + yaml.serializer.Serializer.ANCHOR_TEMPLATE % 1
3453 found_alias = re.search(anchor, content, re.MULTILINE)
3454 if found_alias:
3455 msg = "Error at: %s\nContent:\n%s" % (found_alias, content)
3456 raise ValueError('Found yaml alias in rendered netplan: ' + msg)
3457
3458 print(entry['expected_netplan'])
3459 print('-- expected ^ | v rendered --')
3460 print(files['/etc/netplan/50-cloud-init.yaml'])
3461 self.assertEqual(
3462 entry['expected_netplan'].splitlines(),
3463 files['/etc/netplan/50-cloud-init.yaml'].splitlines())
3464
30683465
3069class TestEniRoundTrip(CiTestCase):3466class TestEniRoundTrip(CiTestCase):
30703467
diff --git a/tools/cloud-init-per b/tools/cloud-init-per
index 7d6754b..eae3e93 100755
--- a/tools/cloud-init-per
+++ b/tools/cloud-init-per
@@ -38,7 +38,7 @@ fi
38[ "$1" = "-h" -o "$1" = "--help" ] && { Usage ; exit 0; }38[ "$1" = "-h" -o "$1" = "--help" ] && { Usage ; exit 0; }
39[ $# -ge 3 ] || { Usage 1>&2; exit 1; }39[ $# -ge 3 ] || { Usage 1>&2; exit 1; }
40freq=$140freq=$1
41name=$241name=${2/-/_}
42shift 2;42shift 2;
4343
44[ "${name#*/}" = "${name}" ] || fail "name cannot contain a /"44[ "${name#*/}" = "${name}" ] || fail "name cannot contain a /"
@@ -53,6 +53,12 @@ esac
53[ -d "${sem%/*}" ] || mkdir -p "${sem%/*}" ||53[ -d "${sem%/*}" ] || mkdir -p "${sem%/*}" ||
54 fail "failed to make directory for ${sem}"54 fail "failed to make directory for ${sem}"
5555
56# Rename legacy sem files with dashes in their names. Do not overwrite existing
57# sem files to prevent clobbering those which may have been created from calls
58# outside of cloud-init.
59sem_legacy="${sem/_/-}"
60[ "$sem" != "$sem_legacy" -a -e "$sem_legacy" ] && mv -n "$sem_legacy" "$sem"
61
56[ "$freq" != "always" -a -e "$sem" ] && exit 062[ "$freq" != "always" -a -e "$sem" ] && exit 0
57"$@"63"$@"
58ret=$?64ret=$?

Subscribers

People subscribed via source and target branches