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
1diff --git a/.gitignore b/.gitignore
2index 75565ed..80c509e 100644
3--- a/.gitignore
4+++ b/.gitignore
5@@ -11,3 +11,4 @@ prime
6 stage
7 *.snap
8 *.cover
9+.idea/
10diff --git a/cloudinit/cmd/clean.py b/cloudinit/cmd/clean.py
11index de22f7f..30e49de 100644
12--- a/cloudinit/cmd/clean.py
13+++ b/cloudinit/cmd/clean.py
14@@ -5,12 +5,13 @@
15 """Define 'clean' utility and handler as part of cloud-init commandline."""
16
17 import argparse
18+import glob
19 import os
20 import sys
21
22 from cloudinit.stages import Init
23 from cloudinit.util import (
24- ProcessExecutionError, chdir, del_dir, del_file, get_config_logfiles,
25+ ProcessExecutionError, del_dir, del_file, get_config_logfiles,
26 is_link, subp)
27
28
29@@ -61,18 +62,18 @@ def remove_artifacts(remove_logs, remove_seed=False):
30
31 if not os.path.isdir(init.paths.cloud_dir):
32 return 0 # Artifacts dir already cleaned
33- with chdir(init.paths.cloud_dir):
34- for path in os.listdir('.'):
35- if path == 'seed' and not remove_seed:
36- continue
37- try:
38- if os.path.isdir(path) and not is_link(path):
39- del_dir(path)
40- else:
41- del_file(path)
42- except OSError as e:
43- error('Could not remove {0}: {1}'.format(path, str(e)))
44- return 1
45+ seed_path = os.path.join(init.paths.cloud_dir, 'seed')
46+ for path in glob.glob('%s/*' % init.paths.cloud_dir):
47+ if path == seed_path and not remove_seed:
48+ continue
49+ try:
50+ if os.path.isdir(path) and not is_link(path):
51+ del_dir(path)
52+ else:
53+ del_file(path)
54+ except OSError as e:
55+ error('Could not remove {0}: {1}'.format(path, str(e)))
56+ return 1
57 return 0
58
59
60diff --git a/cloudinit/cmd/tests/test_clean.py b/cloudinit/cmd/tests/test_clean.py
61index 5a3ec3b..f092ab3 100644
62--- a/cloudinit/cmd/tests/test_clean.py
63+++ b/cloudinit/cmd/tests/test_clean.py
64@@ -22,7 +22,8 @@ class TestClean(CiTestCase):
65 class FakeInit(object):
66 cfg = {'def_log_file': self.log1,
67 'output': {'all': '|tee -a {0}'.format(self.log2)}}
68- paths = mypaths(cloud_dir=self.artifact_dir)
69+ # Ensure cloud_dir has a trailing slash, to match real behaviour
70+ paths = mypaths(cloud_dir='{}/'.format(self.artifact_dir))
71
72 def __init__(self, ds_deps):
73 pass
74@@ -136,7 +137,8 @@ class TestClean(CiTestCase):
75 clean.remove_artifacts, remove_logs=False)
76 self.assertEqual(1, retcode)
77 self.assertEqual(
78- 'ERROR: Could not remove dir1: oops\n', m_stderr.getvalue())
79+ 'ERROR: Could not remove %s/dir1: oops\n' % self.artifact_dir,
80+ m_stderr.getvalue())
81
82 def test_handle_clean_args_reboots(self):
83 """handle_clean_args_reboots when reboot arg is provided."""
84diff --git a/cloudinit/config/cc_apt_pipelining.py b/cloudinit/config/cc_apt_pipelining.py
85index cdf28cd..459332a 100644
86--- a/cloudinit/config/cc_apt_pipelining.py
87+++ b/cloudinit/config/cc_apt_pipelining.py
88@@ -49,7 +49,7 @@ APT_PIPE_TPL = ("//Written by cloud-init per 'apt_pipelining'\n"
89
90 def handle(_name, cfg, _cloud, log, _args):
91
92- apt_pipe_value = util.get_cfg_option_str(cfg, "apt_pipelining", False)
93+ apt_pipe_value = util.get_cfg_option_str(cfg, "apt_pipelining", 'os')
94 apt_pipe_value_s = str(apt_pipe_value).lower().strip()
95
96 if apt_pipe_value_s == "false":
97@@ -59,7 +59,7 @@ def handle(_name, cfg, _cloud, log, _args):
98 elif apt_pipe_value_s in [str(b) for b in range(0, 6)]:
99 write_apt_snippet(apt_pipe_value_s, log, DEFAULT_FILE)
100 else:
101- log.warn("Invalid option for apt_pipeling: %s", apt_pipe_value)
102+ log.warn("Invalid option for apt_pipelining: %s", apt_pipe_value)
103
104
105 def write_apt_snippet(setting, log, f_name):
106diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py
107index 46abedd..a624030 100644
108--- a/cloudinit/config/cc_chef.py
109+++ b/cloudinit/config/cc_chef.py
110@@ -51,6 +51,7 @@ file).
111
112 chef:
113 client_key:
114+ encrypted_data_bag_secret:
115 environment:
116 file_backup_path:
117 file_cache_path:
118@@ -114,6 +115,7 @@ CHEF_RB_TPL_DEFAULTS = {
119 'file_backup_path': "/var/backups/chef",
120 'pid_file': "/var/run/chef/client.pid",
121 'show_time': True,
122+ 'encrypted_data_bag_secret': None,
123 }
124 CHEF_RB_TPL_BOOL_KEYS = frozenset(['show_time'])
125 CHEF_RB_TPL_PATH_KEYS = frozenset([
126@@ -124,6 +126,7 @@ CHEF_RB_TPL_PATH_KEYS = frozenset([
127 'json_attribs',
128 'file_cache_path',
129 'pid_file',
130+ 'encrypted_data_bag_secret',
131 ])
132 CHEF_RB_TPL_KEYS = list(CHEF_RB_TPL_DEFAULTS.keys())
133 CHEF_RB_TPL_KEYS.extend(CHEF_RB_TPL_BOOL_KEYS)
134diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py
135index 27d2366..22b1753 100644
136--- a/cloudinit/config/cc_rsyslog.py
137+++ b/cloudinit/config/cc_rsyslog.py
138@@ -203,7 +203,7 @@ LOG = logging.getLogger(__name__)
139 COMMENT_RE = re.compile(r'[ ]*[#]+[ ]*')
140 HOST_PORT_RE = re.compile(
141 r'^(?P<proto>[@]{0,2})'
142- r'(([[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'
143+ r'(([\[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'
144 r'([:](?P<port>[0-9]+))?$')
145
146
147diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py
148index 080a6d0..807c3ee 100644
149--- a/cloudinit/config/schema.py
150+++ b/cloudinit/config/schema.py
151@@ -367,7 +367,7 @@ def handle_schema_args(name, args):
152 if not args.annotate:
153 error(str(e))
154 except RuntimeError as e:
155- error(str(e))
156+ error(str(e))
157 else:
158 print("Valid cloud-config file {0}".format(args.config_file))
159 if args.doc:
160diff --git a/cloudinit/config/tests/test_apt_pipelining.py b/cloudinit/config/tests/test_apt_pipelining.py
161new file mode 100644
162index 0000000..2a6bb10
163--- /dev/null
164+++ b/cloudinit/config/tests/test_apt_pipelining.py
165@@ -0,0 +1,28 @@
166+# This file is part of cloud-init. See LICENSE file for license information.
167+
168+"""Tests cc_apt_pipelining handler"""
169+
170+import cloudinit.config.cc_apt_pipelining as cc_apt_pipelining
171+
172+from cloudinit.tests.helpers import CiTestCase, mock
173+
174+
175+class TestAptPipelining(CiTestCase):
176+
177+ @mock.patch('cloudinit.config.cc_apt_pipelining.util.write_file')
178+ def test_not_disabled_by_default(self, m_write_file):
179+ """ensure that default behaviour is to not disable pipelining"""
180+ cc_apt_pipelining.handle('foo', {}, None, mock.MagicMock(), None)
181+ self.assertEqual(0, m_write_file.call_count)
182+
183+ @mock.patch('cloudinit.config.cc_apt_pipelining.util.write_file')
184+ def test_false_disables_pipelining(self, m_write_file):
185+ """ensure that pipelining can be disabled with correct config"""
186+ cc_apt_pipelining.handle(
187+ 'foo', {'apt_pipelining': 'false'}, None, mock.MagicMock(), None)
188+ self.assertEqual(1, m_write_file.call_count)
189+ args, _ = m_write_file.call_args
190+ self.assertEqual(cc_apt_pipelining.DEFAULT_FILE, args[0])
191+ self.assertIn('Pipeline-Depth "0"', args[1])
192+
193+# vi: ts=4 expandtab
194diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
195index ef618c2..20c994d 100644
196--- a/cloudinit/distros/__init__.py
197+++ b/cloudinit/distros/__init__.py
198@@ -577,11 +577,16 @@ class Distro(object):
199 """
200 Lock the password of a user, i.e., disable password logins
201 """
202+ # passwd must use short '-l' due to SLES11 lacking long form '--lock'
203+ lock_tools = (['passwd', '-l', name], ['usermod', '--lock', name])
204 try:
205- # Need to use the short option name '-l' instead of '--lock'
206- # (which would be more descriptive) since SLES 11 doesn't know
207- # about long names.
208- util.subp(['passwd', '-l', name])
209+ cmd = next(l for l in lock_tools if util.which(l[0]))
210+ except StopIteration:
211+ raise RuntimeError((
212+ "Unable to lock user account '%s'. No tools available. "
213+ " Tried: %s.") % (name, [c[0] for c in lock_tools]))
214+ try:
215+ util.subp(cmd)
216 except Exception as e:
217 util.logexc(LOG, 'Failed to disable password for user %s', name)
218 raise e
219diff --git a/cloudinit/handlers/upstart_job.py b/cloudinit/handlers/upstart_job.py
220index 83fb072..003cad6 100644
221--- a/cloudinit/handlers/upstart_job.py
222+++ b/cloudinit/handlers/upstart_job.py
223@@ -89,7 +89,7 @@ def _has_suitable_upstart():
224 util.subp(["dpkg", "--compare-versions", dpkg_ver, "ge", good])
225 return True
226 except util.ProcessExecutionError as e:
227- if e.exit_code is 1:
228+ if e.exit_code == 1:
229 pass
230 else:
231 util.logexc(LOG, "dpkg --compare-versions failed [%s]",
232diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
233index 21517fd..e54a34e 100644
234--- a/cloudinit/net/netplan.py
235+++ b/cloudinit/net/netplan.py
236@@ -361,7 +361,8 @@ class Renderer(renderer.Renderer):
237 if section:
238 dump = util.yaml_dumps({name: section},
239 explicit_start=False,
240- explicit_end=False)
241+ explicit_end=False,
242+ noalias=True)
243 txt = util.indent(dump, ' ' * 4)
244 return [txt]
245 return []
246diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
247index f76e508..539b76d 100644
248--- a/cloudinit/net/network_state.py
249+++ b/cloudinit/net/network_state.py
250@@ -706,9 +706,9 @@ class NetworkStateInterpreter(object):
251 """Common ipconfig extraction from v2 to v1 subnets array."""
252
253 subnets = []
254- if 'dhcp4' in cfg:
255+ if cfg.get('dhcp4'):
256 subnets.append({'type': 'dhcp4'})
257- if 'dhcp6' in cfg:
258+ if cfg.get('dhcp6'):
259 self.use_ipv6 = True
260 subnets.append({'type': 'dhcp6'})
261
262diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
263index 79e8842..5139024 100644
264--- a/cloudinit/net/tests/test_dhcp.py
265+++ b/cloudinit/net/tests/test_dhcp.py
266@@ -117,6 +117,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
267 self.assertEqual('eth9', call[0][1])
268 self.assertIn('/var/tmp/cloud-init/cloud-init-dhcp-', call[0][2])
269
270+ @mock.patch('time.sleep', mock.MagicMock())
271 @mock.patch('cloudinit.net.dhcp.os.kill')
272 @mock.patch('cloudinit.net.dhcp.util.subp')
273 def test_dhcp_discovery_run_in_sandbox_warns_invalid_pid(self, m_subp,
274diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
275index 9ff929c..e91cd26 100644
276--- a/cloudinit/netinfo.py
277+++ b/cloudinit/netinfo.py
278@@ -141,6 +141,9 @@ def _netdev_info_ifconfig(ifconfig_data):
279 res = re.match(r'.*<(\S+)>', toks[i + 1])
280 if res:
281 devs[curdev]['ipv6'][-1]['scope6'] = res.group(1)
282+ else:
283+ devs[curdev]['ipv6'][-1]['scope6'] = toks[i + 1]
284+
285 return devs
286
287
288@@ -389,8 +392,8 @@ def netdev_pformat():
289 addr.get('scope', empty), data["hwaddr"]))
290 for addr in data.get('ipv6'):
291 tbl.add_row(
292- (dev, data["up"], addr["ip"], empty, addr["scope6"],
293- data["hwaddr"]))
294+ (dev, data["up"], addr["ip"], empty,
295+ addr.get("scope6", empty), data["hwaddr"]))
296 if len(data.get('ipv6')) + len(data.get('ipv4')) == 0:
297 tbl.add_row((dev, data["up"], empty, empty, empty,
298 data["hwaddr"]))
299diff --git a/cloudinit/safeyaml.py b/cloudinit/safeyaml.py
300index 7bcf9dd..3bd5e03 100644
301--- a/cloudinit/safeyaml.py
302+++ b/cloudinit/safeyaml.py
303@@ -17,6 +17,13 @@ _CustomSafeLoader.add_constructor(
304 _CustomSafeLoader.construct_python_unicode)
305
306
307+class NoAliasSafeDumper(yaml.dumper.SafeDumper):
308+ """A class which avoids constructing anchors/aliases on yaml dump"""
309+
310+ def ignore_aliases(self, data):
311+ return True
312+
313+
314 def load(blob):
315 return(yaml.load(blob, Loader=_CustomSafeLoader))
316
317diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
318index a4f998b..eccbee5 100644
319--- a/cloudinit/sources/DataSourceAzure.py
320+++ b/cloudinit/sources/DataSourceAzure.py
321@@ -627,9 +627,11 @@ class DataSourceAzure(sources.DataSource):
322 if self.ds_cfg['agent_command'] == AGENT_START_BUILTIN:
323 self.bounce_network_with_azure_hostname()
324
325+ pubkey_info = self.cfg.get('_pubkeys', None)
326 metadata_func = partial(get_metadata_from_fabric,
327 fallback_lease_file=self.
328- dhclient_lease_file)
329+ dhclient_lease_file,
330+ pubkey_info=pubkey_info)
331 else:
332 metadata_func = self.get_metadata_from_agent
333
334@@ -642,6 +644,7 @@ class DataSourceAzure(sources.DataSource):
335 "Error communicating with Azure fabric; You may experience."
336 "connectivity issues.", exc_info=True)
337 return False
338+
339 util.del_file(REPORTED_READY_MARKER_FILE)
340 util.del_file(REPROVISION_MARKER_FILE)
341 return fabric_data
342@@ -909,13 +912,15 @@ def find_child(node, filter_func):
343 def load_azure_ovf_pubkeys(sshnode):
344 # This parses a 'SSH' node formatted like below, and returns
345 # an array of dicts.
346- # [{'fp': '6BE7A7C3C8A8F4B123CCA5D0C2F1BE4CA7B63ED7',
347- # 'path': 'where/to/go'}]
348+ # [{'fingerprint': '6BE7A7C3C8A8F4B123CCA5D0C2F1BE4CA7B63ED7',
349+ # 'path': '/where/to/go'}]
350 #
351 # <SSH><PublicKeys>
352- # <PublicKey><Fingerprint>ABC</FingerPrint><Path>/ABC</Path>
353+ # <PublicKey><Fingerprint>ABC</FingerPrint><Path>/x/y/z</Path>
354 # ...
355 # </PublicKeys></SSH>
356+ # Under some circumstances, there may be a <Value> element along with the
357+ # Fingerprint and Path. Pass those along if they appear.
358 results = find_child(sshnode, lambda n: n.localName == "PublicKeys")
359 if len(results) == 0:
360 return []
361diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
362index 9ccf2cd..4f2f6cc 100644
363--- a/cloudinit/sources/DataSourceEc2.py
364+++ b/cloudinit/sources/DataSourceEc2.py
365@@ -19,6 +19,7 @@ from cloudinit import sources
366 from cloudinit import url_helper as uhelp
367 from cloudinit import util
368 from cloudinit import warnings
369+from cloudinit.event import EventType
370
371 LOG = logging.getLogger(__name__)
372
373@@ -107,6 +108,19 @@ class DataSourceEc2(sources.DataSource):
374 'dynamic', {}).get('instance-identity', {}).get('document', {})
375 return True
376
377+ def is_classic_instance(self):
378+ """Report if this instance type is Ec2 Classic (non-vpc)."""
379+ if not self.metadata:
380+ # Can return False on inconclusive as we are also called in
381+ # network_config where metadata will be present.
382+ # Secondary call site is in packaging postinst script.
383+ return False
384+ ifaces_md = self.metadata.get('network', {}).get('interfaces', {})
385+ for _mac, mac_data in ifaces_md.get('macs', {}).items():
386+ if 'vpc-id' in mac_data:
387+ return False
388+ return True
389+
390 @property
391 def launch_index(self):
392 if not self.metadata:
393@@ -320,6 +334,13 @@ class DataSourceEc2(sources.DataSource):
394 if isinstance(net_md, dict):
395 result = convert_ec2_metadata_network_config(
396 net_md, macs_to_nics=macs_to_nics, fallback_nic=iface)
397+ # RELEASE_BLOCKER: Xenial debian/postinst needs to add
398+ # EventType.BOOT on upgrade path for classic.
399+
400+ # Non-VPC (aka Classic) Ec2 instances need to rewrite the
401+ # network config file every boot due to MAC address change.
402+ if self.is_classic_instance():
403+ self.update_events['network'].add(EventType.BOOT)
404 else:
405 LOG.warning("Metadata 'network' key not valid: %s.", net_md)
406 self._network_config = result
407@@ -442,7 +463,7 @@ def identify_aws(data):
408 if (data['uuid'].startswith('ec2') and
409 (data['uuid_source'] == 'hypervisor' or
410 data['uuid'] == data['serial'])):
411- return CloudNames.AWS
412+ return CloudNames.AWS
413
414 return None
415
416diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py
417index 3a3fcdf..70e7a5c 100644
418--- a/cloudinit/sources/DataSourceOVF.py
419+++ b/cloudinit/sources/DataSourceOVF.py
420@@ -15,6 +15,8 @@ import os
421 import re
422 import time
423
424+import six
425+
426 from cloudinit import log as logging
427 from cloudinit import sources
428 from cloudinit import util
429@@ -434,7 +436,7 @@ def maybe_cdrom_device(devname):
430 """
431 if not devname:
432 return False
433- elif not isinstance(devname, util.string_types):
434+ elif not isinstance(devname, six.string_types):
435 raise ValueError("Unexpected input for devname: %s" % devname)
436
437 # resolve '..' and multi '/' elements
438diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py
439index e5696b1..2829dd2 100644
440--- a/cloudinit/sources/helpers/azure.py
441+++ b/cloudinit/sources/helpers/azure.py
442@@ -138,9 +138,36 @@ class OpenSSLManager(object):
443 self.certificate = certificate
444 LOG.debug('New certificate generated.')
445
446- def parse_certificates(self, certificates_xml):
447- tag = ElementTree.fromstring(certificates_xml).find(
448- './/Data')
449+ @staticmethod
450+ def _run_x509_action(action, cert):
451+ cmd = ['openssl', 'x509', '-noout', action]
452+ result, _ = util.subp(cmd, data=cert)
453+ return result
454+
455+ def _get_ssh_key_from_cert(self, certificate):
456+ pub_key = self._run_x509_action('-pubkey', certificate)
457+ keygen_cmd = ['ssh-keygen', '-i', '-m', 'PKCS8', '-f', '/dev/stdin']
458+ ssh_key, _ = util.subp(keygen_cmd, data=pub_key)
459+ return ssh_key
460+
461+ def _get_fingerprint_from_cert(self, certificate):
462+ """openssl x509 formats fingerprints as so:
463+ 'SHA1 Fingerprint=07:3E:19:D1:4D:1C:79:92:24:C6:A0:FD:8D:DA:\
464+ B6:A8:BF:27:D4:73\n'
465+
466+ Azure control plane passes that fingerprint as so:
467+ '073E19D14D1C799224C6A0FD8DDAB6A8BF27D473'
468+ """
469+ raw_fp = self._run_x509_action('-fingerprint', certificate)
470+ eq = raw_fp.find('=')
471+ octets = raw_fp[eq+1:-1].split(':')
472+ return ''.join(octets)
473+
474+ def _decrypt_certs_from_xml(self, certificates_xml):
475+ """Decrypt the certificates XML document using the our private key;
476+ return the list of certs and private keys contained in the doc.
477+ """
478+ tag = ElementTree.fromstring(certificates_xml).find('.//Data')
479 certificates_content = tag.text
480 lines = [
481 b'MIME-Version: 1.0',
482@@ -151,32 +178,30 @@ class OpenSSLManager(object):
483 certificates_content.encode('utf-8'),
484 ]
485 with cd(self.tmpdir):
486- with open('Certificates.p7m', 'wb') as f:
487- f.write(b'\n'.join(lines))
488 out, _ = util.subp(
489- 'openssl cms -decrypt -in Certificates.p7m -inkey'
490+ 'openssl cms -decrypt -in /dev/stdin -inkey'
491 ' {private_key} -recip {certificate} | openssl pkcs12 -nodes'
492 ' -password pass:'.format(**self.certificate_names),
493- shell=True)
494- private_keys, certificates = [], []
495+ shell=True, data=b'\n'.join(lines))
496+ return out
497+
498+ def parse_certificates(self, certificates_xml):
499+ """Given the Certificates XML document, return a dictionary of
500+ fingerprints and associated SSH keys derived from the certs."""
501+ out = self._decrypt_certs_from_xml(certificates_xml)
502 current = []
503+ keys = {}
504 for line in out.splitlines():
505 current.append(line)
506 if re.match(r'[-]+END .*?KEY[-]+$', line):
507- private_keys.append('\n'.join(current))
508+ # ignore private_keys
509 current = []
510 elif re.match(r'[-]+END .*?CERTIFICATE[-]+$', line):
511- certificates.append('\n'.join(current))
512+ certificate = '\n'.join(current)
513+ ssh_key = self._get_ssh_key_from_cert(certificate)
514+ fingerprint = self._get_fingerprint_from_cert(certificate)
515+ keys[fingerprint] = ssh_key
516 current = []
517- keys = []
518- for certificate in certificates:
519- with cd(self.tmpdir):
520- public_key, _ = util.subp(
521- 'openssl x509 -noout -pubkey |'
522- 'ssh-keygen -i -m PKCS8 -f /dev/stdin',
523- data=certificate,
524- shell=True)
525- keys.append(public_key)
526 return keys
527
528
529@@ -206,7 +231,6 @@ class WALinuxAgentShim(object):
530 self.dhcpoptions = dhcp_options
531 self._endpoint = None
532 self.openssl_manager = None
533- self.values = {}
534 self.lease_file = fallback_lease_file
535
536 def clean_up(self):
537@@ -328,8 +352,9 @@ class WALinuxAgentShim(object):
538 LOG.debug('Azure endpoint found at %s', endpoint_ip_address)
539 return endpoint_ip_address
540
541- def register_with_azure_and_fetch_data(self):
542- self.openssl_manager = OpenSSLManager()
543+ def register_with_azure_and_fetch_data(self, pubkey_info=None):
544+ if self.openssl_manager is None:
545+ self.openssl_manager = OpenSSLManager()
546 http_client = AzureEndpointHttpClient(self.openssl_manager.certificate)
547 LOG.info('Registering with Azure...')
548 attempts = 0
549@@ -347,16 +372,37 @@ class WALinuxAgentShim(object):
550 attempts += 1
551 LOG.debug('Successfully fetched GoalState XML.')
552 goal_state = GoalState(response.contents, http_client)
553- public_keys = []
554- if goal_state.certificates_xml is not None:
555+ ssh_keys = []
556+ if goal_state.certificates_xml is not None and pubkey_info is not None:
557 LOG.debug('Certificate XML found; parsing out public keys.')
558- public_keys = self.openssl_manager.parse_certificates(
559+ keys_by_fingerprint = self.openssl_manager.parse_certificates(
560 goal_state.certificates_xml)
561- data = {
562- 'public-keys': public_keys,
563- }
564+ ssh_keys = self._filter_pubkeys(keys_by_fingerprint, pubkey_info)
565 self._report_ready(goal_state, http_client)
566- return data
567+ return {'public-keys': ssh_keys}
568+
569+ def _filter_pubkeys(self, keys_by_fingerprint, pubkey_info):
570+ """cloud-init expects a straightforward array of keys to be dropped
571+ into the user's authorized_keys file. Azure control plane exposes
572+ multiple public keys to the VM via wireserver. Select just the
573+ user's key(s) and return them, ignoring any other certs.
574+ """
575+ keys = []
576+ for pubkey in pubkey_info:
577+ if 'value' in pubkey and pubkey['value']:
578+ keys.append(pubkey['value'])
579+ elif 'fingerprint' in pubkey and pubkey['fingerprint']:
580+ fingerprint = pubkey['fingerprint']
581+ if fingerprint in keys_by_fingerprint:
582+ keys.append(keys_by_fingerprint[fingerprint])
583+ else:
584+ LOG.warning("ovf-env.xml specified PublicKey fingerprint "
585+ "%s not found in goalstate XML", fingerprint)
586+ else:
587+ LOG.warning("ovf-env.xml specified PublicKey with neither "
588+ "value nor fingerprint: %s", pubkey)
589+
590+ return keys
591
592 def _report_ready(self, goal_state, http_client):
593 LOG.debug('Reporting ready to Azure fabric.')
594@@ -373,11 +419,12 @@ class WALinuxAgentShim(object):
595 LOG.info('Reported ready to Azure fabric.')
596
597
598-def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None):
599+def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None,
600+ pubkey_info=None):
601 shim = WALinuxAgentShim(fallback_lease_file=fallback_lease_file,
602 dhcp_options=dhcp_opts)
603 try:
604- return shim.register_with_azure_and_fetch_data()
605+ return shim.register_with_azure_and_fetch_data(pubkey_info=pubkey_info)
606 finally:
607 shim.clean_up()
608
609diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py
610index 9c29cea..8f06911 100644
611--- a/cloudinit/sources/helpers/openstack.py
612+++ b/cloudinit/sources/helpers/openstack.py
613@@ -67,7 +67,7 @@ OS_VERSIONS = (
614 OS_ROCKY,
615 )
616
617-PHYSICAL_TYPES = (
618+KNOWN_PHYSICAL_TYPES = (
619 None,
620 'bgpovs', # not present in OpenStack upstream but used on OVH cloud.
621 'bridge',
622@@ -600,9 +600,7 @@ def convert_net_json(network_json=None, known_macs=None):
623 subnet['ipv6'] = True
624 subnets.append(subnet)
625 cfg.update({'subnets': subnets})
626- if link['type'] in PHYSICAL_TYPES:
627- cfg.update({'type': 'physical', 'mac_address': link_mac_addr})
628- elif link['type'] in ['bond']:
629+ if link['type'] in ['bond']:
630 params = {}
631 if link_mac_addr:
632 params['mac_address'] = link_mac_addr
633@@ -641,8 +639,10 @@ def convert_net_json(network_json=None, known_macs=None):
634 curinfo.update({'mac': link['vlan_mac_address'],
635 'name': name})
636 else:
637- raise ValueError(
638- 'Unknown network_data link type: %s' % link['type'])
639+ if link['type'] not in KNOWN_PHYSICAL_TYPES:
640+ LOG.warning('Unknown network_data link type (%s); treating as'
641+ ' physical', link['type'])
642+ cfg.update({'type': 'physical', 'mac_address': link_mac_addr})
643
644 config.append(cfg)
645 link_id_info[curinfo['id']] = curinfo
646diff --git a/cloudinit/stages.py b/cloudinit/stages.py
647index 8a06412..da7d349 100644
648--- a/cloudinit/stages.py
649+++ b/cloudinit/stages.py
650@@ -548,11 +548,11 @@ class Init(object):
651 with events.ReportEventStack("consume-user-data",
652 "reading and applying user-data",
653 parent=self.reporter):
654- self._consume_userdata(frequency)
655+ self._consume_userdata(frequency)
656 with events.ReportEventStack("consume-vendor-data",
657 "reading and applying vendor-data",
658 parent=self.reporter):
659- self._consume_vendordata(frequency)
660+ self._consume_vendordata(frequency)
661
662 # Perform post-consumption adjustments so that
663 # modules that run during the init stage reflect
664diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
665index 2eb7b0c..f41180f 100644
666--- a/cloudinit/tests/helpers.py
667+++ b/cloudinit/tests/helpers.py
668@@ -41,26 +41,6 @@ _real_subp = util.subp
669 SkipTest = unittest2.SkipTest
670 skipIf = unittest2.skipIf
671
672-# Used for detecting different python versions
673-PY2 = False
674-PY26 = False
675-PY27 = False
676-PY3 = False
677-
678-_PY_VER = sys.version_info
679-_PY_MAJOR, _PY_MINOR, _PY_MICRO = _PY_VER[0:3]
680-if (_PY_MAJOR, _PY_MINOR) <= (2, 6):
681- if (_PY_MAJOR, _PY_MINOR) == (2, 6):
682- PY26 = True
683- if (_PY_MAJOR, _PY_MINOR) >= (2, 0):
684- PY2 = True
685-else:
686- if (_PY_MAJOR, _PY_MINOR) == (2, 7):
687- PY27 = True
688- PY2 = True
689- if (_PY_MAJOR, _PY_MINOR) >= (3, 0):
690- PY3 = True
691-
692
693 # Makes the old path start
694 # with new base instead of whatever
695@@ -207,6 +187,7 @@ class CiTestCase(TestCase):
696 if self.with_logs:
697 # Remove the handler we setup
698 logging.getLogger().handlers = self.old_handlers
699+ logging.getLogger().level = None
700 util.subp = _real_subp
701 super(CiTestCase, self).tearDown()
702
703@@ -356,7 +337,7 @@ class FilesystemMockingTestCase(ResourceUsingTestCase):
704
705 def patchOpen(self, new_root):
706 trap_func = retarget_many_wrapper(new_root, 1, open)
707- name = 'builtins.open' if PY3 else '__builtin__.open'
708+ name = 'builtins.open' if six.PY3 else '__builtin__.open'
709 self.patched_funcs.enter_context(mock.patch(name, trap_func))
710
711 def patchStdoutAndStderr(self, stdout=None, stderr=None):
712diff --git a/cloudinit/tests/test_netinfo.py b/cloudinit/tests/test_netinfo.py
713index d76e768..1c8a791 100644
714--- a/cloudinit/tests/test_netinfo.py
715+++ b/cloudinit/tests/test_netinfo.py
716@@ -11,6 +11,7 @@ from cloudinit.tests.helpers import CiTestCase, mock, readResource
717 # Example ifconfig and route output
718 SAMPLE_OLD_IFCONFIG_OUT = readResource("netinfo/old-ifconfig-output")
719 SAMPLE_NEW_IFCONFIG_OUT = readResource("netinfo/new-ifconfig-output")
720+SAMPLE_FREEBSD_IFCONFIG_OUT = readResource("netinfo/freebsd-ifconfig-output")
721 SAMPLE_IPADDRSHOW_OUT = readResource("netinfo/sample-ipaddrshow-output")
722 SAMPLE_ROUTE_OUT_V4 = readResource("netinfo/sample-route-output-v4")
723 SAMPLE_ROUTE_OUT_V6 = readResource("netinfo/sample-route-output-v6")
724@@ -18,6 +19,7 @@ SAMPLE_IPROUTE_OUT_V4 = readResource("netinfo/sample-iproute-output-v4")
725 SAMPLE_IPROUTE_OUT_V6 = readResource("netinfo/sample-iproute-output-v6")
726 NETDEV_FORMATTED_OUT = readResource("netinfo/netdev-formatted-output")
727 ROUTE_FORMATTED_OUT = readResource("netinfo/route-formatted-output")
728+FREEBSD_NETDEV_OUT = readResource("netinfo/freebsd-netdev-formatted-output")
729
730
731 class TestNetInfo(CiTestCase):
732@@ -45,6 +47,18 @@ class TestNetInfo(CiTestCase):
733
734 @mock.patch('cloudinit.netinfo.util.which')
735 @mock.patch('cloudinit.netinfo.util.subp')
736+ def test_netdev_freebsd_nettools_pformat(self, m_subp, m_which):
737+ """netdev_pformat properly rendering netdev new nettools info."""
738+ m_subp.return_value = (SAMPLE_FREEBSD_IFCONFIG_OUT, '')
739+ m_which.side_effect = lambda x: x if x == 'ifconfig' else None
740+ content = netdev_pformat()
741+ print()
742+ print(content)
743+ print()
744+ self.assertEqual(FREEBSD_NETDEV_OUT, content)
745+
746+ @mock.patch('cloudinit.netinfo.util.which')
747+ @mock.patch('cloudinit.netinfo.util.subp')
748 def test_netdev_iproute_pformat(self, m_subp, m_which):
749 """netdev_pformat properly rendering ip route info."""
750 m_subp.return_value = (SAMPLE_IPADDRSHOW_OUT, '')
751diff --git a/cloudinit/url_helper.py b/cloudinit/url_helper.py
752index 396d69a..0af0d9e 100644
753--- a/cloudinit/url_helper.py
754+++ b/cloudinit/url_helper.py
755@@ -521,7 +521,7 @@ class OauthUrlHelper(object):
756 if extra_exception_cb:
757 ret = extra_exception_cb(msg, exception)
758 finally:
759- self.exception_cb(msg, exception)
760+ self.exception_cb(msg, exception)
761 return ret
762
763 def _headers_cb(self, extra_headers_cb, url):
764diff --git a/cloudinit/util.py b/cloudinit/util.py
765index a8a232b..a192091 100644
766--- a/cloudinit/util.py
767+++ b/cloudinit/util.py
768@@ -51,11 +51,6 @@ from cloudinit import version
769
770 from cloudinit.settings import (CFG_BUILTIN)
771
772-try:
773- string_types = (basestring,)
774-except NameError:
775- string_types = (str,)
776-
777 _DNS_REDIRECT_IP = None
778 LOG = logging.getLogger(__name__)
779
780@@ -77,7 +72,6 @@ CONTAINER_TESTS = (['systemd-detect-virt', '--quiet', '--container'],
781 PROC_CMDLINE = None
782
783 _LSB_RELEASE = {}
784-PY26 = sys.version_info[0:2] == (2, 6)
785
786
787 def get_architecture(target=None):
788@@ -125,7 +119,7 @@ def target_path(target, path=None):
789 # return 'path' inside target, accepting target as None
790 if target in (None, ""):
791 target = "/"
792- elif not isinstance(target, string_types):
793+ elif not isinstance(target, six.string_types):
794 raise ValueError("Unexpected input for target: %s" % target)
795 else:
796 target = os.path.abspath(target)
797@@ -1596,14 +1590,17 @@ def json_dumps(data):
798 separators=(',', ': '), default=json_serialize_default)
799
800
801-def yaml_dumps(obj, explicit_start=True, explicit_end=True):
802+def yaml_dumps(obj, explicit_start=True, explicit_end=True, noalias=False):
803 """Return data in nicely formatted yaml."""
804- return yaml.safe_dump(obj,
805- line_break="\n",
806- indent=4,
807- explicit_start=explicit_start,
808- explicit_end=explicit_end,
809- default_flow_style=False)
810+
811+ return yaml.dump(obj,
812+ line_break="\n",
813+ indent=4,
814+ explicit_start=explicit_start,
815+ explicit_end=explicit_end,
816+ default_flow_style=False,
817+ Dumper=(safeyaml.NoAliasSafeDumper
818+ if noalias else yaml.dumper.Dumper))
819
820
821 def ensure_dir(path, mode=None):
822@@ -2817,9 +2814,6 @@ def load_shell_content(content, add_empty=False, empty_val=None):
823 variables. Set their value to empty_val."""
824
825 def _shlex_split(blob):
826- if PY26 and isinstance(blob, six.text_type):
827- # Older versions don't support unicode input
828- blob = blob.encode("utf8")
829 return shlex.split(blob, comments=True)
830
831 data = {}
832diff --git a/debian/changelog b/debian/changelog
833index 70f1879..61f09d7 100644
834--- a/debian/changelog
835+++ b/debian/changelog
836@@ -1,3 +1,47 @@
837+cloud-init (18.5-44-g7c07af28-0ubuntu1) disco; urgency=medium
838+
839+ * New upstream snapshot.
840+ - Support locking user with usermod if passwd is not available.
841+ [Scott Moser]
842+ - Example for Microsoft Azure data disk added. [Anton Olifir]
843+ - clean: correctly determine the path for excluding seed directory
844+ (LP: #1818571)
845+ - helpers/openstack: Treat unknown link types as physical (LP: #1639263)
846+ - drop Python 2.6 support and our NIH version detection
847+ - tip-pylint: Fix assignment-from-return-none errors
848+ - net: append type:dhcp[46] only if dhcp[46] is True in v2 netconfig
849+ [Kurt Stieger] (LP: #1818032)
850+ - cc_apt_pipelining: stop disabling pipelining by default (LP: #1794982)
851+ - tests: fix some slow tests and some leaking state
852+ - util: don't determine string_types ourselves
853+ - cc_rsyslog: Escape possible nested set (LP: #1816967)
854+ - Enable encrypted_data_bag_secret support for Chef
855+ [Eric Williams] (LP: #1817082)
856+ - azure: Filter list of ssh keys pulled from fabric [Jason Zions (MSFT)]
857+ - doc: update merging doc with fixes and some additional details/examples
858+ - tests: integration test failure summary to use traceback if empty error
859+ - This is to fix https://bugs.launchpad.net/cloud-init/+bug/1812676
860+ [Vitaly Kuznetsov]
861+ - EC2: Rewrite network config on AWS Classic instances every boot
862+ [Guilherme G. Piccoli] (LP: #1802073)
863+ - netinfo: Adjust ifconfig output parsing for FreeBSD ipv6 entries
864+ (LP: #1779672)
865+ - netplan: Don't render yaml aliases when dumping netplan (LP: #1815051)
866+ - add PyCharm IDE .idea/ path to .gitignore [Dominic Schlegel]
867+ - correct grammar issue in instance metadata documentation
868+ [Dominic Schlegel] (LP: #1802188)
869+ - clean: cloud-init clean should not trace when run from within cloud_dir
870+ (LP: #1795508)
871+ - Resolve flake8 comparison and pycodestyle over-ident issues
872+ [Paride Legovini]
873+ * Update netplan dependency package (LP: #1813667)
874+ * Fix build-depends-on-obsolete-package for dh-systemd
875+ * Change Priority from extra to optional
876+ * Override lintian warnings about WantedBy=cloud-init.target
877+ * Change Maintainer to Ubuntu Developers
878+
879+ -- Daniel Watkins <oddbloke@ubuntu.com> Thu, 07 Mar 2019 10:32:26 -0500
880+
881 cloud-init (18.5-21-g8ee294d5-0ubuntu1) disco; urgency=medium
882
883 * New upstream snapshot.
884diff --git a/debian/cloud-init.lintian-overrides b/debian/cloud-init.lintian-overrides
885new file mode 100644
886index 0000000..58fac0d
887--- /dev/null
888+++ b/debian/cloud-init.lintian-overrides
889@@ -0,0 +1,4 @@
890+cloud-init binary: systemd-service-file-refers-to-unusual-wantedby-target lib/systemd/system/cloud-config.service cloud-init.target
891+cloud-init binary: systemd-service-file-refers-to-unusual-wantedby-target lib/systemd/system/cloud-final.service cloud-init.target
892+cloud-init binary: systemd-service-file-refers-to-unusual-wantedby-target lib/systemd/system/cloud-init-local.service cloud-init.target
893+cloud-init binary: systemd-service-file-refers-to-unusual-wantedby-target lib/systemd/system/cloud-init.service cloud-init.target
894diff --git a/debian/control b/debian/control
895index 282304a..b1e2e8f 100644
896--- a/debian/control
897+++ b/debian/control
898@@ -1,10 +1,9 @@
899 Source: cloud-init
900 Section: admin
901-Priority: extra
902-Maintainer: Scott Moser <smoser@ubuntu.com>
903-Build-Depends: debhelper (>= 9),
904+Priority: optional
905+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
906+Build-Depends: debhelper (>= 9.20160709),
907 dh-python,
908- dh-systemd,
909 iproute2,
910 pep8,
911 po-debconf,
912@@ -36,7 +35,7 @@ Architecture: all
913 Depends: cloud-guest-utils | cloud-utils,
914 isc-dhcp-client,
915 iproute2,
916- nplan | ifupdown,
917+ netplan.io | ifupdown,
918 procps,
919 python3,
920 python3-requests,
921diff --git a/doc/examples/cloud-config-chef.txt b/doc/examples/cloud-config-chef.txt
922index defc5a5..2320e01 100644
923--- a/doc/examples/cloud-config-chef.txt
924+++ b/doc/examples/cloud-config-chef.txt
925@@ -98,6 +98,9 @@ chef:
926 # to the install script
927 omnibus_version: "12.3.0"
928
929+ # If encrypted data bags are used, the client needs to have a secrets file
930+ # configured to decrypt them
931+ encrypted_data_bag_secret: "/etc/chef/encrypted_data_bag_secret"
932
933 # Capture all subprocess output into a logfile
934 # Useful for troubleshooting cloud-init issues
935diff --git a/doc/examples/cloud-config-disk-setup.txt b/doc/examples/cloud-config-disk-setup.txt
936index 43a62a2..89d9ff5 100644
937--- a/doc/examples/cloud-config-disk-setup.txt
938+++ b/doc/examples/cloud-config-disk-setup.txt
939@@ -17,7 +17,7 @@ fs_setup:
940 device: ephemeral0
941 partition: auto
942
943-# Default disk definitions for Windows Azure
944+# Default disk definitions for Microsoft Azure
945 # ------------------------------------------
946
947 device_aliases: {'ephemeral0': '/dev/sdb'}
948@@ -34,6 +34,21 @@ fs_setup:
949 replace_fs: ntfs
950
951
952+# Data disks definitions for Microsoft Azure
953+# ------------------------------------------
954+
955+disk_setup:
956+ /dev/disk/azure/scsi1/lun0:
957+ table_type: gpt
958+ layout: True
959+ overwrite: True
960+
961+fs_setup:
962+ - device: /dev/disk/azure/scsi1/lun0
963+ partition: 1
964+ filesystem: ext4
965+
966+
967 # Default disk definitions for SmartOS
968 # ------------------------------------
969
970@@ -242,7 +257,7 @@ fs_setup:
971 #
972 # "false": If an existing file system exists, skip the creation.
973 #
974-# <REPLACE_FS>: This is a special directive, used for Windows Azure that
975+# <REPLACE_FS>: This is a special directive, used for Microsoft Azure that
976 # instructs cloud-init to replace a file system of <FS_TYPE>. NOTE:
977 # unless you define a label, this requires the use of the 'any' partition
978 # directive.
979diff --git a/doc/rtd/topics/datasources/ec2.rst b/doc/rtd/topics/datasources/ec2.rst
980index 64c325d..76beca9 100644
981--- a/doc/rtd/topics/datasources/ec2.rst
982+++ b/doc/rtd/topics/datasources/ec2.rst
983@@ -90,4 +90,15 @@ An example configuration with the default values is provided below:
984 max_wait: 120
985 timeout: 50
986
987+Notes
988+-----
989+ * There are 2 types of EC2 instances network-wise: VPC ones (Virtual Private
990+ Cloud) and Classic ones (also known as non-VPC). One major difference
991+ between them is that Classic instances have their MAC address changed on
992+ stop/restart operations, so cloud-init will recreate the network config
993+ file for EC2 Classic instances every boot. On VPC instances this file is
994+ generated only in the first boot of the instance.
995+ The check for the instance type is performed by is_classic_instance()
996+ method.
997+
998 .. vi: textwidth=78
999diff --git a/doc/rtd/topics/instancedata.rst b/doc/rtd/topics/instancedata.rst
1000index 5d2dc94..231a008 100644
1001--- a/doc/rtd/topics/instancedata.rst
1002+++ b/doc/rtd/topics/instancedata.rst
1003@@ -4,7 +4,7 @@
1004 Instance Metadata
1005 *****************
1006
1007-What is a instance data?
1008+What is instance data?
1009 ========================
1010
1011 Instance data is the collection of all configuration data that cloud-init
1012diff --git a/doc/rtd/topics/merging.rst b/doc/rtd/topics/merging.rst
1013index c75ca59..5f7ca18 100644
1014--- a/doc/rtd/topics/merging.rst
1015+++ b/doc/rtd/topics/merging.rst
1016@@ -21,12 +21,12 @@ For example.
1017 .. code-block:: yaml
1018
1019 #cloud-config (1)
1020- run_cmd:
1021+ runcmd:
1022 - bash1
1023 - bash2
1024
1025 #cloud-config (2)
1026- run_cmd:
1027+ runcmd:
1028 - bash3
1029 - bash4
1030
1031@@ -36,7 +36,7 @@ cloud-config object that contains the following.
1032 .. code-block:: yaml
1033
1034 #cloud-config (merged)
1035- run_cmd:
1036+ runcmd:
1037 - bash3
1038 - bash4
1039
1040@@ -45,7 +45,7 @@ Typically this is not what users want; instead they would likely prefer:
1041 .. code-block:: yaml
1042
1043 #cloud-config (merged)
1044- run_cmd:
1045+ runcmd:
1046 - bash1
1047 - bash2
1048 - bash3
1049@@ -55,6 +55,45 @@ This way makes it easier to combine the various cloud-config objects you have
1050 into a more useful list, thus reducing duplication necessary to accomplish the
1051 same result with the previous method.
1052
1053+
1054+Built-in Mergers
1055+================
1056+
1057+Cloud-init provides merging for the following built-in types:
1058+
1059+- Dict
1060+- List
1061+- String
1062+
1063+The ``Dict`` merger has the following options which control what is done with
1064+values contained within the config.
1065+
1066+- ``allow_delete``: Existing values not present in the new value can be deleted, defaults to False
1067+- ``no_replace``: Do not replace an existing value if one is already present, enabled by default.
1068+- ``replace``: Overwrite existing values with new ones.
1069+
1070+The ``List`` merger has the following options which control what is done with
1071+the values contained within the config.
1072+
1073+- ``append``: Add new value to the end of the list, defaults to False.
1074+- ``prepend``: Add new values to the start of the list, defaults to False.
1075+- ``no_replace``: Do not replace an existing value if one is already present, enabled by default.
1076+- ``replace``: Overwrite existing values with new ones.
1077+
1078+The ``Str`` merger has the following options which control what is done with
1079+the values contained within the config.
1080+
1081+- ``append``: Add new value to the end of the string, defaults to False.
1082+
1083+Common options for all merge types which control how recursive merging is
1084+done on other types.
1085+
1086+- ``recurse_dict``: If True merge the new values of the dictionary, defaults to True.
1087+- ``recurse_list``: If True merge the new values of the list, defaults to False.
1088+- ``recurse_array``: Alias for ``recurse_list``.
1089+- ``recurse_str``: If True merge the new values of the string, defaults to False.
1090+
1091+
1092 Customizability
1093 ===============
1094
1095@@ -164,8 +203,8 @@ string format (i.e. the second option above), for example:
1096
1097 .. code-block:: python
1098
1099- {'merge_how': [{'name': 'list', 'settings': ['extend']},
1100- {'name': 'dict', 'settings': []},
1101+ {'merge_how': [{'name': 'list', 'settings': ['append']},
1102+ {'name': 'dict', 'settings': ['no_replace', 'recurse_list']},
1103 {'name': 'str', 'settings': ['append']}]}
1104
1105 This would be the equivalent format for default string format but in dictionary
1106@@ -201,4 +240,43 @@ Note, however, that merge algorithms are not used *across* types of
1107 configuration. As was the case before merging was implemented,
1108 user-data will overwrite conf.d configuration without merging.
1109
1110+Example cloud-config
1111+====================
1112+
1113+A common request is to include multiple ``runcmd`` directives in different
1114+files and merge all of the commands together. To achieve this, we must modify
1115+the default merging to allow for dictionaries to join list values.
1116+
1117+
1118+The first config
1119+
1120+.. code-block:: yaml
1121+
1122+ #cloud-config
1123+ merge_how:
1124+ - name: list
1125+ settings: [append]
1126+ - name: dict
1127+ settings: [no_replace, recurse_list]
1128+
1129+ runcmd:
1130+ - bash1
1131+ - bash2
1132+
1133+The second config
1134+
1135+.. code-block:: yaml
1136+
1137+ #cloud-config
1138+ merge_how:
1139+ - name: list
1140+ settings: [append]
1141+ - name: dict
1142+ settings: [no_replace, recurse_list]
1143+
1144+ runcmd:
1145+ - bash3
1146+ - bash4
1147+
1148+
1149 .. vi: textwidth=78
1150diff --git a/templates/chef_client.rb.tmpl b/templates/chef_client.rb.tmpl
1151index cbb6b15..99978d3 100644
1152--- a/templates/chef_client.rb.tmpl
1153+++ b/templates/chef_client.rb.tmpl
1154@@ -1,6 +1,6 @@
1155 ## template:jinja
1156 {#
1157-This file is only utilized if the module 'cc_chef' is enabled in
1158+This file is only utilized if the module 'cc_chef' is enabled in
1159 cloud-config. Specifically, in order to enable it
1160 you need to add the following to config:
1161 chef:
1162@@ -56,3 +56,6 @@ pid_file "{{pid_file}}"
1163 {% if show_time %}
1164 Chef::Log::Formatter.show_time = true
1165 {% endif %}
1166+{% if encrypted_data_bag_secret %}
1167+encrypted_data_bag_secret "{{encrypted_data_bag_secret}}"
1168+{% endif %}
1169diff --git a/tests/cloud_tests/verify.py b/tests/cloud_tests/verify.py
1170index 9911ecf..7018f4d 100644
1171--- a/tests/cloud_tests/verify.py
1172+++ b/tests/cloud_tests/verify.py
1173@@ -61,12 +61,17 @@ def format_test_failures(test_result):
1174 if not test_result['failures']:
1175 return ''
1176 failure_hdr = ' test failures:'
1177- failure_fmt = ' * {module}.{class}.{function}\n {error}'
1178+ failure_fmt = ' * {module}.{class}.{function}\n '
1179 output = []
1180 for failure in test_result['failures']:
1181 if not output:
1182 output = [failure_hdr]
1183- output.append(failure_fmt.format(**failure))
1184+ msg = failure_fmt.format(**failure)
1185+ if failure.get('error'):
1186+ msg += failure['error']
1187+ else:
1188+ msg += failure.get('traceback', '')
1189+ output.append(msg)
1190 return '\n'.join(output)
1191
1192
1193diff --git a/tests/data/azure/parse_certificates_fingerprints b/tests/data/azure/parse_certificates_fingerprints
1194new file mode 100644
1195index 0000000..f7293c5
1196--- /dev/null
1197+++ b/tests/data/azure/parse_certificates_fingerprints
1198@@ -0,0 +1,4 @@
1199+ECEDEB3B8488D31AF3BC4CCED493F64B7D27D7B1
1200+073E19D14D1C799224C6A0FD8DDAB6A8BF27D473
1201+4C16E7FAD6297D74A9B25EB8F0A12808CEBE293E
1202+929130695289B450FE45DCD5F6EF0CDE69865867
1203diff --git a/tests/data/azure/parse_certificates_pem b/tests/data/azure/parse_certificates_pem
1204new file mode 100644
1205index 0000000..3521ea3
1206--- /dev/null
1207+++ b/tests/data/azure/parse_certificates_pem
1208@@ -0,0 +1,152 @@
1209+Bag Attributes
1210+ localKeyID: 01 00 00 00
1211+ Microsoft CSP Name: Microsoft Enhanced Cryptographic Provider v1.0
1212+Key Attributes
1213+ X509v3 Key Usage: 10
1214+-----BEGIN PRIVATE KEY-----
1215+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDlEe5fUqwdrQTP
1216+W2oVlGK2f31q/8ULT8KmOTyUvL0RPdJQ69vvHOc5Q2CKg2eviHC2LWhF8WmpnZj6
1217+61RL0GeFGizwvU8Moebw5p3oqdcgoGpHVtxf+mr4QcWF58/Fwez0dA4hcsimVNBz
1218+eNpBBUIKNBMTBG+4d6hcQBUAGKUdGRcCGEyTqXLU0MgHjxC9JgVqWJl+X2LcAGj5
1219+7J+tGYGTLzKJmeCeGVNN5ZtJ0T85MYHCKQk1/FElK+Kq5akovXffQHjlnCPcx0NJ
1220+47NBjlPaFp2gjnAChn79bT4iCjOFZ9avWpqRpeU517UCnY7djOr3fuod/MSQyh3L
1221+Wuem1tWBAgMBAAECggEBAM4ZXQRs6Kjmo95BHGiAEnSqrlgX+dycjcBq3QPh8KZT
1222+nifqnf48XhnackENy7tWIjr3DctoUq4mOp8AHt77ijhqfaa4XSg7fwKeK9NLBGC5
1223+lAXNtAey0o2894/sKrd+LMkgphoYIUnuI4LRaGV56potkj/ZDP/GwTcG/R4SDnTn
1224+C1Nb05PNTAPQtPZrgPo7TdM6gGsTnFbVrYHQLyg2Sq/osHfF15YohB01esRLCAwb
1225+EF8JkRC4hWIZoV7BsyQ39232zAJQGGla7+wKFs3kObwh3VnFkQpT94KZnNiZuEfG
1226+x5pW4Pn3gXgNsftscXsaNe/M9mYZqo//Qw7NvUIvAvECgYEA9AVveyK0HOA06fhh
1227++3hUWdvw7Pbrl+e06jO9+bT1RjQMbHKyI60DZyVGuAySN86iChJRoJr5c6xj+iXU
1228+cR6BVJDjGH5t1tyiK2aYf6hEpK9/j8Z54UiVQ486zPP0PGfT2TO4lBLK+8AUmoaH
1229+gk21ul8QeVCeCJa/o+xEoRFvzcUCgYEA8FCbbvInrUtNY+9eKaUYoNodsgBVjm5X
1230+I0YPUL9D4d+1nvupHSV2NVmQl0w1RaJwrNTafrl5LkqjhQbmuWNta6QgfZzSA3LB
1231+lWXo1Mm0azKdcD3qMGbvn0Q3zU+yGNEgmB/Yju3/NtgYRG6tc+FCWRbPbiCnZWT8
1232+v3C2Y0XggI0CgYEA2/jCZBgGkTkzue5kNVJlh5OS/aog+pCvL6hxCtarfBuTT3ed
1233+Sje+p46cz3DVpmUpATc+Si8py7KNdYQAm/BJ2be6X+woi9Xcgo87zWgcaPCjZzId
1234+0I2jsIE/Gl6XvpRCDrxnGWRPgt3GNP4szbPLrDPiH9oie8+Y9eYYf7G+PZkCgYEA
1235+nRSzZOPYV4f/QDF4pVQLMykfe/iH9B/fyWjEHg3He19VQmRReIHCMMEoqBziPXAe
1236+onpHj8oAkeer1wpZyhhZr6CKtFDLXgGm09bXSC/IRMHC81klORovyzU2HHfZfCtG
1237+WOmIDnU2+0xpIGIP8sztJ3qnf97MTJSkOSadsWo9gwkCgYEAh5AQmJQmck88Dff2
1238+qIfJIX8d+BDw47BFJ89OmMFjGV8TNB+JO+AV4Vkodg4hxKpLqTFZTTUFgoYfy5u1
1239+1/BhAjpmCDCrzubCFhx+8VEoM2+2+MmnuQoMAm9+/mD/IidwRaARgXgvEmp7sfdt
1240+RyWd+p2lYvFkC/jORQtDMY4uW1o=
1241+-----END PRIVATE KEY-----
1242+Bag Attributes
1243+ localKeyID: 02 00 00 00
1244+ Microsoft CSP Name: Microsoft Strong Cryptographic Provider
1245+Key Attributes
1246+ X509v3 Key Usage: 10
1247+-----BEGIN PRIVATE KEY-----
1248+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDlQhPrZwVQYFV4
1249+FBc0H1iTXYaznMpwZvEITKtXWACzTdguUderEVOkXW3HTi5HvC2rMayt0nqo3zcd
1250+x1eGiqdjpZQ/wMrkz9wNEM/nNMsXntEwxk0jCVNKB/jz6vf+BOtrSI01SritAGZW
1251+dpKoTUyztT8C2mA3X6D8g3m4Dd07ltnzxaDqAQIU5jBHh3f/Q14tlPNZWUIiqVTC
1252+gDxgAe7MDmfs9h3CInTBX1XM5J4UsLTL23/padgeSvP5YF5qr1+0c7Tdftxr2lwA
1253+N3rLkisf5EiLAToVyJJlgP/exo2I8DaIKe7DZzD3Y1CrurOpkcMKYu5kM1Htlbua
1254+tDkAa2oDAgMBAAECggEAOvdueS9DyiMlCKAeQb1IQosdQOh0l0ma+FgEABC2CWhd
1255+0LgjQTBRM6cGO+urcq7/jhdWQ1UuUG4tVn71z7itCi/F/Enhxc2C22d2GhFVpWsn
1256+giSXJYpZ/mIjkdVfWNo6FRuRmmHwMys1p0qTOS+8qUJWhSzW75csqJZGgeUrAI61
1257+LBV5F0SGR7dR2xZfy7PeDs9xpD0QivDt5DpsZWPaPvw4QlhdLgw6/YU1h9vtm6ci
1258+xLjnPRLZ7JMpcQHO8dUDl6FiEI7yQ11BDm253VQAVMddYRPQABn7SpEF8kD/aZVh
1259+2Clvz61Rz80SKjPUthMPLWMCRp7zB0xDMzt3/1i+tQKBgQD6Ar1/oD3eFnRnpi4u
1260+n/hdHJtMuXWNfUA4dspNjP6WGOid9sgIeUUdif1XyVJ+afITzvgpWc7nUWIqG2bQ
1261+WxJ/4q2rjUdvjNXTy1voVungR2jD5WLQ9DKeaTR0yCliWlx4JgdPG7qGI5MMwsr+
1262+R/PUoUUhGeEX+o/sCSieO3iUrQKBgQDqwBEMvIdhAv/CK2sG3fsKYX8rFT55ZNX3
1263+Tix9DbUGY3wQColNuI8U1nDlxE9U6VOfT9RPqKelBLCgbzB23kdEJnjSlnqlTxrx
1264+E+Hkndyf2ckdJAR3XNxoQ6SRLJNBsgoBj/z5tlfZE9/Jc+uh0mYy3e6g6XCVPBcz
1265+MgoIc+ofbwKBgQCGQhZ1hR30N+bHCozeaPW9OvGDIE0qcEqeh9xYDRFilXnF6pK9
1266+SjJ9jG7KR8jPLiHb1VebDSl5O1EV/6UU2vNyTc6pw7LLCryBgkGW4aWy1WZDXNnW
1267+EG1meGS9GghvUss5kmJ2bxOZmV0Mi0brisQ8OWagQf+JGvtS7BAt+Q3l+QKBgAb9
1268+8YQPmXiqPjPqVyW9Ntz4SnFeEJ5NApJ7IZgX8GxgSjGwHqbR+HEGchZl4ncE/Bii
1269+qBA3Vcb0fM5KgYcI19aPzsl28fA6ivLjRLcqfIfGVNcpW3iyq13vpdctHLW4N9QU
1270+FdTaOYOds+ysJziKq8CYG6NvUIshXw+HTgUybqbBAoGBAIIOqcmmtgOClAwipA17
1271+dAHsI9Sjk+J0+d4JU6o+5TsmhUfUKIjXf5+xqJkJcQZMEe5GhxcCuYkgFicvh4Hz
1272+kv2H/EU35LcJTqC6KTKZOWIbGcn1cqsvwm3GQJffYDiO8fRZSwCaif2J3F2lfH4Y
1273+R/fA67HXFSTT+OncdRpY1NOn
1274+-----END PRIVATE KEY-----
1275+Bag Attributes: <Empty Attributes>
1276+subject=/CN=CRP/OU=AzureRT/O=Microsoft Corporation/L=Redmond/ST=WA/C=US
1277+issuer=/CN=Root Agency
1278+-----BEGIN CERTIFICATE-----
1279+MIIB+TCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtSb290
1280+IEFnZW5jeTAeFw0xOTAyMTUxOTA0MDRaFw0yOTAyMTUxOTE0MDRaMGwxDDAKBgNV
1281+BAMMA0NSUDEQMA4GA1UECwwHQXp1cmVSVDEeMBwGA1UECgwVTWljcm9zb2Z0IENv
1282+cnBvcmF0aW9uMRAwDgYDVQQHDAdSZWRtb25kMQswCQYDVQQIDAJXQTELMAkGA1UE
1283+BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIlPjJXzrRih4C
1284+k/XsoI01oqo7IUxH3dA2F7vHGXQoIpKCp8Qe6Z6cFfdD8Uj+s+B1BX6hngwzIwjN
1285+jE/23X3SALVzJVWzX4Y/IEjbgsuao6sOyNyB18wIU9YzZkVGj68fmMlUw3LnhPbe
1286+eWkufZaJCaLyhQOwlRMbOcn48D6Ys8fccOyXNzpq3rH1OzeQpxS2M8zaJYP4/VZ/
1287+sf6KRpI7bP+QwyFvNKfhcaO9/gj4kMo9lVGjvDU20FW6g8UVNJCV9N4GO6mOcyqo
1288+OhuhVfjCNGgW7N1qi0TIVn0/MQM4l4dcT2R7Z/bV9fhMJLjGsy5A4TLAdRrhKUHT
1289+bzi9HyDvAgMBAAEwDQYJKoZIhvcNAQEFBQADAQA=
1290+-----END CERTIFICATE-----
1291+Bag Attributes
1292+ localKeyID: 01 00 00 00
1293+subject=/C=US/ST=WASHINGTON/L=Seattle/O=Microsoft/OU=Azure/CN=AnhVo/emailAddress=redacted@microsoft.com
1294+issuer=/C=US/ST=WASHINGTON/L=Seattle/O=Microsoft/OU=Azure/CN=AnhVo/emailAddress=redacted@microsoft.com
1295+-----BEGIN CERTIFICATE-----
1296+MIID7TCCAtWgAwIBAgIJALQS3yMg3R41MA0GCSqGSIb3DQEBCwUAMIGMMQswCQYD
1297+VQQGEwJVUzETMBEGA1UECAwKV0FTSElOR1RPTjEQMA4GA1UEBwwHU2VhdHRsZTES
1298+MBAGA1UECgwJTWljcm9zb2Z0MQ4wDAYDVQQLDAVBenVyZTEOMAwGA1UEAwwFQW5o
1299+Vm8xIjAgBgkqhkiG9w0BCQEWE2FuaHZvQG1pY3Jvc29mdC5jb20wHhcNMTkwMjE0
1300+MjMxMjQwWhcNMjExMTEwMjMxMjQwWjCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgM
1301+CldBU0hJTkdUT04xEDAOBgNVBAcMB1NlYXR0bGUxEjAQBgNVBAoMCU1pY3Jvc29m
1302+dDEOMAwGA1UECwwFQXp1cmUxDjAMBgNVBAMMBUFuaFZvMSIwIAYJKoZIhvcNAQkB
1303+FhNhbmh2b0BtaWNyb3NvZnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
1304+CgKCAQEA5RHuX1KsHa0Ez1tqFZRitn99av/FC0/Cpjk8lLy9ET3SUOvb7xznOUNg
1305+ioNnr4hwti1oRfFpqZ2Y+utUS9BnhRos8L1PDKHm8Oad6KnXIKBqR1bcX/pq+EHF
1306+hefPxcHs9HQOIXLIplTQc3jaQQVCCjQTEwRvuHeoXEAVABilHRkXAhhMk6ly1NDI
1307+B48QvSYFaliZfl9i3ABo+eyfrRmBky8yiZngnhlTTeWbSdE/OTGBwikJNfxRJSvi
1308+quWpKL1330B45Zwj3MdDSeOzQY5T2hadoI5wAoZ+/W0+IgozhWfWr1qakaXlOde1
1309+Ap2O3Yzq937qHfzEkMody1rnptbVgQIDAQABo1AwTjAdBgNVHQ4EFgQUPvdgLiv3
1310+pAk4r0QTPZU3PFOZJvgwHwYDVR0jBBgwFoAUPvdgLiv3pAk4r0QTPZU3PFOZJvgw
1311+DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAVUHZT+h9+uCPLTEl5IDg
1312+kqd9WpzXA7PJd/V+7DeDDTkEd06FIKTWZLfxLVVDjQJnQqubQb//e0zGu1qKbXnX
1313+R7xqWabGU4eyPeUFWddmt1OHhxKLU3HbJNJJdL6XKiQtpGGUQt/mqNQ/DEr6hhNF
1314+im5I79iA8H/dXA2gyZrj5Rxea4mtsaYO0mfp1NrFtJpAh2Djy4B1lBXBIv4DWG9e
1315+mMEwzcLCOZj2cOMA6+mdLMUjYCvIRtnn5MKUHyZX5EmX79wsqMTvVpddlVLB9Kgz
1316+Qnvft9+SBWh9+F3ip7BsL6Q4Q9v8eHRbnP0ya7ddlgh64uwf9VOfZZdKCnwqudJP
1317+3g==
1318+-----END CERTIFICATE-----
1319+Bag Attributes
1320+ localKeyID: 02 00 00 00
1321+subject=/CN=/subscriptions/redacted/resourcegroups/redacted/providers/Microsoft.Compute/virtualMachines/redacted
1322+issuer=/CN=Microsoft.ManagedIdentity
1323+-----BEGIN CERTIFICATE-----
1324+MIIDnTCCAoWgAwIBAgIUB2lauSRccvFkoJybUfIwOUqBN7MwDQYJKoZIhvcNAQEL
1325+BQAwJDEiMCAGA1UEAxMZTWljcm9zb2Z0Lk1hbmFnZWRJZGVudGl0eTAeFw0xOTAy
1326+MTUxOTA5MDBaFw0xOTA4MTQxOTA5MDBaMIGUMYGRMIGOBgNVBAMTgYYvc3Vic2Ny
1327+aXB0aW9ucy8yN2I3NTBjZC1lZDQzLTQyZmQtOTA0NC04ZDc1ZTEyNGFlNTUvcmVz
1328+b3VyY2Vncm91cHMvYW5oZXh0cmFzc2gvcHJvdmlkZXJzL01pY3Jvc29mdC5Db21w
1329+dXRlL3ZpcnR1YWxNYWNoaW5lcy9hbmh0ZXN0Y2VydDCCASIwDQYJKoZIhvcNAQEB
1330+BQADggEPADCCAQoCggEBAOVCE+tnBVBgVXgUFzQfWJNdhrOcynBm8QhMq1dYALNN
1331+2C5R16sRU6RdbcdOLke8LasxrK3SeqjfNx3HV4aKp2OllD/AyuTP3A0Qz+c0yxee
1332+0TDGTSMJU0oH+PPq9/4E62tIjTVKuK0AZlZ2kqhNTLO1PwLaYDdfoPyDebgN3TuW
1333+2fPFoOoBAhTmMEeHd/9DXi2U81lZQiKpVMKAPGAB7swOZ+z2HcIidMFfVczknhSw
1334+tMvbf+lp2B5K8/lgXmqvX7RztN1+3GvaXAA3esuSKx/kSIsBOhXIkmWA/97GjYjw
1335+Nogp7sNnMPdjUKu6s6mRwwpi7mQzUe2Vu5q0OQBragMCAwEAAaNWMFQwDgYDVR0P
1336+AQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYD
1337+VR0jBBgwFoAUOJvzEsriQWdJBndPrK+Me1bCPjYwDQYJKoZIhvcNAQELBQADggEB
1338+AFGP/g8o7Hv/to11M0UqfzJuW/AyH9RZtSRcNQFLZUndwweQ6fap8lFsA4REUdqe
1339+7Quqp5JNNY1XzKLWXMPoheIDH1A8FFXdsAroArzlNs9tO3TlIHE8A7HxEVZEmR4b
1340+7ZiixmkQPS2RkjEoV/GM6fheBrzuFn7X5kVZyE6cC5sfcebn8xhk3ZcXI0VmpdT0
1341+jFBsf5IvFCIXXLLhJI4KXc8VMoKFU1jT9na/jyaoGmfwovKj4ib8s2aiXGAp7Y38
1342+UCmY+bJapWom6Piy5Jzi/p/kzMVdJcSa+GqpuFxBoQYEVs2XYVl7cGu/wPM+NToC
1343+pkSoWwF1QAnHn0eokR9E1rU=
1344+-----END CERTIFICATE-----
1345+Bag Attributes: <Empty Attributes>
1346+subject=/CN=CRP/OU=AzureRT/O=Microsoft Corporation/L=Redmond/ST=WA/C=US
1347+issuer=/CN=Root Agency
1348+-----BEGIN CERTIFICATE-----
1349+MIIB+TCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtSb290
1350+IEFnZW5jeTAeFw0xOTAyMTUxOTA0MDRaFw0yOTAyMTUxOTE0MDRaMGwxDDAKBgNV
1351+BAMMA0NSUDEQMA4GA1UECwwHQXp1cmVSVDEeMBwGA1UECgwVTWljcm9zb2Z0IENv
1352+cnBvcmF0aW9uMRAwDgYDVQQHDAdSZWRtb25kMQswCQYDVQQIDAJXQTELMAkGA1UE
1353+BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHU9IDclbKVYVb
1354+Yuv0+zViX+wTwlKspslmy/uf3hkWLh7pyzyrq70S7qtSW2EGixUPxZS/R8pOLHoi
1355+nlKF9ILgj0gVTCJsSwnWpXRg3rhZwIVoYMHN50BHS1SqVD0lsWNMXmo76LoJcjmW
1356+vwIznvj5C/gnhU+K7+c3m7AlCyU2wjwpBAEYj7PQs6l/wTqpEiaqC5NytNBd7qp+
1357+lYYysVrpa1PFL0Nj4MMZARIfjkiJtL9qDhy9YZeJRQ6q/Fhz0kjvkZnfxixfKF4y
1358+WzOfhBrAtpF6oOnuYKk3hxjh9KjTTX4/U8zdLojalX09iyHyEjwJKGlGEpzh1aY7
1359+t5btUyvpAgMBAAEwDQYJKoZIhvcNAQEFBQADAQA=
1360+-----END CERTIFICATE-----
1361diff --git a/tests/data/azure/pubkey_extract_cert b/tests/data/azure/pubkey_extract_cert
1362new file mode 100644
1363index 0000000..ce9b852
1364--- /dev/null
1365+++ b/tests/data/azure/pubkey_extract_cert
1366@@ -0,0 +1,13 @@
1367+-----BEGIN CERTIFICATE-----
1368+MIIB+TCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtSb290
1369+IEFnZW5jeTAeFw0xOTAyMTUxOTA0MDRaFw0yOTAyMTUxOTE0MDRaMGwxDDAKBgNV
1370+BAMMA0NSUDEQMA4GA1UECwwHQXp1cmVSVDEeMBwGA1UECgwVTWljcm9zb2Z0IENv
1371+cnBvcmF0aW9uMRAwDgYDVQQHDAdSZWRtb25kMQswCQYDVQQIDAJXQTELMAkGA1UE
1372+BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHU9IDclbKVYVb
1373+Yuv0+zViX+wTwlKspslmy/uf3hkWLh7pyzyrq70S7qtSW2EGixUPxZS/R8pOLHoi
1374+nlKF9ILgj0gVTCJsSwnWpXRg3rhZwIVoYMHN50BHS1SqVD0lsWNMXmo76LoJcjmW
1375+vwIznvj5C/gnhU+K7+c3m7AlCyU2wjwpBAEYj7PQs6l/wTqpEiaqC5NytNBd7qp+
1376+lYYysVrpa1PFL0Nj4MMZARIfjkiJtL9qDhy9YZeJRQ6q/Fhz0kjvkZnfxixfKF4y
1377+WzOfhBrAtpF6oOnuYKk3hxjh9KjTTX4/U8zdLojalX09iyHyEjwJKGlGEpzh1aY7
1378+t5btUyvpAgMBAAEwDQYJKoZIhvcNAQEFBQADAQA=
1379+-----END CERTIFICATE-----
1380diff --git a/tests/data/azure/pubkey_extract_ssh_key b/tests/data/azure/pubkey_extract_ssh_key
1381new file mode 100644
1382index 0000000..54d749e
1383--- /dev/null
1384+++ b/tests/data/azure/pubkey_extract_ssh_key
1385@@ -0,0 +1 @@
1386+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHU9IDclbKVYVbYuv0+zViX+wTwlKspslmy/uf3hkWLh7pyzyrq70S7qtSW2EGixUPxZS/R8pOLHoinlKF9ILgj0gVTCJsSwnWpXRg3rhZwIVoYMHN50BHS1SqVD0lsWNMXmo76LoJcjmWvwIznvj5C/gnhU+K7+c3m7AlCyU2wjwpBAEYj7PQs6l/wTqpEiaqC5NytNBd7qp+lYYysVrpa1PFL0Nj4MMZARIfjkiJtL9qDhy9YZeJRQ6q/Fhz0kjvkZnfxixfKF4yWzOfhBrAtpF6oOnuYKk3hxjh9KjTTX4/U8zdLojalX09iyHyEjwJKGlGEpzh1aY7t5btUyvp
1387diff --git a/tests/data/netinfo/freebsd-ifconfig-output b/tests/data/netinfo/freebsd-ifconfig-output
1388new file mode 100644
1389index 0000000..3de15a5
1390--- /dev/null
1391+++ b/tests/data/netinfo/freebsd-ifconfig-output
1392@@ -0,0 +1,17 @@
1393+vtnet0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
1394+ options=6c07bb<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,JUMBO_MTU,VLAN_HWCSUM,TSO4,TSO6,LRO,VLAN_HWTSO,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
1395+ ether fa:16:3e:14:1f:99
1396+ hwaddr fa:16:3e:14:1f:99
1397+ inet 10.1.80.61 netmask 0xfffff000 broadcast 10.1.95.255
1398+ nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
1399+ media: Ethernet 10Gbase-T <full-duplex>
1400+ status: active
1401+pflog0: flags=0<> metric 0 mtu 33160
1402+pfsync0: flags=0<> metric 0 mtu 1500
1403+ syncpeer: 0.0.0.0 maxupd: 128 defer: off
1404+lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
1405+ options=600003<RXCSUM,TXCSUM,RXCSUM_IPV6,TXCSUM_IPV6>
1406+ inet6 ::1 prefixlen 128
1407+ inet6 fe80::1%lo0 prefixlen 64 scopeid 0x4
1408+ inet 127.0.0.1 netmask 0xff000000
1409+ nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
1410diff --git a/tests/data/netinfo/freebsd-netdev-formatted-output b/tests/data/netinfo/freebsd-netdev-formatted-output
1411new file mode 100644
1412index 0000000..a9d2ac1
1413--- /dev/null
1414+++ b/tests/data/netinfo/freebsd-netdev-formatted-output
1415@@ -0,0 +1,11 @@
1416++++++++++++++++++++++++++++++++Net device info+++++++++++++++++++++++++++++++
1417++---------+-------+----------------+------------+-------+-------------------+
1418+| Device | Up | Address | Mask | Scope | Hw-Address |
1419++---------+-------+----------------+------------+-------+-------------------+
1420+| lo0 | True | 127.0.0.1 | 0xff000000 | . | . |
1421+| lo0 | True | ::1/128 | . | . | . |
1422+| lo0 | True | fe80::1%lo0/64 | . | 0x4 | . |
1423+| pflog0 | False | . | . | . | . |
1424+| pfsync0 | False | . | . | . | . |
1425+| vtnet0 | True | 10.1.80.61 | 0xfffff000 | . | fa:16:3e:14:1f:99 |
1426++---------+-------+----------------+------------+-------+-------------------+
1427diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
1428index 417d86a..6b05b8f 100644
1429--- a/tests/unittests/test_datasource/test_azure.py
1430+++ b/tests/unittests/test_datasource/test_azure.py
1431@@ -11,7 +11,7 @@ from cloudinit.util import (b64e, decode_binary, load_file, write_file,
1432 from cloudinit.version import version_string as vs
1433 from cloudinit.tests.helpers import (
1434 HttprettyTestCase, CiTestCase, populate_dir, mock, wrap_and_call,
1435- ExitStack, PY26, SkipTest)
1436+ ExitStack)
1437
1438 import crypt
1439 import httpretty
1440@@ -221,8 +221,6 @@ class TestAzureDataSource(CiTestCase):
1441
1442 def setUp(self):
1443 super(TestAzureDataSource, self).setUp()
1444- if PY26:
1445- raise SkipTest("Does not work on python 2.6")
1446 self.tmp = self.tmp_dir()
1447
1448 # patch cloud_dir, so our 'seed_dir' is guaranteed empty
1449@@ -1692,6 +1690,7 @@ class TestPreprovisioningPollIMDS(CiTestCase):
1450 self.paths = helpers.Paths({'cloud_dir': self.tmp})
1451 dsaz.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d
1452
1453+ @mock.patch('time.sleep', mock.MagicMock())
1454 @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
1455 def test_poll_imds_re_dhcp_on_timeout(self, m_dhcpv4, report_ready_func,
1456 fake_resp, m_media_switch, m_dhcp,
1457diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py
1458index 26b2b93..0255616 100644
1459--- a/tests/unittests/test_datasource/test_azure_helper.py
1460+++ b/tests/unittests/test_datasource/test_azure_helper.py
1461@@ -1,11 +1,13 @@
1462 # This file is part of cloud-init. See LICENSE file for license information.
1463
1464 import os
1465+import unittest2
1466 from textwrap import dedent
1467
1468 from cloudinit.sources.helpers import azure as azure_helper
1469 from cloudinit.tests.helpers import CiTestCase, ExitStack, mock, populate_dir
1470
1471+from cloudinit.util import load_file
1472 from cloudinit.sources.helpers.azure import WALinuxAgentShim as wa_shim
1473
1474 GOAL_STATE_TEMPLATE = """\
1475@@ -289,6 +291,50 @@ class TestOpenSSLManager(CiTestCase):
1476 self.assertEqual([mock.call(manager.tmpdir)], del_dir.call_args_list)
1477
1478
1479+class TestOpenSSLManagerActions(CiTestCase):
1480+
1481+ def setUp(self):
1482+ super(TestOpenSSLManagerActions, self).setUp()
1483+
1484+ self.allowed_subp = True
1485+
1486+ def _data_file(self, name):
1487+ path = 'tests/data/azure'
1488+ return os.path.join(path, name)
1489+
1490+ @unittest2.skip("todo move to cloud_test")
1491+ def test_pubkey_extract(self):
1492+ cert = load_file(self._data_file('pubkey_extract_cert'))
1493+ good_key = load_file(self._data_file('pubkey_extract_ssh_key'))
1494+ sslmgr = azure_helper.OpenSSLManager()
1495+ key = sslmgr._get_ssh_key_from_cert(cert)
1496+ self.assertEqual(good_key, key)
1497+
1498+ good_fingerprint = '073E19D14D1C799224C6A0FD8DDAB6A8BF27D473'
1499+ fingerprint = sslmgr._get_fingerprint_from_cert(cert)
1500+ self.assertEqual(good_fingerprint, fingerprint)
1501+
1502+ @unittest2.skip("todo move to cloud_test")
1503+ @mock.patch.object(azure_helper.OpenSSLManager, '_decrypt_certs_from_xml')
1504+ def test_parse_certificates(self, mock_decrypt_certs):
1505+ """Azure control plane puts private keys as well as certificates
1506+ into the Certificates XML object. Make sure only the public keys
1507+ from certs are extracted and that fingerprints are converted to
1508+ the form specified in the ovf-env.xml file.
1509+ """
1510+ cert_contents = load_file(self._data_file('parse_certificates_pem'))
1511+ fingerprints = load_file(self._data_file(
1512+ 'parse_certificates_fingerprints')
1513+ ).splitlines()
1514+ mock_decrypt_certs.return_value = cert_contents
1515+ sslmgr = azure_helper.OpenSSLManager()
1516+ keys_by_fp = sslmgr.parse_certificates('')
1517+ for fp in keys_by_fp.keys():
1518+ self.assertIn(fp, fingerprints)
1519+ for fp in fingerprints:
1520+ self.assertIn(fp, keys_by_fp)
1521+
1522+
1523 class TestWALinuxAgentShim(CiTestCase):
1524
1525 def setUp(self):
1526@@ -329,18 +375,31 @@ class TestWALinuxAgentShim(CiTestCase):
1527
1528 def test_certificates_used_to_determine_public_keys(self):
1529 shim = wa_shim()
1530- data = shim.register_with_azure_and_fetch_data()
1531+ """if register_with_azure_and_fetch_data() isn't passed some info about
1532+ the user's public keys, there's no point in even trying to parse
1533+ the certificates
1534+ """
1535+ mypk = [{'fingerprint': 'fp1', 'path': 'path1'},
1536+ {'fingerprint': 'fp3', 'path': 'path3', 'value': ''}]
1537+ certs = {'fp1': 'expected-key',
1538+ 'fp2': 'should-not-be-found',
1539+ 'fp3': 'expected-no-value-key',
1540+ }
1541+ sslmgr = self.OpenSSLManager.return_value
1542+ sslmgr.parse_certificates.return_value = certs
1543+ data = shim.register_with_azure_and_fetch_data(pubkey_info=mypk)
1544 self.assertEqual(
1545 [mock.call(self.GoalState.return_value.certificates_xml)],
1546- self.OpenSSLManager.return_value.parse_certificates.call_args_list)
1547- self.assertEqual(
1548- self.OpenSSLManager.return_value.parse_certificates.return_value,
1549- data['public-keys'])
1550+ sslmgr.parse_certificates.call_args_list)
1551+ self.assertIn('expected-key', data['public-keys'])
1552+ self.assertIn('expected-no-value-key', data['public-keys'])
1553+ self.assertNotIn('should-not-be-found', data['public-keys'])
1554
1555 def test_absent_certificates_produces_empty_public_keys(self):
1556+ mypk = [{'fingerprint': 'fp1', 'path': 'path1'}]
1557 self.GoalState.return_value.certificates_xml = None
1558 shim = wa_shim()
1559- data = shim.register_with_azure_and_fetch_data()
1560+ data = shim.register_with_azure_and_fetch_data(pubkey_info=mypk)
1561 self.assertEqual([], data['public-keys'])
1562
1563 def test_correct_url_used_for_report_ready(self):
1564diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py
1565index dcdabea..520c50f 100644
1566--- a/tests/unittests/test_datasource/test_configdrive.py
1567+++ b/tests/unittests/test_datasource/test_configdrive.py
1568@@ -268,8 +268,7 @@ class TestConfigDriveDataSource(CiTestCase):
1569 exists_mock = mocks.enter_context(
1570 mock.patch.object(os.path, 'exists',
1571 side_effect=exists_side_effect()))
1572- device = cfg_ds.device_name_to_device(name)
1573- self.assertEqual(dev_name, device)
1574+ self.assertEqual(dev_name, cfg_ds.device_name_to_device(name))
1575
1576 find_mock.assert_called_once_with(mock.ANY)
1577 self.assertEqual(exists_mock.call_count, 2)
1578@@ -296,8 +295,7 @@ class TestConfigDriveDataSource(CiTestCase):
1579 exists_mock = mocks.enter_context(
1580 mock.patch.object(os.path, 'exists',
1581 return_value=True))
1582- device = cfg_ds.device_name_to_device(name)
1583- self.assertEqual(dev_name, device)
1584+ self.assertEqual(dev_name, cfg_ds.device_name_to_device(name))
1585
1586 find_mock.assert_called_once_with(mock.ANY)
1587 exists_mock.assert_called_once_with(mock.ANY)
1588@@ -331,8 +329,7 @@ class TestConfigDriveDataSource(CiTestCase):
1589 yield True
1590 with mock.patch.object(os.path, 'exists',
1591 side_effect=exists_side_effect()):
1592- device = cfg_ds.device_name_to_device(name)
1593- self.assertEqual(dev_name, device)
1594+ self.assertEqual(dev_name, cfg_ds.device_name_to_device(name))
1595 # We don't assert the call count for os.path.exists() because
1596 # not all of the entries in name_tests results in two calls to
1597 # that function. Specifically, 'root2k' doesn't seem to call
1598@@ -359,8 +356,7 @@ class TestConfigDriveDataSource(CiTestCase):
1599 }
1600 for name, dev_name in name_tests.items():
1601 with mock.patch.object(os.path, 'exists', return_value=True):
1602- device = cfg_ds.device_name_to_device(name)
1603- self.assertEqual(dev_name, device)
1604+ self.assertEqual(dev_name, cfg_ds.device_name_to_device(name))
1605
1606 def test_dir_valid(self):
1607 """Verify a dir is read as such."""
1608@@ -604,6 +600,9 @@ class TestNetJson(CiTestCase):
1609
1610
1611 class TestConvertNetworkData(CiTestCase):
1612+
1613+ with_logs = True
1614+
1615 def setUp(self):
1616 super(TestConvertNetworkData, self).setUp()
1617 self.tmp = self.tmp_dir()
1618@@ -730,6 +729,26 @@ class TestConvertNetworkData(CiTestCase):
1619 'enp0s2': 'fa:16:3e:d4:57:ad'}
1620 self.assertEqual(expected, config_name2mac)
1621
1622+ def test_unknown_device_types_accepted(self):
1623+ # If we don't recognise a link, we should treat it as physical for a
1624+ # best-effort boot
1625+ my_netdata = deepcopy(NETWORK_DATA)
1626+ my_netdata['links'][0]['type'] = 'my-special-link-type'
1627+
1628+ ncfg = openstack.convert_net_json(my_netdata, known_macs=KNOWN_MACS)
1629+ config_name2mac = {}
1630+ for n in ncfg['config']:
1631+ if n['type'] == 'physical':
1632+ config_name2mac[n['name']] = n['mac_address']
1633+
1634+ expected = {'nic0': 'fa:16:3e:05:30:fe', 'enp0s1': 'fa:16:3e:69:b0:58',
1635+ 'enp0s2': 'fa:16:3e:d4:57:ad'}
1636+ self.assertEqual(expected, config_name2mac)
1637+
1638+ # We should, however, warn the user that we don't recognise the type
1639+ self.assertIn('Unknown network_data link type (my-special-link-type)',
1640+ self.logs.getvalue())
1641+
1642
1643 def cfg_ds_from_dir(base_d, files=None):
1644 run = os.path.join(base_d, "run")
1645diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py
1646index 1a5956d..20d59bf 100644
1647--- a/tests/unittests/test_datasource/test_ec2.py
1648+++ b/tests/unittests/test_datasource/test_ec2.py
1649@@ -401,6 +401,30 @@ class TestEc2(test_helpers.HttprettyTestCase):
1650 ds.metadata = DEFAULT_METADATA
1651 self.assertEqual('my-identity-id', ds.get_instance_id())
1652
1653+ def test_classic_instance_true(self):
1654+ """If no vpc-id in metadata, is_classic_instance must return true."""
1655+ md_copy = copy.deepcopy(DEFAULT_METADATA)
1656+ ifaces_md = md_copy.get('network', {}).get('interfaces', {})
1657+ for _mac, mac_data in ifaces_md.get('macs', {}).items():
1658+ if 'vpc-id' in mac_data:
1659+ del mac_data['vpc-id']
1660+
1661+ ds = self._setup_ds(
1662+ platform_data=self.valid_platform_data,
1663+ sys_cfg={'datasource': {'Ec2': {'strict_id': False}}},
1664+ md={'md': md_copy})
1665+ self.assertTrue(ds.get_data())
1666+ self.assertTrue(ds.is_classic_instance())
1667+
1668+ def test_classic_instance_false(self):
1669+ """If vpc-id in metadata, is_classic_instance must return false."""
1670+ ds = self._setup_ds(
1671+ platform_data=self.valid_platform_data,
1672+ sys_cfg={'datasource': {'Ec2': {'strict_id': False}}},
1673+ md={'md': DEFAULT_METADATA})
1674+ self.assertTrue(ds.get_data())
1675+ self.assertFalse(ds.is_classic_instance())
1676+
1677 @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
1678 def test_valid_platform_with_strict_true(self, m_dhcp):
1679 """Valid platform data should return true with strict_id true."""
1680diff --git a/tests/unittests/test_distros/test_create_users.py b/tests/unittests/test_distros/test_create_users.py
1681index c3f258d..4062495 100644
1682--- a/tests/unittests/test_distros/test_create_users.py
1683+++ b/tests/unittests/test_distros/test_create_users.py
1684@@ -240,4 +240,32 @@ class TestCreateUser(CiTestCase):
1685 [mock.call(set(['auth1']), user), # not disabled
1686 mock.call(set(['key1']), 'foouser', options=disable_prefix)])
1687
1688+ @mock.patch("cloudinit.distros.util.which")
1689+ def test_lock_with_usermod_if_no_passwd(self, m_which, m_subp,
1690+ m_is_snappy):
1691+ """Lock uses usermod --lock if no 'passwd' cmd available."""
1692+ m_which.side_effect = lambda m: m in ('usermod',)
1693+ self.dist.lock_passwd("bob")
1694+ self.assertEqual(
1695+ [mock.call(['usermod', '--lock', 'bob'])],
1696+ m_subp.call_args_list)
1697+
1698+ @mock.patch("cloudinit.distros.util.which")
1699+ def test_lock_with_passwd_if_available(self, m_which, m_subp,
1700+ m_is_snappy):
1701+ """Lock with only passwd will use passwd."""
1702+ m_which.side_effect = lambda m: m in ('passwd',)
1703+ self.dist.lock_passwd("bob")
1704+ self.assertEqual(
1705+ [mock.call(['passwd', '-l', 'bob'])],
1706+ m_subp.call_args_list)
1707+
1708+ @mock.patch("cloudinit.distros.util.which")
1709+ def test_lock_raises_runtime_if_no_commands(self, m_which, m_subp,
1710+ m_is_snappy):
1711+ """Lock with no commands available raises RuntimeError."""
1712+ m_which.return_value = None
1713+ with self.assertRaises(RuntimeError):
1714+ self.dist.lock_passwd("bob")
1715+
1716 # vi: ts=4 expandtab
1717diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
1718index e986b59..e453040 100644
1719--- a/tests/unittests/test_distros/test_netconfig.py
1720+++ b/tests/unittests/test_distros/test_netconfig.py
1721@@ -407,7 +407,7 @@ class TestNetCfgDistroUbuntuNetplan(TestNetCfgDistroBase):
1722 self.assertEqual(0o644, get_mode(cfgpath, tmpd))
1723
1724 def netplan_path(self):
1725- return '/etc/netplan/50-cloud-init.yaml'
1726+ return '/etc/netplan/50-cloud-init.yaml'
1727
1728 def test_apply_network_config_v1_to_netplan_ub(self):
1729 expected_cfgs = {
1730diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
1731index 756b4fb..d00c1b4 100644
1732--- a/tests/unittests/test_ds_identify.py
1733+++ b/tests/unittests/test_ds_identify.py
1734@@ -441,7 +441,7 @@ class TestDsIdentify(DsIdentifyBase):
1735 nova does not identify itself on platforms other than intel.
1736 https://bugs.launchpad.net/cloud-init/+bugs?field.tag=dsid-nova"""
1737
1738- data = VALID_CFG['OpenStack'].copy()
1739+ data = copy.deepcopy(VALID_CFG['OpenStack'])
1740 del data['files'][P_PRODUCT_NAME]
1741 data.update({'policy_dmi': POLICY_FOUND_OR_MAYBE,
1742 'policy_no_dmi': POLICY_FOUND_OR_MAYBE})
1743diff --git a/tests/unittests/test_handler/test_handler_chef.py b/tests/unittests/test_handler/test_handler_chef.py
1744index b16532e..f431126 100644
1745--- a/tests/unittests/test_handler/test_handler_chef.py
1746+++ b/tests/unittests/test_handler/test_handler_chef.py
1747@@ -145,6 +145,7 @@ class TestChef(FilesystemMockingTestCase):
1748 file_backup_path "/var/backups/chef"
1749 pid_file "/var/run/chef/client.pid"
1750 Chef::Log::Formatter.show_time = true
1751+ encrypted_data_bag_secret "/etc/chef/encrypted_data_bag_secret"
1752 """
1753 tpl_file = util.load_file('templates/chef_client.rb.tmpl')
1754 self.patchUtils(self.tmp)
1755@@ -157,6 +158,8 @@ class TestChef(FilesystemMockingTestCase):
1756 'validation_name': 'bob',
1757 'validation_key': "/etc/chef/vkey.pem",
1758 'validation_cert': "this is my cert",
1759+ 'encrypted_data_bag_secret':
1760+ '/etc/chef/encrypted_data_bag_secret'
1761 },
1762 }
1763 cc_chef.handle('chef', cfg, self.fetch_cloud('ubuntu'), LOG, [])
1764diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
1765index e041e97..e3b9e02 100644
1766--- a/tests/unittests/test_net.py
1767+++ b/tests/unittests/test_net.py
1768@@ -19,6 +19,7 @@ import gzip
1769 import io
1770 import json
1771 import os
1772+import re
1773 import textwrap
1774 import yaml
1775
1776@@ -103,6 +104,326 @@ STATIC_EXPECTED_1 = {
1777 'address': '10.0.0.2'}],
1778 }
1779
1780+V1_NAMESERVER_ALIAS = """
1781+config:
1782+- id: eno1
1783+ mac_address: 08:94:ef:51:ae:e0
1784+ mtu: 1500
1785+ name: eno1
1786+ subnets:
1787+ - type: manual
1788+ type: physical
1789+- id: eno2
1790+ mac_address: 08:94:ef:51:ae:e1
1791+ mtu: 1500
1792+ name: eno2
1793+ subnets:
1794+ - type: manual
1795+ type: physical
1796+- id: eno3
1797+ mac_address: 08:94:ef:51:ae:de
1798+ mtu: 1500
1799+ name: eno3
1800+ subnets:
1801+ - type: manual
1802+ type: physical
1803+- bond_interfaces:
1804+ - eno1
1805+ - eno3
1806+ id: bondM
1807+ mac_address: 08:94:ef:51:ae:e0
1808+ mtu: 1500
1809+ name: bondM
1810+ params:
1811+ bond-downdelay: 0
1812+ bond-lacp-rate: fast
1813+ bond-miimon: 100
1814+ bond-mode: 802.3ad
1815+ bond-updelay: 0
1816+ bond-xmit-hash-policy: layer3+4
1817+ subnets:
1818+ - address: 10.101.10.47/23
1819+ gateway: 10.101.11.254
1820+ type: static
1821+ type: bond
1822+- id: eno4
1823+ mac_address: 08:94:ef:51:ae:df
1824+ mtu: 1500
1825+ name: eno4
1826+ subnets:
1827+ - type: manual
1828+ type: physical
1829+- id: enp0s20f0u1u6
1830+ mac_address: 0a:94:ef:51:a4:b9
1831+ mtu: 1500
1832+ name: enp0s20f0u1u6
1833+ subnets:
1834+ - type: manual
1835+ type: physical
1836+- id: enp216s0f0
1837+ mac_address: 68:05:ca:81:7c:e8
1838+ mtu: 9000
1839+ name: enp216s0f0
1840+ subnets:
1841+ - type: manual
1842+ type: physical
1843+- id: enp216s0f1
1844+ mac_address: 68:05:ca:81:7c:e9
1845+ mtu: 9000
1846+ name: enp216s0f1
1847+ subnets:
1848+ - type: manual
1849+ type: physical
1850+- id: enp47s0f0
1851+ mac_address: 68:05:ca:64:d3:6c
1852+ mtu: 9000
1853+ name: enp47s0f0
1854+ subnets:
1855+ - type: manual
1856+ type: physical
1857+- bond_interfaces:
1858+ - enp216s0f0
1859+ - enp47s0f0
1860+ id: bond0
1861+ mac_address: 68:05:ca:64:d3:6c
1862+ mtu: 9000
1863+ name: bond0
1864+ params:
1865+ bond-downdelay: 0
1866+ bond-lacp-rate: fast
1867+ bond-miimon: 100
1868+ bond-mode: 802.3ad
1869+ bond-updelay: 0
1870+ bond-xmit-hash-policy: layer3+4
1871+ subnets:
1872+ - type: manual
1873+ type: bond
1874+- id: bond0.3502
1875+ mtu: 9000
1876+ name: bond0.3502
1877+ subnets:
1878+ - address: 172.20.80.4/25
1879+ type: static
1880+ type: vlan
1881+ vlan_id: 3502
1882+ vlan_link: bond0
1883+- id: bond0.3503
1884+ mtu: 9000
1885+ name: bond0.3503
1886+ subnets:
1887+ - address: 172.20.80.129/25
1888+ type: static
1889+ type: vlan
1890+ vlan_id: 3503
1891+ vlan_link: bond0
1892+- id: enp47s0f1
1893+ mac_address: 68:05:ca:64:d3:6d
1894+ mtu: 9000
1895+ name: enp47s0f1
1896+ subnets:
1897+ - type: manual
1898+ type: physical
1899+- bond_interfaces:
1900+ - enp216s0f1
1901+ - enp47s0f1
1902+ id: bond1
1903+ mac_address: 68:05:ca:64:d3:6d
1904+ mtu: 9000
1905+ name: bond1
1906+ params:
1907+ bond-downdelay: 0
1908+ bond-lacp-rate: fast
1909+ bond-miimon: 100
1910+ bond-mode: 802.3ad
1911+ bond-updelay: 0
1912+ bond-xmit-hash-policy: layer3+4
1913+ subnets:
1914+ - address: 10.101.8.65/26
1915+ routes:
1916+ - destination: 213.119.192.0/24
1917+ gateway: 10.101.8.126
1918+ metric: 0
1919+ type: static
1920+ type: bond
1921+- address:
1922+ - 10.101.10.1
1923+ - 10.101.10.2
1924+ - 10.101.10.3
1925+ - 10.101.10.5
1926+ search:
1927+ - foo.bar
1928+ - maas
1929+ type: nameserver
1930+version: 1
1931+"""
1932+
1933+NETPLAN_NO_ALIAS = """
1934+network:
1935+ version: 2
1936+ ethernets:
1937+ eno1:
1938+ match:
1939+ macaddress: 08:94:ef:51:ae:e0
1940+ mtu: 1500
1941+ set-name: eno1
1942+ eno2:
1943+ match:
1944+ macaddress: 08:94:ef:51:ae:e1
1945+ mtu: 1500
1946+ set-name: eno2
1947+ eno3:
1948+ match:
1949+ macaddress: 08:94:ef:51:ae:de
1950+ mtu: 1500
1951+ set-name: eno3
1952+ eno4:
1953+ match:
1954+ macaddress: 08:94:ef:51:ae:df
1955+ mtu: 1500
1956+ set-name: eno4
1957+ enp0s20f0u1u6:
1958+ match:
1959+ macaddress: 0a:94:ef:51:a4:b9
1960+ mtu: 1500
1961+ set-name: enp0s20f0u1u6
1962+ enp216s0f0:
1963+ match:
1964+ macaddress: 68:05:ca:81:7c:e8
1965+ mtu: 9000
1966+ set-name: enp216s0f0
1967+ enp216s0f1:
1968+ match:
1969+ macaddress: 68:05:ca:81:7c:e9
1970+ mtu: 9000
1971+ set-name: enp216s0f1
1972+ enp47s0f0:
1973+ match:
1974+ macaddress: 68:05:ca:64:d3:6c
1975+ mtu: 9000
1976+ set-name: enp47s0f0
1977+ enp47s0f1:
1978+ match:
1979+ macaddress: 68:05:ca:64:d3:6d
1980+ mtu: 9000
1981+ set-name: enp47s0f1
1982+ bonds:
1983+ bond0:
1984+ interfaces:
1985+ - enp216s0f0
1986+ - enp47s0f0
1987+ macaddress: 68:05:ca:64:d3:6c
1988+ mtu: 9000
1989+ parameters:
1990+ down-delay: 0
1991+ lacp-rate: fast
1992+ mii-monitor-interval: 100
1993+ mode: 802.3ad
1994+ transmit-hash-policy: layer3+4
1995+ up-delay: 0
1996+ bond1:
1997+ addresses:
1998+ - 10.101.8.65/26
1999+ interfaces:
2000+ - enp216s0f1
2001+ - enp47s0f1
2002+ macaddress: 68:05:ca:64:d3:6d
2003+ mtu: 9000
2004+ nameservers:
2005+ addresses:
2006+ - 10.101.10.1
2007+ - 10.101.10.2
2008+ - 10.101.10.3
2009+ - 10.101.10.5
2010+ search:
2011+ - foo.bar
2012+ - maas
2013+ parameters:
2014+ down-delay: 0
2015+ lacp-rate: fast
2016+ mii-monitor-interval: 100
2017+ mode: 802.3ad
2018+ transmit-hash-policy: layer3+4
2019+ up-delay: 0
2020+ routes:
2021+ - metric: 0
2022+ to: 213.119.192.0/24
2023+ via: 10.101.8.126
2024+ bondM:
2025+ addresses:
2026+ - 10.101.10.47/23
2027+ gateway4: 10.101.11.254
2028+ interfaces:
2029+ - eno1
2030+ - eno3
2031+ macaddress: 08:94:ef:51:ae:e0
2032+ mtu: 1500
2033+ nameservers:
2034+ addresses:
2035+ - 10.101.10.1
2036+ - 10.101.10.2
2037+ - 10.101.10.3
2038+ - 10.101.10.5
2039+ search:
2040+ - foo.bar
2041+ - maas
2042+ parameters:
2043+ down-delay: 0
2044+ lacp-rate: fast
2045+ mii-monitor-interval: 100
2046+ mode: 802.3ad
2047+ transmit-hash-policy: layer3+4
2048+ up-delay: 0
2049+ vlans:
2050+ bond0.3502:
2051+ addresses:
2052+ - 172.20.80.4/25
2053+ id: 3502
2054+ link: bond0
2055+ mtu: 9000
2056+ nameservers:
2057+ addresses:
2058+ - 10.101.10.1
2059+ - 10.101.10.2
2060+ - 10.101.10.3
2061+ - 10.101.10.5
2062+ search:
2063+ - foo.bar
2064+ - maas
2065+ bond0.3503:
2066+ addresses:
2067+ - 172.20.80.129/25
2068+ id: 3503
2069+ link: bond0
2070+ mtu: 9000
2071+ nameservers:
2072+ addresses:
2073+ - 10.101.10.1
2074+ - 10.101.10.2
2075+ - 10.101.10.3
2076+ - 10.101.10.5
2077+ search:
2078+ - foo.bar
2079+ - maas
2080+"""
2081+
2082+NETPLAN_DHCP_FALSE = """
2083+version: 2
2084+ethernets:
2085+ ens3:
2086+ match:
2087+ macaddress: 52:54:00:ab:cd:ef
2088+ dhcp4: false
2089+ dhcp6: false
2090+ addresses:
2091+ - 192.168.42.100/24
2092+ - 2001:db8::100/32
2093+ gateway4: 192.168.42.1
2094+ gateway6: 2001:db8::1
2095+ nameservers:
2096+ search: [example.com]
2097+ addresses: [192.168.42.53, 1.1.1.1]
2098+"""
2099+
2100 # Examples (and expected outputs for various renderers).
2101 OS_SAMPLES = [
2102 {
2103@@ -2286,6 +2607,50 @@ USERCTL=no
2104 config = sysconfig.ConfigObj(nm_cfg)
2105 self.assertIn('ifcfg-rh', config['main']['plugins'])
2106
2107+ def test_netplan_dhcp_false_disable_dhcp_in_state(self):
2108+ """netplan config with dhcp[46]: False should not add dhcp in state"""
2109+ net_config = yaml.load(NETPLAN_DHCP_FALSE)
2110+ ns = network_state.parse_net_config_data(net_config,
2111+ skip_broken=False)
2112+
2113+ dhcp_found = [snet for iface in ns.iter_interfaces()
2114+ for snet in iface['subnets'] if 'dhcp' in snet['type']]
2115+
2116+ self.assertEqual([], dhcp_found)
2117+
2118+ def test_netplan_dhcp_false_no_dhcp_in_sysconfig(self):
2119+ """netplan cfg with dhcp[46]: False should not have bootproto=dhcp"""
2120+
2121+ entry = {
2122+ 'yaml': NETPLAN_DHCP_FALSE,
2123+ 'expected_sysconfig': {
2124+ 'ifcfg-ens3': textwrap.dedent("""\
2125+ BOOTPROTO=none
2126+ DEFROUTE=yes
2127+ DEVICE=ens3
2128+ DNS1=192.168.42.53
2129+ DNS2=1.1.1.1
2130+ DOMAIN=example.com
2131+ GATEWAY=192.168.42.1
2132+ HWADDR=52:54:00:ab:cd:ef
2133+ IPADDR=192.168.42.100
2134+ IPV6ADDR=2001:db8::100/32
2135+ IPV6INIT=yes
2136+ IPV6_DEFAULTGW=2001:db8::1
2137+ NETMASK=255.255.255.0
2138+ NM_CONTROLLED=no
2139+ ONBOOT=yes
2140+ STARTMODE=auto
2141+ TYPE=Ethernet
2142+ USERCTL=no
2143+ """),
2144+ }
2145+ }
2146+
2147+ found = self._render_and_read(network_config=yaml.load(entry['yaml']))
2148+ self._compare_files_to_expected(entry['expected_sysconfig'], found)
2149+ self._assert_headers(found)
2150+
2151
2152 class TestOpenSuseSysConfigRendering(CiTestCase):
2153
2154@@ -3065,6 +3430,38 @@ class TestNetplanRoundTrip(CiTestCase):
2155 entry['expected_netplan'].splitlines(),
2156 files['/etc/netplan/50-cloud-init.yaml'].splitlines())
2157
2158+ def test_render_output_has_yaml_no_aliases(self):
2159+ entry = {
2160+ 'yaml': V1_NAMESERVER_ALIAS,
2161+ 'expected_netplan': NETPLAN_NO_ALIAS,
2162+ }
2163+ network_config = yaml.load(entry['yaml'])
2164+ ns = network_state.parse_net_config_data(network_config)
2165+ files = self._render_and_read(state=ns)
2166+ # check for alias
2167+ content = files['/etc/netplan/50-cloud-init.yaml']
2168+
2169+ # test load the yaml to ensure we don't render something not loadable
2170+ # this allows single aliases, but not duplicate ones
2171+ parsed = yaml.load(files['/etc/netplan/50-cloud-init.yaml'])
2172+ self.assertNotEqual(None, parsed)
2173+
2174+ # now look for any alias, avoid rendering them entirely
2175+ # generate the first anchor string using the template
2176+ # as of this writing, looks like "&id001"
2177+ anchor = r'&' + yaml.serializer.Serializer.ANCHOR_TEMPLATE % 1
2178+ found_alias = re.search(anchor, content, re.MULTILINE)
2179+ if found_alias:
2180+ msg = "Error at: %s\nContent:\n%s" % (found_alias, content)
2181+ raise ValueError('Found yaml alias in rendered netplan: ' + msg)
2182+
2183+ print(entry['expected_netplan'])
2184+ print('-- expected ^ | v rendered --')
2185+ print(files['/etc/netplan/50-cloud-init.yaml'])
2186+ self.assertEqual(
2187+ entry['expected_netplan'].splitlines(),
2188+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
2189+
2190
2191 class TestEniRoundTrip(CiTestCase):
2192
2193diff --git a/tools/cloud-init-per b/tools/cloud-init-per
2194index 7d6754b..eae3e93 100755
2195--- a/tools/cloud-init-per
2196+++ b/tools/cloud-init-per
2197@@ -38,7 +38,7 @@ fi
2198 [ "$1" = "-h" -o "$1" = "--help" ] && { Usage ; exit 0; }
2199 [ $# -ge 3 ] || { Usage 1>&2; exit 1; }
2200 freq=$1
2201-name=$2
2202+name=${2/-/_}
2203 shift 2;
2204
2205 [ "${name#*/}" = "${name}" ] || fail "name cannot contain a /"
2206@@ -53,6 +53,12 @@ esac
2207 [ -d "${sem%/*}" ] || mkdir -p "${sem%/*}" ||
2208 fail "failed to make directory for ${sem}"
2209
2210+# Rename legacy sem files with dashes in their names. Do not overwrite existing
2211+# sem files to prevent clobbering those which may have been created from calls
2212+# outside of cloud-init.
2213+sem_legacy="${sem/_/-}"
2214+[ "$sem" != "$sem_legacy" -a -e "$sem_legacy" ] && mv -n "$sem_legacy" "$sem"
2215+
2216 [ "$freq" != "always" -a -e "$sem" ] && exit 0
2217 "$@"
2218 ret=$?

Subscribers

People subscribed via source and target branches