Merge ~raharper/cloud-init:ubuntu-devel-new-artful-release-v5 into cloud-init:ubuntu/devel

Proposed by Scott Moser on 2017-10-03
Status: Merged
Merged at revision: 39513265b4e37280f4a56040ae1029634444da19
Proposed branch: ~raharper/cloud-init:ubuntu-devel-new-artful-release-v5
Merge into: cloud-init:ubuntu/devel
Diff against target: 2252 lines (+1196/-148)
97 files modified
cloudinit/config/cc_ssh_authkey_fingerprints.py (+2/-2)
cloudinit/config/cc_zypper_add_repo.py (+218/-0)
cloudinit/net/dhcp.py (+42/-0)
cloudinit/net/tests/test_dhcp.py (+111/-2)
cloudinit/netinfo.py (+4/-4)
cloudinit/simpletable.py (+62/-0)
cloudinit/sources/DataSourceAltCloud.py (+2/-2)
cloudinit/sources/DataSourceCloudStack.py (+13/-4)
cloudinit/sources/DataSourceOVF.py (+47/-27)
cloudinit/sources/helpers/azure.py (+14/-6)
cloudinit/tests/helpers.py (+10/-0)
cloudinit/tests/test_simpletable.py (+100/-0)
config/cloud.cfg.tmpl (+3/-0)
debian/changelog (+25/-3)
debian/control (+0/-1)
packages/debian/copyright (+10/-15)
packages/pkg-deps.json (+0/-3)
requirements.txt (+0/-3)
systemd/cloud-final.service.tmpl (+3/-1)
tests/cloud_tests/__init__.py (+1/-1)
tests/cloud_tests/instances/nocloudkvm.py (+16/-15)
tests/cloud_tests/testcases/bugs/README.md (+0/-0)
tests/cloud_tests/testcases/bugs/lp1511485.yaml (+0/-0)
tests/cloud_tests/testcases/bugs/lp1611074.yaml (+0/-0)
tests/cloud_tests/testcases/bugs/lp1628337.yaml (+0/-0)
tests/cloud_tests/testcases/examples/README.md (+0/-0)
tests/cloud_tests/testcases/examples/TODO.md (+0/-0)
tests/cloud_tests/testcases/examples/add_apt_repositories.yaml (+0/-0)
tests/cloud_tests/testcases/examples/alter_completion_message.yaml (+0/-0)
tests/cloud_tests/testcases/examples/configure_instance_trusted_ca_certificates.yaml (+0/-0)
tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.yaml (+0/-0)
tests/cloud_tests/testcases/examples/including_user_groups.yaml (+0/-0)
tests/cloud_tests/testcases/examples/install_arbitrary_packages.yaml (+0/-0)
tests/cloud_tests/testcases/examples/install_run_chef_recipes.yaml (+0/-0)
tests/cloud_tests/testcases/examples/run_apt_upgrade.yaml (+0/-0)
tests/cloud_tests/testcases/examples/run_commands.yaml (+0/-0)
tests/cloud_tests/testcases/examples/run_commands_first_boot.yaml (+0/-0)
tests/cloud_tests/testcases/examples/setup_run_puppet.yaml (+0/-0)
tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.yaml (+0/-0)
tests/cloud_tests/testcases/main/README.md (+0/-0)
tests/cloud_tests/testcases/main/command_output_simple.yaml (+0/-0)
tests/cloud_tests/testcases/modules/README.md (+0/-0)
tests/cloud_tests/testcases/modules/TODO.md (+0/-0)
tests/cloud_tests/testcases/modules/apt_configure_conf.yaml (+0/-0)
tests/cloud_tests/testcases/modules/apt_configure_disable_suites.yaml (+0/-0)
tests/cloud_tests/testcases/modules/apt_configure_primary.yaml (+0/-0)
tests/cloud_tests/testcases/modules/apt_configure_proxy.yaml (+0/-0)
tests/cloud_tests/testcases/modules/apt_configure_security.yaml (+0/-0)
tests/cloud_tests/testcases/modules/apt_configure_sources_key.yaml (+0/-0)
tests/cloud_tests/testcases/modules/apt_configure_sources_keyserver.yaml (+0/-0)
tests/cloud_tests/testcases/modules/apt_configure_sources_list.yaml (+0/-0)
tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml (+0/-0)
tests/cloud_tests/testcases/modules/apt_pipelining_disable.yaml (+0/-0)
tests/cloud_tests/testcases/modules/apt_pipelining_os.yaml (+0/-0)
tests/cloud_tests/testcases/modules/bootcmd.yaml (+0/-0)
tests/cloud_tests/testcases/modules/byobu.yaml (+0/-0)
tests/cloud_tests/testcases/modules/ca_certs.yaml (+0/-0)
tests/cloud_tests/testcases/modules/debug_disable.yaml (+0/-0)
tests/cloud_tests/testcases/modules/debug_enable.yaml (+0/-0)
tests/cloud_tests/testcases/modules/final_message.yaml (+0/-0)
tests/cloud_tests/testcases/modules/keys_to_console.yaml (+0/-0)
tests/cloud_tests/testcases/modules/landscape.yaml (+0/-0)
tests/cloud_tests/testcases/modules/locale.yaml (+0/-0)
tests/cloud_tests/testcases/modules/lxd_bridge.yaml (+0/-0)
tests/cloud_tests/testcases/modules/lxd_dir.yaml (+0/-0)
tests/cloud_tests/testcases/modules/ntp.yaml (+0/-0)
tests/cloud_tests/testcases/modules/ntp_pools.yaml (+0/-0)
tests/cloud_tests/testcases/modules/ntp_servers.yaml (+0/-0)
tests/cloud_tests/testcases/modules/package_update_upgrade_install.yaml (+0/-0)
tests/cloud_tests/testcases/modules/runcmd.yaml (+0/-0)
tests/cloud_tests/testcases/modules/salt_minion.yaml (+0/-0)
tests/cloud_tests/testcases/modules/seed_random_command.yaml (+0/-0)
tests/cloud_tests/testcases/modules/seed_random_data.yaml (+0/-0)
tests/cloud_tests/testcases/modules/set_hostname.yaml (+0/-0)
tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml (+0/-0)
tests/cloud_tests/testcases/modules/set_password.yaml (+0/-0)
tests/cloud_tests/testcases/modules/set_password_expire.yaml (+0/-0)
tests/cloud_tests/testcases/modules/set_password_list.yaml (+0/-0)
tests/cloud_tests/testcases/modules/set_password_list_string.yaml (+0/-0)
tests/cloud_tests/testcases/modules/snappy.yaml (+0/-0)
tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml (+0/-0)
tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_enable.yaml (+0/-0)
tests/cloud_tests/testcases/modules/ssh_import_id.yaml (+0/-0)
tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml (+0/-0)
tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml (+0/-0)
tests/cloud_tests/testcases/modules/timezone.yaml (+0/-0)
tests/cloud_tests/testcases/modules/user_groups.yaml (+0/-0)
tests/cloud_tests/testcases/modules/write_files.yaml (+0/-0)
tests/unittests/test_datasource/test_altcloud.py (+2/-2)
tests/unittests/test_datasource/test_azure_helper.py (+95/-48)
tests/unittests/test_datasource/test_cloudstack.py (+7/-4)
tests/unittests/test_datasource/test_ovf.py (+164/-0)
tests/unittests/test_handler/test_handler_bootcmd.py (+1/-0)
tests/unittests/test_handler/test_handler_zypper_add_repo.py (+237/-0)
tests/unittests/test_handler/test_schema.py (+7/-1)
tools/build-on-freebsd (+0/-1)
tox.ini (+0/-3)
Reviewer Review Type Date Requested Status
Scott Moser Approve on 2017-10-03
Review via email: mp+331723@code.launchpad.net
To post a comment you must log in.
Scott Moser (smoser) wrote :

looks good, thank you Ryan.

review: Approve
Scott Moser (smoser) wrote :

I am going to make a single change to the number of characters in the hash.
for some reason (probably git-version related), Ryan's git-describe created a 7 digit hash, mine did/does an 8 digit.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/config/cc_ssh_authkey_fingerprints.py b/cloudinit/config/cc_ssh_authkey_fingerprints.py
2index 0066e97..35d8c57 100755
3--- a/cloudinit/config/cc_ssh_authkey_fingerprints.py
4+++ b/cloudinit/config/cc_ssh_authkey_fingerprints.py
5@@ -28,7 +28,7 @@ the keys can be specified, but defaults to ``md5``.
6 import base64
7 import hashlib
8
9-from prettytable import PrettyTable
10+from cloudinit.simpletable import SimpleTable
11
12 from cloudinit.distros import ug_util
13 from cloudinit import ssh_util
14@@ -74,7 +74,7 @@ def _pprint_key_entries(user, key_fn, key_entries, hash_meth='md5',
15 return
16 tbl_fields = ['Keytype', 'Fingerprint (%s)' % (hash_meth), 'Options',
17 'Comment']
18- tbl = PrettyTable(tbl_fields)
19+ tbl = SimpleTable(tbl_fields)
20 for entry in key_entries:
21 if _is_printable_key(entry):
22 row = []
23diff --git a/cloudinit/config/cc_zypper_add_repo.py b/cloudinit/config/cc_zypper_add_repo.py
24new file mode 100644
25index 0000000..aba2695
26--- /dev/null
27+++ b/cloudinit/config/cc_zypper_add_repo.py
28@@ -0,0 +1,218 @@
29+#
30+# Copyright (C) 2017 SUSE LLC.
31+#
32+# This file is part of cloud-init. See LICENSE file for license information.
33+
34+"""zypper_add_repo: Add zyper repositories to the system"""
35+
36+import configobj
37+import os
38+from six import string_types
39+from textwrap import dedent
40+
41+from cloudinit.config.schema import get_schema_doc
42+from cloudinit import log as logging
43+from cloudinit.settings import PER_ALWAYS
44+from cloudinit import util
45+
46+distros = ['opensuse', 'sles']
47+
48+schema = {
49+ 'id': 'cc_zypper_add_repo',
50+ 'name': 'ZypperAddRepo',
51+ 'title': 'Configure zypper behavior and add zypper repositories',
52+ 'description': dedent("""\
53+ Configure zypper behavior by modifying /etc/zypp/zypp.conf. The
54+ configuration writer is "dumb" and will simply append the provided
55+ configuration options to the configuration file. Option settings
56+ that may be duplicate will be resolved by the way the zypp.conf file
57+ is parsed. The file is in INI format.
58+ Add repositories to the system. No validation is performed on the
59+ repository file entries, it is assumed the user is familiar with
60+ the zypper repository file format."""),
61+ 'distros': distros,
62+ 'examples': [dedent("""\
63+ zypper:
64+ repos:
65+ - id: opensuse-oss
66+ name: os-oss
67+ baseurl: http://dl.opensuse.org/dist/leap/v/repo/oss/
68+ enabled: 1
69+ autorefresh: 1
70+ - id: opensuse-oss-update
71+ name: os-oss-up
72+ baseurl: http://dl.opensuse.org/dist/leap/v/update
73+ # any setting per
74+ # https://en.opensuse.org/openSUSE:Standards_RepoInfo
75+ # enable and autorefresh are on by default
76+ config:
77+ reposdir: /etc/zypp/repos.dir
78+ servicesdir: /etc/zypp/services.d
79+ download.use_deltarpm: true
80+ # any setting in /etc/zypp/zypp.conf
81+ """)],
82+ 'frequency': PER_ALWAYS,
83+ 'type': 'object',
84+ 'properties': {
85+ 'zypper': {
86+ 'type': 'object',
87+ 'properties': {
88+ 'repos': {
89+ 'type': 'array',
90+ 'items': {
91+ 'type': 'object',
92+ 'properties': {
93+ 'id': {
94+ 'type': 'string',
95+ 'description': dedent("""\
96+ The unique id of the repo, used when
97+ writing
98+ /etc/zypp/repos.d/<id>.repo.""")
99+ },
100+ 'baseurl': {
101+ 'type': 'string',
102+ 'format': 'uri', # built-in format type
103+ 'description': 'The base repositoy URL'
104+ }
105+ },
106+ 'required': ['id', 'baseurl'],
107+ 'additionalProperties': True
108+ },
109+ 'minItems': 1
110+ },
111+ 'config': {
112+ 'type': 'object',
113+ 'description': dedent("""\
114+ Any supported zypo.conf key is written to
115+ /etc/zypp/zypp.conf'""")
116+ }
117+ },
118+ 'required': [],
119+ 'minProperties': 1, # Either config or repo must be provided
120+ 'additionalProperties': False, # only repos and config allowed
121+ }
122+ }
123+}
124+
125+__doc__ = get_schema_doc(schema) # Supplement python help()
126+
127+LOG = logging.getLogger(__name__)
128+
129+
130+def _canonicalize_id(repo_id):
131+ repo_id = repo_id.replace(" ", "_")
132+ return repo_id
133+
134+
135+def _format_repo_value(val):
136+ if isinstance(val, bool):
137+ # zypp prefers 1/0
138+ return 1 if val else 0
139+ if isinstance(val, (list, tuple)):
140+ return "\n ".join([_format_repo_value(v) for v in val])
141+ if not isinstance(val, string_types):
142+ return str(val)
143+ return val
144+
145+
146+def _format_repository_config(repo_id, repo_config):
147+ to_be = configobj.ConfigObj()
148+ to_be[repo_id] = {}
149+ # Do basic translation of the items -> values
150+ for (k, v) in repo_config.items():
151+ # For now assume that people using this know the format
152+ # of zypper repos and don't verify keys/values further
153+ to_be[repo_id][k] = _format_repo_value(v)
154+ lines = to_be.write()
155+ return "\n".join(lines)
156+
157+
158+def _write_repos(repos, repo_base_path):
159+ """Write the user-provided repo definition files
160+ @param repos: A list of repo dictionary objects provided by the user's
161+ cloud config.
162+ @param repo_base_path: The directory path to which repo definitions are
163+ written.
164+ """
165+
166+ if not repos:
167+ return
168+ valid_repos = {}
169+ for index, user_repo_config in enumerate(repos):
170+ # Skip on absent required keys
171+ missing_keys = set(['id', 'baseurl']).difference(set(user_repo_config))
172+ if missing_keys:
173+ LOG.warning(
174+ "Repo config at index %d is missing required config keys: %s",
175+ index, ",".join(missing_keys))
176+ continue
177+ repo_id = user_repo_config.get('id')
178+ canon_repo_id = _canonicalize_id(repo_id)
179+ repo_fn_pth = os.path.join(repo_base_path, "%s.repo" % (canon_repo_id))
180+ if os.path.exists(repo_fn_pth):
181+ LOG.info("Skipping repo %s, file %s already exists!",
182+ repo_id, repo_fn_pth)
183+ continue
184+ elif repo_id in valid_repos:
185+ LOG.info("Skipping repo %s, file %s already pending!",
186+ repo_id, repo_fn_pth)
187+ continue
188+
189+ # Do some basic key formatting
190+ repo_config = dict(
191+ (k.lower().strip().replace("-", "_"), v)
192+ for k, v in user_repo_config.items()
193+ if k and k != 'id')
194+
195+ # Set defaults if not present
196+ for field in ['enabled', 'autorefresh']:
197+ if field not in repo_config:
198+ repo_config[field] = '1'
199+
200+ valid_repos[repo_id] = (repo_fn_pth, repo_config)
201+
202+ for (repo_id, repo_data) in valid_repos.items():
203+ repo_blob = _format_repository_config(repo_id, repo_data[-1])
204+ util.write_file(repo_data[0], repo_blob)
205+
206+
207+def _write_zypp_config(zypper_config):
208+ """Write to the default zypp configuration file /etc/zypp/zypp.conf"""
209+ if not zypper_config:
210+ return
211+ zypp_config = '/etc/zypp/zypp.conf'
212+ zypp_conf_content = util.load_file(zypp_config)
213+ new_settings = ['# Added via cloud.cfg']
214+ for setting, value in zypper_config.items():
215+ if setting == 'configdir':
216+ msg = 'Changing the location of the zypper configuration is '
217+ msg += 'not supported, skipping "configdir" setting'
218+ LOG.warning(msg)
219+ continue
220+ if value:
221+ new_settings.append('%s=%s' % (setting, value))
222+ if len(new_settings) > 1:
223+ new_config = zypp_conf_content + '\n'.join(new_settings)
224+ else:
225+ new_config = zypp_conf_content
226+ util.write_file(zypp_config, new_config)
227+
228+
229+def handle(name, cfg, _cloud, log, _args):
230+ zypper_section = cfg.get('zypper')
231+ if not zypper_section:
232+ LOG.debug(("Skipping module named %s,"
233+ " no 'zypper' relevant configuration found"), name)
234+ return
235+ repos = zypper_section.get('repos')
236+ if not repos:
237+ LOG.debug(("Skipping module named %s,"
238+ " no 'repos' configuration found"), name)
239+ return
240+ zypper_config = zypper_section.get('config', {})
241+ repo_base_path = zypper_config.get('reposdir', '/etc/zypp/repos.d/')
242+
243+ _write_zypp_config(zypper_config)
244+ _write_repos(repos, repo_base_path)
245+
246+# vi: ts=4 expandtab
247diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
248index 0535063..0cba703 100644
249--- a/cloudinit/net/dhcp.py
250+++ b/cloudinit/net/dhcp.py
251@@ -4,6 +4,7 @@
252 #
253 # This file is part of cloud-init. See LICENSE file for license information.
254
255+import configobj
256 import logging
257 import os
258 import re
259@@ -11,9 +12,12 @@ import re
260 from cloudinit.net import find_fallback_nic, get_devicelist
261 from cloudinit import temp_utils
262 from cloudinit import util
263+from six import StringIO
264
265 LOG = logging.getLogger(__name__)
266
267+NETWORKD_LEASES_DIR = '/run/systemd/netif/leases'
268+
269
270 class InvalidDHCPLeaseFileError(Exception):
271 """Raised when parsing an empty or invalid dhcp.leases file.
272@@ -118,4 +122,42 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir):
273 return parse_dhcp_lease_file(lease_file)
274
275
276+def networkd_parse_lease(content):
277+ """Parse a systemd lease file content as in /run/systemd/netif/leases/
278+
279+ Parse this (almost) ini style file even though it says:
280+ # This is private data. Do not parse.
281+
282+ Simply return a dictionary of key/values."""
283+
284+ return dict(configobj.ConfigObj(StringIO(content), list_values=False))
285+
286+
287+def networkd_load_leases(leases_d=None):
288+ """Return a dictionary of dictionaries representing each lease
289+ found in lease_d.i
290+
291+ The top level key will be the filename, which is typically the ifindex."""
292+
293+ if leases_d is None:
294+ leases_d = NETWORKD_LEASES_DIR
295+
296+ ret = {}
297+ if not os.path.isdir(leases_d):
298+ return ret
299+ for lfile in os.listdir(leases_d):
300+ ret[lfile] = networkd_parse_lease(
301+ util.load_file(os.path.join(leases_d, lfile)))
302+ return ret
303+
304+
305+def networkd_get_option_from_leases(keyname, leases_d=None):
306+ if leases_d is None:
307+ leases_d = NETWORKD_LEASES_DIR
308+ leases = networkd_load_leases(leases_d=leases_d)
309+ for ifindex, data in sorted(leases.items()):
310+ if data.get(keyname):
311+ return data[keyname]
312+ return None
313+
314 # vi: ts=4 expandtab
315diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
316index a38edae..1c1f504 100644
317--- a/cloudinit/net/tests/test_dhcp.py
318+++ b/cloudinit/net/tests/test_dhcp.py
319@@ -6,9 +6,9 @@ from textwrap import dedent
320
321 from cloudinit.net.dhcp import (
322 InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,
323- parse_dhcp_lease_file, dhcp_discovery)
324+ parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases)
325 from cloudinit.util import ensure_file, write_file
326-from cloudinit.tests.helpers import CiTestCase, wrap_and_call
327+from cloudinit.tests.helpers import CiTestCase, wrap_and_call, populate_dir
328
329
330 class TestParseDHCPLeasesFile(CiTestCase):
331@@ -149,3 +149,112 @@ class TestDHCPDiscoveryClean(CiTestCase):
332 [os.path.join(tmpdir, 'dhclient'), '-1', '-v', '-lf',
333 lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'),
334 'eth9', '-sf', '/bin/true'], capture=True)])
335+
336+
337+class TestSystemdParseLeases(CiTestCase):
338+
339+ lxd_lease = dedent("""\
340+ # This is private data. Do not parse.
341+ ADDRESS=10.75.205.242
342+ NETMASK=255.255.255.0
343+ ROUTER=10.75.205.1
344+ SERVER_ADDRESS=10.75.205.1
345+ NEXT_SERVER=10.75.205.1
346+ BROADCAST=10.75.205.255
347+ T1=1580
348+ T2=2930
349+ LIFETIME=3600
350+ DNS=10.75.205.1
351+ DOMAINNAME=lxd
352+ HOSTNAME=a1
353+ CLIENTID=ffe617693400020000ab110c65a6a0866931c2
354+ """)
355+
356+ lxd_parsed = {
357+ 'ADDRESS': '10.75.205.242',
358+ 'NETMASK': '255.255.255.0',
359+ 'ROUTER': '10.75.205.1',
360+ 'SERVER_ADDRESS': '10.75.205.1',
361+ 'NEXT_SERVER': '10.75.205.1',
362+ 'BROADCAST': '10.75.205.255',
363+ 'T1': '1580',
364+ 'T2': '2930',
365+ 'LIFETIME': '3600',
366+ 'DNS': '10.75.205.1',
367+ 'DOMAINNAME': 'lxd',
368+ 'HOSTNAME': 'a1',
369+ 'CLIENTID': 'ffe617693400020000ab110c65a6a0866931c2',
370+ }
371+
372+ azure_lease = dedent("""\
373+ # This is private data. Do not parse.
374+ ADDRESS=10.132.0.5
375+ NETMASK=255.255.255.255
376+ ROUTER=10.132.0.1
377+ SERVER_ADDRESS=169.254.169.254
378+ NEXT_SERVER=10.132.0.1
379+ MTU=1460
380+ T1=43200
381+ T2=75600
382+ LIFETIME=86400
383+ DNS=169.254.169.254
384+ NTP=169.254.169.254
385+ DOMAINNAME=c.ubuntu-foundations.internal
386+ DOMAIN_SEARCH_LIST=c.ubuntu-foundations.internal google.internal
387+ HOSTNAME=tribaal-test-171002-1349.c.ubuntu-foundations.internal
388+ ROUTES=10.132.0.1/32,0.0.0.0 0.0.0.0/0,10.132.0.1
389+ CLIENTID=ff405663a200020000ab11332859494d7a8b4c
390+ OPTION_245=624c3620
391+ """)
392+
393+ azure_parsed = {
394+ 'ADDRESS': '10.132.0.5',
395+ 'NETMASK': '255.255.255.255',
396+ 'ROUTER': '10.132.0.1',
397+ 'SERVER_ADDRESS': '169.254.169.254',
398+ 'NEXT_SERVER': '10.132.0.1',
399+ 'MTU': '1460',
400+ 'T1': '43200',
401+ 'T2': '75600',
402+ 'LIFETIME': '86400',
403+ 'DNS': '169.254.169.254',
404+ 'NTP': '169.254.169.254',
405+ 'DOMAINNAME': 'c.ubuntu-foundations.internal',
406+ 'DOMAIN_SEARCH_LIST': 'c.ubuntu-foundations.internal google.internal',
407+ 'HOSTNAME': 'tribaal-test-171002-1349.c.ubuntu-foundations.internal',
408+ 'ROUTES': '10.132.0.1/32,0.0.0.0 0.0.0.0/0,10.132.0.1',
409+ 'CLIENTID': 'ff405663a200020000ab11332859494d7a8b4c',
410+ 'OPTION_245': '624c3620'}
411+
412+ def setUp(self):
413+ super(TestSystemdParseLeases, self).setUp()
414+ self.lease_d = self.tmp_dir()
415+
416+ def test_no_leases_returns_empty_dict(self):
417+ """A leases dir with no lease files should return empty dictionary."""
418+ self.assertEqual({}, networkd_load_leases(self.lease_d))
419+
420+ def test_no_leases_dir_returns_empty_dict(self):
421+ """A non-existing leases dir should return empty dict."""
422+ enodir = os.path.join(self.lease_d, 'does-not-exist')
423+ self.assertEqual({}, networkd_load_leases(enodir))
424+
425+ def test_single_leases_file(self):
426+ """A leases dir with one leases file."""
427+ populate_dir(self.lease_d, {'2': self.lxd_lease})
428+ self.assertEqual(
429+ {'2': self.lxd_parsed}, networkd_load_leases(self.lease_d))
430+
431+ def test_single_azure_leases_file(self):
432+ """On Azure, option 245 should be present, verify it specifically."""
433+ populate_dir(self.lease_d, {'1': self.azure_lease})
434+ self.assertEqual(
435+ {'1': self.azure_parsed}, networkd_load_leases(self.lease_d))
436+
437+ def test_multiple_files(self):
438+ """Multiple leases files on azure with one found return that value."""
439+ self.maxDiff = None
440+ populate_dir(self.lease_d, {'1': self.azure_lease,
441+ '9': self.lxd_lease})
442+ self.assertEqual({'1': self.azure_parsed, '9': self.lxd_parsed},
443+ networkd_load_leases(self.lease_d))
444diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
445index 39c79de..8f99d99 100644
446--- a/cloudinit/netinfo.py
447+++ b/cloudinit/netinfo.py
448@@ -13,7 +13,7 @@ import re
449 from cloudinit import log as logging
450 from cloudinit import util
451
452-from prettytable import PrettyTable
453+from cloudinit.simpletable import SimpleTable
454
455 LOG = logging.getLogger()
456
457@@ -170,7 +170,7 @@ def netdev_pformat():
458 lines.append(util.center("Net device info failed", '!', 80))
459 else:
460 fields = ['Device', 'Up', 'Address', 'Mask', 'Scope', 'Hw-Address']
461- tbl = PrettyTable(fields)
462+ tbl = SimpleTable(fields)
463 for (dev, d) in netdev.items():
464 tbl.add_row([dev, d["up"], d["addr"], d["mask"], ".", d["hwaddr"]])
465 if d.get('addr6'):
466@@ -194,7 +194,7 @@ def route_pformat():
467 if routes.get('ipv4'):
468 fields_v4 = ['Route', 'Destination', 'Gateway',
469 'Genmask', 'Interface', 'Flags']
470- tbl_v4 = PrettyTable(fields_v4)
471+ tbl_v4 = SimpleTable(fields_v4)
472 for (n, r) in enumerate(routes.get('ipv4')):
473 route_id = str(n)
474 tbl_v4.add_row([route_id, r['destination'],
475@@ -207,7 +207,7 @@ def route_pformat():
476 if routes.get('ipv6'):
477 fields_v6 = ['Route', 'Proto', 'Recv-Q', 'Send-Q',
478 'Local Address', 'Foreign Address', 'State']
479- tbl_v6 = PrettyTable(fields_v6)
480+ tbl_v6 = SimpleTable(fields_v6)
481 for (n, r) in enumerate(routes.get('ipv6')):
482 route_id = str(n)
483 tbl_v6.add_row([route_id, r['proto'],
484diff --git a/cloudinit/simpletable.py b/cloudinit/simpletable.py
485new file mode 100644
486index 0000000..9060322
487--- /dev/null
488+++ b/cloudinit/simpletable.py
489@@ -0,0 +1,62 @@
490+# Copyright (C) 2017 Amazon.com, Inc. or its affiliates
491+#
492+# Author: Ethan Faust <efaust@amazon.com>
493+# Author: Andrew Jorgensen <ajorgens@amazon.com>
494+#
495+# This file is part of cloud-init. See LICENSE file for license information.
496+
497+
498+class SimpleTable(object):
499+ """A minimal implementation of PrettyTable
500+ for distribution with cloud-init.
501+ """
502+
503+ def __init__(self, fields):
504+ self.fields = fields
505+ self.rows = []
506+
507+ # initialize list of 0s the same length
508+ # as the number of fields
509+ self.column_widths = [0] * len(self.fields)
510+ self.update_column_widths(fields)
511+
512+ def update_column_widths(self, values):
513+ for i, value in enumerate(values):
514+ self.column_widths[i] = max(
515+ len(value),
516+ self.column_widths[i])
517+
518+ def add_row(self, values):
519+ if len(values) > len(self.fields):
520+ raise TypeError('too many values')
521+ values = [str(value) for value in values]
522+ self.rows.append(values)
523+ self.update_column_widths(values)
524+
525+ def _hdiv(self):
526+ """Returns a horizontal divider for the table."""
527+ return '+' + '+'.join(
528+ ['-' * (w + 2) for w in self.column_widths]) + '+'
529+
530+ def _row(self, row):
531+ """Returns a formatted row."""
532+ return '|' + '|'.join(
533+ [col.center(self.column_widths[i] + 2)
534+ for i, col in enumerate(row)]) + '|'
535+
536+ def __str__(self):
537+ """Returns a string representation of the table with lines around.
538+
539+ +-----+-----+
540+ | one | two |
541+ +-----+-----+
542+ | 1 | 2 |
543+ | 01 | 10 |
544+ +-----+-----+
545+ """
546+ lines = [self._hdiv(), self._row(self.fields), self._hdiv()]
547+ lines += [self._row(r) for r in self.rows] + [self._hdiv()]
548+ return '\n'.join(lines)
549+
550+ def get_string(self):
551+ return repr(self)
552diff --git a/cloudinit/sources/DataSourceAltCloud.py b/cloudinit/sources/DataSourceAltCloud.py
553index ed1d691..c78ad9e 100644
554--- a/cloudinit/sources/DataSourceAltCloud.py
555+++ b/cloudinit/sources/DataSourceAltCloud.py
556@@ -28,8 +28,8 @@ LOG = logging.getLogger(__name__)
557 CLOUD_INFO_FILE = '/etc/sysconfig/cloud-info'
558
559 # Shell command lists
560-CMD_PROBE_FLOPPY = ['/sbin/modprobe', 'floppy']
561-CMD_UDEVADM_SETTLE = ['/sbin/udevadm', 'settle', '--timeout=5']
562+CMD_PROBE_FLOPPY = ['modprobe', 'floppy']
563+CMD_UDEVADM_SETTLE = ['udevadm', 'settle', '--timeout=5']
564
565 META_DATA_NOT_SUPPORTED = {
566 'block-device-mapping': {},
567diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
568index 7e0f9bb..9dc473f 100644
569--- a/cloudinit/sources/DataSourceCloudStack.py
570+++ b/cloudinit/sources/DataSourceCloudStack.py
571@@ -19,6 +19,7 @@ import time
572
573 from cloudinit import ec2_utils as ec2
574 from cloudinit import log as logging
575+from cloudinit.net import dhcp
576 from cloudinit import sources
577 from cloudinit import url_helper as uhelp
578 from cloudinit import util
579@@ -224,20 +225,28 @@ def get_vr_address():
580 # Get the address of the virtual router via dhcp leases
581 # If no virtual router is detected, fallback on default gateway.
582 # See http://docs.cloudstack.apache.org/projects/cloudstack-administration/en/4.8/virtual_machines/user-data.html # noqa
583+
584+ # Try networkd first...
585+ latest_address = dhcp.networkd_get_option_from_leases('SERVER_ADDRESS')
586+ if latest_address:
587+ LOG.debug("Found SERVER_ADDRESS '%s' via networkd_leases",
588+ latest_address)
589+ return latest_address
590+
591+ # Try dhcp lease files next...
592 lease_file = get_latest_lease()
593 if not lease_file:
594 LOG.debug("No lease file found, using default gateway")
595 return get_default_gateway()
596
597- latest_address = None
598 with open(lease_file, "r") as fd:
599 for line in fd:
600 if "dhcp-server-identifier" in line:
601 words = line.strip(" ;\r\n").split(" ")
602 if len(words) > 2:
603- dhcp = words[2]
604- LOG.debug("Found DHCP identifier %s", dhcp)
605- latest_address = dhcp
606+ dhcptok = words[2]
607+ LOG.debug("Found DHCP identifier %s", dhcptok)
608+ latest_address = dhcptok
609 if not latest_address:
610 # No virtual router found, fallback on default gateway
611 LOG.debug("No DHCP found, using default gateway")
612diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py
613index 24b45d5..ccebf11 100644
614--- a/cloudinit/sources/DataSourceOVF.py
615+++ b/cloudinit/sources/DataSourceOVF.py
616@@ -375,26 +375,56 @@ def get_ovf_env(dirname):
617 return (None, False)
618
619
620-# Transport functions take no input and return
621-# a 3 tuple of content, path, filename
622-def transport_iso9660(require_iso=True):
623+def maybe_cdrom_device(devname):
624+ """Test if devname matches known list of devices which may contain iso9660
625+ filesystems.
626
627- # default_regex matches values in
628- # /lib/udev/rules.d/60-cdrom_id.rules
629- # KERNEL!="sr[0-9]*|hd[a-z]|xvd*", GOTO="cdrom_end"
630- envname = "CLOUD_INIT_CDROM_DEV_REGEX"
631- default_regex = "^(sr[0-9]+|hd[a-z]|xvd.*)"
632+ Be helpful in accepting either knames (with no leading /dev/) or full path
633+ names, but do not allow paths outside of /dev/, like /dev/foo/bar/xxx.
634+ """
635+ if not devname:
636+ return False
637+ elif not isinstance(devname, util.string_types):
638+ raise ValueError("Unexpected input for devname: %s" % devname)
639+
640+ # resolve '..' and multi '/' elements
641+ devname = os.path.normpath(devname)
642
643- devname_regex = os.environ.get(envname, default_regex)
644+ # drop leading '/dev/'
645+ if devname.startswith("/dev/"):
646+ # partition returns tuple (before, partition, after)
647+ devname = devname.partition("/dev/")[-1]
648+
649+ # ignore leading slash (/sr0), else fail on / in name (foo/bar/xvdc)
650+ if devname.startswith("/"):
651+ devname = devname.split("/")[-1]
652+ elif devname.count("/") > 0:
653+ return False
654+
655+ # if empty string
656+ if not devname:
657+ return False
658+
659+ # default_regex matches values in /lib/udev/rules.d/60-cdrom_id.rules
660+ # KERNEL!="sr[0-9]*|hd[a-z]|xvd*", GOTO="cdrom_end"
661+ default_regex = r"^(sr[0-9]+|hd[a-z]|xvd.*)"
662+ devname_regex = os.environ.get("CLOUD_INIT_CDROM_DEV_REGEX", default_regex)
663 cdmatch = re.compile(devname_regex)
664
665+ return cdmatch.match(devname) is not None
666+
667+
668+# Transport functions take no input and return
669+# a 3 tuple of content, path, filename
670+def transport_iso9660(require_iso=True):
671+
672 # Go through mounts to see if it was already mounted
673 mounts = util.mounts()
674 for (dev, info) in mounts.items():
675 fstype = info['fstype']
676 if fstype != "iso9660" and require_iso:
677 continue
678- if cdmatch.match(dev[5:]) is None: # take off '/dev/'
679+ if not maybe_cdrom_device(dev):
680 continue
681 mp = info['mountpoint']
682 (fname, contents) = get_ovf_env(mp)
683@@ -406,29 +436,19 @@ def transport_iso9660(require_iso=True):
684 else:
685 mtype = None
686
687- devs = os.listdir("/dev/")
688- devs.sort()
689+ # generate a list of devices with mtype filesystem, filter by regex
690+ devs = [dev for dev in
691+ util.find_devs_with("TYPE=%s" % mtype if mtype else None)
692+ if maybe_cdrom_device(dev)]
693 for dev in devs:
694- fullp = os.path.join("/dev/", dev)
695-
696- if (fullp in mounts or
697- not cdmatch.match(dev) or os.path.isdir(fullp)):
698- continue
699-
700- try:
701- # See if we can read anything at all...??
702- util.peek_file(fullp, 512)
703- except IOError:
704- continue
705-
706 try:
707- (fname, contents) = util.mount_cb(fullp, get_ovf_env, mtype=mtype)
708+ (fname, contents) = util.mount_cb(dev, get_ovf_env, mtype=mtype)
709 except util.MountFailedError:
710- LOG.debug("%s not mountable as iso9660", fullp)
711+ LOG.debug("%s not mountable as iso9660", dev)
712 continue
713
714 if contents is not False:
715- return (contents, fullp, fname)
716+ return (contents, dev, fname)
717
718 return (False, None, None)
719
720diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py
721index 28ed0ae..959b1bd 100644
722--- a/cloudinit/sources/helpers/azure.py
723+++ b/cloudinit/sources/helpers/azure.py
724@@ -8,6 +8,7 @@ import socket
725 import struct
726 import time
727
728+from cloudinit.net import dhcp
729 from cloudinit import stages
730 from cloudinit import temp_utils
731 from contextlib import contextmanager
732@@ -15,7 +16,6 @@ from xml.etree import ElementTree
733
734 from cloudinit import util
735
736-
737 LOG = logging.getLogger(__name__)
738
739
740@@ -239,6 +239,11 @@ class WALinuxAgentShim(object):
741 return socket.inet_ntoa(packed_bytes)
742
743 @staticmethod
744+ def _networkd_get_value_from_leases(leases_d=None):
745+ return dhcp.networkd_get_option_from_leases(
746+ 'OPTION_245', leases_d=leases_d)
747+
748+ @staticmethod
749 def _get_value_from_leases_file(fallback_lease_file):
750 leases = []
751 content = util.load_file(fallback_lease_file)
752@@ -287,12 +292,15 @@ class WALinuxAgentShim(object):
753
754 @staticmethod
755 def find_endpoint(fallback_lease_file=None):
756- LOG.debug('Finding Azure endpoint...')
757 value = None
758- # Option-245 stored in /run/cloud-init/dhclient.hooks/<ifc>.json
759- # a dhclient exit hook that calls cloud-init-dhclient-hook
760- dhcp_options = WALinuxAgentShim._load_dhclient_json()
761- value = WALinuxAgentShim._get_value_from_dhcpoptions(dhcp_options)
762+ LOG.debug('Finding Azure endpoint from networkd...')
763+ value = WALinuxAgentShim._networkd_get_value_from_leases()
764+ if value is None:
765+ # Option-245 stored in /run/cloud-init/dhclient.hooks/<ifc>.json
766+ # a dhclient exit hook that calls cloud-init-dhclient-hook
767+ LOG.debug('Finding Azure endpoint from hook json...')
768+ dhcp_options = WALinuxAgentShim._load_dhclient_json()
769+ value = WALinuxAgentShim._get_value_from_dhcpoptions(dhcp_options)
770 if value is None:
771 # Fallback and check the leases file if unsuccessful
772 LOG.debug("Unable to find endpoint in dhclient logs. "
773diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
774index 28e2662..6f88a5b 100644
775--- a/cloudinit/tests/helpers.py
776+++ b/cloudinit/tests/helpers.py
777@@ -104,6 +104,16 @@ class TestCase(unittest2.TestCase):
778 super(TestCase, self).setUp()
779 self.reset_global_state()
780
781+ def add_patch(self, target, attr, **kwargs):
782+ """Patches specified target object and sets it as attr on test
783+ instance also schedules cleanup"""
784+ if 'autospec' not in kwargs:
785+ kwargs['autospec'] = True
786+ m = mock.patch(target, **kwargs)
787+ p = m.start()
788+ self.addCleanup(m.stop)
789+ setattr(self, attr, p)
790+
791
792 class CiTestCase(TestCase):
793 """This is the preferred test case base class unless user
794diff --git a/cloudinit/tests/test_simpletable.py b/cloudinit/tests/test_simpletable.py
795new file mode 100644
796index 0000000..96bc24c
797--- /dev/null
798+++ b/cloudinit/tests/test_simpletable.py
799@@ -0,0 +1,100 @@
800+# Copyright (C) 2017 Amazon.com, Inc. or its affiliates
801+#
802+# Author: Andrew Jorgensen <ajorgens@amazon.com>
803+#
804+# This file is part of cloud-init. See LICENSE file for license information.
805+"""Tests that SimpleTable works just like PrettyTable for cloud-init.
806+
807+Not all possible PrettyTable cases are tested because we're not trying to
808+reimplement the entire library, only the minimal parts we actually use.
809+"""
810+
811+from cloudinit.simpletable import SimpleTable
812+from cloudinit.tests.helpers import CiTestCase
813+
814+# Examples rendered by cloud-init using PrettyTable
815+NET_DEVICE_FIELDS = (
816+ 'Device', 'Up', 'Address', 'Mask', 'Scope', 'Hw-Address')
817+NET_DEVICE_ROWS = (
818+ ('ens3', True, '172.31.4.203', '255.255.240.0', '.', '0a:1f:07:15:98:70'),
819+ ('ens3', True, 'fe80::81f:7ff:fe15:9870/64', '.', 'link',
820+ '0a:1f:07:15:98:70'),
821+ ('lo', True, '127.0.0.1', '255.0.0.0', '.', '.'),
822+ ('lo', True, '::1/128', '.', 'host', '.'),
823+)
824+NET_DEVICE_TABLE = """\
825++--------+------+----------------------------+---------------+-------+-------------------+
826+| Device | Up | Address | Mask | Scope | Hw-Address |
827++--------+------+----------------------------+---------------+-------+-------------------+
828+| ens3 | True | 172.31.4.203 | 255.255.240.0 | . | 0a:1f:07:15:98:70 |
829+| ens3 | True | fe80::81f:7ff:fe15:9870/64 | . | link | 0a:1f:07:15:98:70 |
830+| lo | True | 127.0.0.1 | 255.0.0.0 | . | . |
831+| lo | True | ::1/128 | . | host | . |
832++--------+------+----------------------------+---------------+-------+-------------------+""" # noqa: E501
833+ROUTE_IPV4_FIELDS = (
834+ 'Route', 'Destination', 'Gateway', 'Genmask', 'Interface', 'Flags')
835+ROUTE_IPV4_ROWS = (
836+ ('0', '0.0.0.0', '172.31.0.1', '0.0.0.0', 'ens3', 'UG'),
837+ ('1', '169.254.0.0', '0.0.0.0', '255.255.0.0', 'ens3', 'U'),
838+ ('2', '172.31.0.0', '0.0.0.0', '255.255.240.0', 'ens3', 'U'),
839+)
840+ROUTE_IPV4_TABLE = """\
841++-------+-------------+------------+---------------+-----------+-------+
842+| Route | Destination | Gateway | Genmask | Interface | Flags |
843++-------+-------------+------------+---------------+-----------+-------+
844+| 0 | 0.0.0.0 | 172.31.0.1 | 0.0.0.0 | ens3 | UG |
845+| 1 | 169.254.0.0 | 0.0.0.0 | 255.255.0.0 | ens3 | U |
846+| 2 | 172.31.0.0 | 0.0.0.0 | 255.255.240.0 | ens3 | U |
847++-------+-------------+------------+---------------+-----------+-------+"""
848+
849+AUTHORIZED_KEYS_FIELDS = (
850+ 'Keytype', 'Fingerprint (md5)', 'Options', 'Comment')
851+AUTHORIZED_KEYS_ROWS = (
852+ ('ssh-rsa', '24:c7:41:49:47:12:31:a0:de:6f:62:79:9b:13:06:36', '-',
853+ 'ajorgens'),
854+)
855+AUTHORIZED_KEYS_TABLE = """\
856++---------+-------------------------------------------------+---------+----------+
857+| Keytype | Fingerprint (md5) | Options | Comment |
858++---------+-------------------------------------------------+---------+----------+
859+| ssh-rsa | 24:c7:41:49:47:12:31:a0:de:6f:62:79:9b:13:06:36 | - | ajorgens |
860++---------+-------------------------------------------------+---------+----------+""" # noqa: E501
861+
862+# from prettytable import PrettyTable
863+# pt = PrettyTable(('HEADER',))
864+# print(pt)
865+NO_ROWS_FIELDS = ('HEADER',)
866+NO_ROWS_TABLE = """\
867++--------+
868+| HEADER |
869++--------+
870++--------+"""
871+
872+
873+class TestSimpleTable(CiTestCase):
874+
875+ def test_no_rows(self):
876+ """An empty table is rendered as PrettyTable would have done it."""
877+ table = SimpleTable(NO_ROWS_FIELDS)
878+ self.assertEqual(str(table), NO_ROWS_TABLE)
879+
880+ def test_net_dev(self):
881+ """Net device info is rendered as it was with PrettyTable."""
882+ table = SimpleTable(NET_DEVICE_FIELDS)
883+ for row in NET_DEVICE_ROWS:
884+ table.add_row(row)
885+ self.assertEqual(str(table), NET_DEVICE_TABLE)
886+
887+ def test_route_ipv4(self):
888+ """Route IPv4 info is rendered as it was with PrettyTable."""
889+ table = SimpleTable(ROUTE_IPV4_FIELDS)
890+ for row in ROUTE_IPV4_ROWS:
891+ table.add_row(row)
892+ self.assertEqual(str(table), ROUTE_IPV4_TABLE)
893+
894+ def test_authorized_keys(self):
895+ """SSH authorized keys are rendered as they were with PrettyTable."""
896+ table = SimpleTable(AUTHORIZED_KEYS_FIELDS)
897+ for row in AUTHORIZED_KEYS_ROWS:
898+ table.add_row(row)
899+ self.assertEqual(str(table), AUTHORIZED_KEYS_TABLE)
900diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl
901index 50e3bd8..32de9c9 100644
902--- a/config/cloud.cfg.tmpl
903+++ b/config/cloud.cfg.tmpl
904@@ -84,6 +84,9 @@ cloud_config_modules:
905 - apt-pipelining
906 - apt-configure
907 {% endif %}
908+{% if variant in ["suse"] %}
909+ - zypper-add-repo
910+{% endif %}
911 {% if variant not in ["freebsd"] %}
912 - ntp
913 {% endif %}
914diff --git a/debian/changelog b/debian/changelog
915index 2080def..1e1007d 100644
916--- a/debian/changelog
917+++ b/debian/changelog
918@@ -1,9 +1,31 @@
919-cloud-init (17.1-0ubuntu2) UNRELEASED; urgency=medium
920+cloud-init (17.1-13-g7fd0425-0ubuntu1) artful; urgency=medium
921
922 * debian/copyright: dep5 updates, reorganize, add Apache 2.0 license.
923 (LP: #1718681)
924-
925- -- Scott Moser <smoser@brickies.net> Fri, 29 Sep 2017 08:44:15 -0400
926+ * debian/control: drop dependency on python3-prettytable
927+ * New upstream snapshot.
928+ - systemd: remove limit on tasks created by cloud-init-final.service.
929+ [Robert Schweikert] (LP: #1717969)
930+ - suse: Support addition of zypper repos via cloud-config.
931+ [Robert Schweikert] (LP: #1718675)
932+ - tests: Combine integration configs and testcases [Joshua Powers]
933+ - Azure, CloudStack: Support reading dhcp options from systemd-networkd.
934+ [Dimitri John Ledkov] (LP: #1718029)
935+ - packages/debian/copyright: remove mention of boto and MIT license
936+ - systemd: only mention Before=apt-daily.service on debian based distros.
937+ [Robert Schweikert]
938+ - Add missing simpletable and simpletable tests for failed merge
939+ [Chad Smith]
940+ - Remove prettytable dependency, introduce simpletable [Andrew Jorgensen]
941+ - debian/copyright: dep5 updates, reorganize, add Apache 2.0 license.
942+ [Joshua Powers] (LP: #1718681)
943+ - tests: remove dependency on shlex [Joshua Powers]
944+ - AltCloud: Trust PATH for udevadm and modprobe.
945+ - DataSourceOVF: use util.find_devs_with(TYPE=iso9660)
946+ [Ryan Harper] (LP: #1718287)
947+ - tests: remove a temp file used in bootcmd tests.
948+
949+ -- Ryan Harper <ryan.harper@canonical.com> Tue, 03 Oct 2017 10:59:52 -0500
950
951 cloud-init (17.1-0ubuntu1) artful; urgency=medium
952
953diff --git a/debian/control b/debian/control
954index 731821e..3f46d7b 100644
955--- a/debian/control
956+++ b/debian/control
957@@ -19,7 +19,6 @@ Build-Depends: debhelper (>= 9),
958 python3-nose,
959 python3-oauthlib,
960 python3-pep8,
961- python3-prettytable,
962 python3-pyflakes | pyflakes (<< 1.1.0-2),
963 python3-requests,
964 python3-serial,
965diff --git a/packages/debian/copyright b/packages/debian/copyright
966index c9c7d23..598cda1 100644
967--- a/packages/debian/copyright
968+++ b/packages/debian/copyright
969@@ -1,33 +1,28 @@
970-Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135
971-Name: cloud-init
972-Maintainer: Scott Moser <scott.moser@canonical.com>
973+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
974+Upstream-Name: cloud-init
975+Upstream-Contact: cloud-init-dev@lists.launchpad.net
976 Source: https://launchpad.net/cloud-init
977
978-This package was debianized by Soren Hansen <soren@ubuntu.com> on
979-Thu, 04 Sep 2008 12:49:15 +0200 as ec2-init. It was later renamed to
980-cloud-init by Scott Moser <scott.moser@canonical.com>
981-
982-Upstream Author: Scott Moser <smoser@canonical.com>
983- Soren Hansen <soren@canonical.com>
984- Chuck Short <chuck.short@canonical.com>
985-
986-Copyright: 2010, Canonical Ltd.
987+Files: *
988+Copyright: 2010, Canonical Ltd.
989 License: GPL-3 or Apache-2.0
990+
991 License: GPL-3
992 This program is free software: you can redistribute it and/or modify
993 it under the terms of the GNU General Public License version 3, as
994 published by the Free Software Foundation.
995-
996+ .
997 This program is distributed in the hope that it will be useful,
998 but WITHOUT ANY WARRANTY; without even the implied warranty of
999 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1000 GNU General Public License for more details.
1001-
1002+ .
1003 You should have received a copy of the GNU General Public License
1004 along with this program. If not, see <http://www.gnu.org/licenses/>.
1005-
1006+ .
1007 The complete text of the GPL version 3 can be seen in
1008 /usr/share/common-licenses/GPL-3.
1009+
1010 License: Apache-2.0
1011 Licensed under the Apache License, Version 2.0 (the "License");
1012 you may not use this file except in compliance with the License.
1013diff --git a/packages/pkg-deps.json b/packages/pkg-deps.json
1014index 822d29d..72409dd 100644
1015--- a/packages/pkg-deps.json
1016+++ b/packages/pkg-deps.json
1017@@ -34,9 +34,6 @@
1018 "jsonschema" : {
1019 "3" : "python34-jsonschema"
1020 },
1021- "prettytable" : {
1022- "3" : "python34-prettytable"
1023- },
1024 "pyflakes" : {
1025 "2" : "pyflakes",
1026 "3" : "python34-pyflakes"
1027diff --git a/requirements.txt b/requirements.txt
1028index 61d1e90..dd10d85 100644
1029--- a/requirements.txt
1030+++ b/requirements.txt
1031@@ -3,9 +3,6 @@
1032 # Used for untemplating any files or strings with parameters.
1033 jinja2
1034
1035-# This is used for any pretty printing of tabular data.
1036-PrettyTable
1037-
1038 # This one is currently only used by the MAAS datasource. If that
1039 # datasource is removed, this is no longer needed
1040 oauthlib
1041diff --git a/systemd/cloud-final.service.tmpl b/systemd/cloud-final.service.tmpl
1042index fc01b89..8207b18 100644
1043--- a/systemd/cloud-final.service.tmpl
1044+++ b/systemd/cloud-final.service.tmpl
1045@@ -4,9 +4,10 @@ Description=Execute cloud user/final scripts
1046 After=network-online.target cloud-config.service rc-local.service
1047 {% if variant in ["ubuntu", "unknown", "debian"] %}
1048 After=multi-user.target
1049+Before=apt-daily.service
1050 {% endif %}
1051 Wants=network-online.target cloud-config.service
1052-Before=apt-daily.service
1053+
1054
1055 [Service]
1056 Type=oneshot
1057@@ -14,6 +15,7 @@ ExecStart=/usr/bin/cloud-init modules --mode=final
1058 RemainAfterExit=yes
1059 TimeoutSec=0
1060 KillMode=process
1061+TasksMax=infinity
1062
1063 # Output needs to appear in instance console output
1064 StandardOutput=journal+console
1065diff --git a/tests/cloud_tests/__init__.py b/tests/cloud_tests/__init__.py
1066index 07148c1..98c1d6c 100644
1067--- a/tests/cloud_tests/__init__.py
1068+++ b/tests/cloud_tests/__init__.py
1069@@ -7,7 +7,7 @@ import os
1070
1071 BASE_DIR = os.path.dirname(os.path.abspath(__file__))
1072 TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')
1073-TEST_CONF_DIR = os.path.join(BASE_DIR, 'configs')
1074+TEST_CONF_DIR = os.path.join(BASE_DIR, 'testcases')
1075 TREE_BASE = os.sep.join(BASE_DIR.split(os.sep)[:-2])
1076
1077
1078diff --git a/tests/cloud_tests/instances/nocloudkvm.py b/tests/cloud_tests/instances/nocloudkvm.py
1079index 7abfe73..8a0e531 100644
1080--- a/tests/cloud_tests/instances/nocloudkvm.py
1081+++ b/tests/cloud_tests/instances/nocloudkvm.py
1082@@ -4,7 +4,6 @@
1083
1084 import os
1085 import paramiko
1086-import shlex
1087 import socket
1088 import subprocess
1089 import time
1090@@ -83,10 +82,10 @@ class NoCloudKVMInstance(base.Instance):
1091
1092 def mount_image_callback(self, cmd):
1093 """Run mount-image-callback."""
1094- mic = ('sudo mount-image-callback --system-mounts --system-resolvconf '
1095- '%s -- chroot _MOUNTPOINT_ ' % self.name)
1096-
1097- out, err = c_util.subp(shlex.split(mic) + cmd)
1098+ out, err = c_util.subp(['sudo', 'mount-image-callback',
1099+ '--system-mounts', '--system-resolvconf',
1100+ self.name, '--', 'chroot',
1101+ '_MOUNTPOINT_'] + cmd)
1102
1103 return out, err
1104
1105@@ -122,11 +121,11 @@ class NoCloudKVMInstance(base.Instance):
1106 if self.pid:
1107 super(NoCloudKVMInstance, self).push_file()
1108 else:
1109- cmd = ("sudo mount-image-callback --system-mounts "
1110- "--system-resolvconf %s -- chroot _MOUNTPOINT_ "
1111- "/bin/sh -c 'cat - > %s'" % (self.name, remote_path))
1112 local_file = open(local_path)
1113- p = subprocess.Popen(shlex.split(cmd),
1114+ p = subprocess.Popen(['sudo', 'mount-image-callback',
1115+ '--system-mounts', '--system-resolvconf',
1116+ self.name, '--', 'chroot', '_MOUNTPOINT_',
1117+ '/bin/sh', '-c', 'cat - > %s' % remote_path],
1118 stdin=local_file,
1119 stdout=subprocess.PIPE,
1120 stderr=subprocess.PIPE)
1121@@ -186,12 +185,14 @@ class NoCloudKVMInstance(base.Instance):
1122 self.pid_file = os.path.join(tmpdir, '%s.pid' % self.name)
1123 self.ssh_port = self.get_free_port()
1124
1125- cmd = ('./tools/xkvm --disk %s,cache=unsafe --disk %s,cache=unsafe '
1126- '--netdev user,hostfwd=tcp::%s-:22 '
1127- '-- -pidfile %s -vnc none -m 2G -smp 2'
1128- % (self.name, seed, self.ssh_port, self.pid_file))
1129-
1130- subprocess.Popen(shlex.split(cmd), close_fds=True,
1131+ subprocess.Popen(['./tools/xkvm',
1132+ '--disk', '%s,cache=unsafe' % self.name,
1133+ '--disk', '%s,cache=unsafe' % seed,
1134+ '--netdev',
1135+ 'user,hostfwd=tcp::%s-:22' % self.ssh_port,
1136+ '--', '-pidfile', self.pid_file, '-vnc', 'none',
1137+ '-m', '2G', '-smp', '2'],
1138+ close_fds=True,
1139 stdin=subprocess.PIPE,
1140 stdout=subprocess.PIPE,
1141 stderr=subprocess.PIPE)
1142diff --git a/tests/cloud_tests/configs/bugs/README.md b/tests/cloud_tests/testcases/bugs/README.md
1143index 09ce076..09ce076 100644
1144--- a/tests/cloud_tests/configs/bugs/README.md
1145+++ b/tests/cloud_tests/testcases/bugs/README.md
1146diff --git a/tests/cloud_tests/configs/bugs/lp1511485.yaml b/tests/cloud_tests/testcases/bugs/lp1511485.yaml
1147index ebf9763..ebf9763 100644
1148--- a/tests/cloud_tests/configs/bugs/lp1511485.yaml
1149+++ b/tests/cloud_tests/testcases/bugs/lp1511485.yaml
1150diff --git a/tests/cloud_tests/configs/bugs/lp1611074.yaml b/tests/cloud_tests/testcases/bugs/lp1611074.yaml
1151index 960679d..960679d 100644
1152--- a/tests/cloud_tests/configs/bugs/lp1611074.yaml
1153+++ b/tests/cloud_tests/testcases/bugs/lp1611074.yaml
1154diff --git a/tests/cloud_tests/configs/bugs/lp1628337.yaml b/tests/cloud_tests/testcases/bugs/lp1628337.yaml
1155index e39b3cd..e39b3cd 100644
1156--- a/tests/cloud_tests/configs/bugs/lp1628337.yaml
1157+++ b/tests/cloud_tests/testcases/bugs/lp1628337.yaml
1158diff --git a/tests/cloud_tests/configs/examples/README.md b/tests/cloud_tests/testcases/examples/README.md
1159index 110a223..110a223 100644
1160--- a/tests/cloud_tests/configs/examples/README.md
1161+++ b/tests/cloud_tests/testcases/examples/README.md
1162diff --git a/tests/cloud_tests/configs/examples/TODO.md b/tests/cloud_tests/testcases/examples/TODO.md
1163index 8db0e98..8db0e98 100644
1164--- a/tests/cloud_tests/configs/examples/TODO.md
1165+++ b/tests/cloud_tests/testcases/examples/TODO.md
1166diff --git a/tests/cloud_tests/configs/examples/add_apt_repositories.yaml b/tests/cloud_tests/testcases/examples/add_apt_repositories.yaml
1167index 4b8575f..4b8575f 100644
1168--- a/tests/cloud_tests/configs/examples/add_apt_repositories.yaml
1169+++ b/tests/cloud_tests/testcases/examples/add_apt_repositories.yaml
1170diff --git a/tests/cloud_tests/configs/examples/alter_completion_message.yaml b/tests/cloud_tests/testcases/examples/alter_completion_message.yaml
1171index 9e154f8..9e154f8 100644
1172--- a/tests/cloud_tests/configs/examples/alter_completion_message.yaml
1173+++ b/tests/cloud_tests/testcases/examples/alter_completion_message.yaml
1174diff --git a/tests/cloud_tests/configs/examples/configure_instance_trusted_ca_certificates.yaml b/tests/cloud_tests/testcases/examples/configure_instance_trusted_ca_certificates.yaml
1175index ad32b08..ad32b08 100644
1176--- a/tests/cloud_tests/configs/examples/configure_instance_trusted_ca_certificates.yaml
1177+++ b/tests/cloud_tests/testcases/examples/configure_instance_trusted_ca_certificates.yaml
1178diff --git a/tests/cloud_tests/configs/examples/configure_instances_ssh_keys.yaml b/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.yaml
1179index f3eaf3c..f3eaf3c 100644
1180--- a/tests/cloud_tests/configs/examples/configure_instances_ssh_keys.yaml
1181+++ b/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.yaml
1182diff --git a/tests/cloud_tests/configs/examples/including_user_groups.yaml b/tests/cloud_tests/testcases/examples/including_user_groups.yaml
1183index 0aa7ad2..0aa7ad2 100644
1184--- a/tests/cloud_tests/configs/examples/including_user_groups.yaml
1185+++ b/tests/cloud_tests/testcases/examples/including_user_groups.yaml
1186diff --git a/tests/cloud_tests/configs/examples/install_arbitrary_packages.yaml b/tests/cloud_tests/testcases/examples/install_arbitrary_packages.yaml
1187index d398022..d398022 100644
1188--- a/tests/cloud_tests/configs/examples/install_arbitrary_packages.yaml
1189+++ b/tests/cloud_tests/testcases/examples/install_arbitrary_packages.yaml
1190diff --git a/tests/cloud_tests/configs/examples/install_run_chef_recipes.yaml b/tests/cloud_tests/testcases/examples/install_run_chef_recipes.yaml
1191index 0bec305..0bec305 100644
1192--- a/tests/cloud_tests/configs/examples/install_run_chef_recipes.yaml
1193+++ b/tests/cloud_tests/testcases/examples/install_run_chef_recipes.yaml
1194diff --git a/tests/cloud_tests/configs/examples/run_apt_upgrade.yaml b/tests/cloud_tests/testcases/examples/run_apt_upgrade.yaml
1195index 2b7eae4..2b7eae4 100644
1196--- a/tests/cloud_tests/configs/examples/run_apt_upgrade.yaml
1197+++ b/tests/cloud_tests/testcases/examples/run_apt_upgrade.yaml
1198diff --git a/tests/cloud_tests/configs/examples/run_commands.yaml b/tests/cloud_tests/testcases/examples/run_commands.yaml
1199index b0e311b..b0e311b 100644
1200--- a/tests/cloud_tests/configs/examples/run_commands.yaml
1201+++ b/tests/cloud_tests/testcases/examples/run_commands.yaml
1202diff --git a/tests/cloud_tests/configs/examples/run_commands_first_boot.yaml b/tests/cloud_tests/testcases/examples/run_commands_first_boot.yaml
1203index 7bd803d..7bd803d 100644
1204--- a/tests/cloud_tests/configs/examples/run_commands_first_boot.yaml
1205+++ b/tests/cloud_tests/testcases/examples/run_commands_first_boot.yaml
1206diff --git a/tests/cloud_tests/configs/examples/setup_run_puppet.yaml b/tests/cloud_tests/testcases/examples/setup_run_puppet.yaml
1207index e366c04..e366c04 100644
1208--- a/tests/cloud_tests/configs/examples/setup_run_puppet.yaml
1209+++ b/tests/cloud_tests/testcases/examples/setup_run_puppet.yaml
1210diff --git a/tests/cloud_tests/configs/examples/writing_out_arbitrary_files.yaml b/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.yaml
1211index 6f78f99..6f78f99 100644
1212--- a/tests/cloud_tests/configs/examples/writing_out_arbitrary_files.yaml
1213+++ b/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.yaml
1214diff --git a/tests/cloud_tests/configs/main/README.md b/tests/cloud_tests/testcases/main/README.md
1215index 6034606..6034606 100644
1216--- a/tests/cloud_tests/configs/main/README.md
1217+++ b/tests/cloud_tests/testcases/main/README.md
1218diff --git a/tests/cloud_tests/configs/main/command_output_simple.yaml b/tests/cloud_tests/testcases/main/command_output_simple.yaml
1219index 08ca894..08ca894 100644
1220--- a/tests/cloud_tests/configs/main/command_output_simple.yaml
1221+++ b/tests/cloud_tests/testcases/main/command_output_simple.yaml
1222diff --git a/tests/cloud_tests/configs/modules/README.md b/tests/cloud_tests/testcases/modules/README.md
1223index d66101f..d66101f 100644
1224--- a/tests/cloud_tests/configs/modules/README.md
1225+++ b/tests/cloud_tests/testcases/modules/README.md
1226diff --git a/tests/cloud_tests/configs/modules/TODO.md b/tests/cloud_tests/testcases/modules/TODO.md
1227index 0b933b3..0b933b3 100644
1228--- a/tests/cloud_tests/configs/modules/TODO.md
1229+++ b/tests/cloud_tests/testcases/modules/TODO.md
1230diff --git a/tests/cloud_tests/configs/modules/apt_configure_conf.yaml b/tests/cloud_tests/testcases/modules/apt_configure_conf.yaml
1231index de45300..de45300 100644
1232--- a/tests/cloud_tests/configs/modules/apt_configure_conf.yaml
1233+++ b/tests/cloud_tests/testcases/modules/apt_configure_conf.yaml
1234diff --git a/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml b/tests/cloud_tests/testcases/modules/apt_configure_disable_suites.yaml
1235index 9880067..9880067 100644
1236--- a/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml
1237+++ b/tests/cloud_tests/testcases/modules/apt_configure_disable_suites.yaml
1238diff --git a/tests/cloud_tests/configs/modules/apt_configure_primary.yaml b/tests/cloud_tests/testcases/modules/apt_configure_primary.yaml
1239index 41bcf2f..41bcf2f 100644
1240--- a/tests/cloud_tests/configs/modules/apt_configure_primary.yaml
1241+++ b/tests/cloud_tests/testcases/modules/apt_configure_primary.yaml
1242diff --git a/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml b/tests/cloud_tests/testcases/modules/apt_configure_proxy.yaml
1243index be6c6f8..be6c6f8 100644
1244--- a/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml
1245+++ b/tests/cloud_tests/testcases/modules/apt_configure_proxy.yaml
1246diff --git a/tests/cloud_tests/configs/modules/apt_configure_security.yaml b/tests/cloud_tests/testcases/modules/apt_configure_security.yaml
1247index 83dd51d..83dd51d 100644
1248--- a/tests/cloud_tests/configs/modules/apt_configure_security.yaml
1249+++ b/tests/cloud_tests/testcases/modules/apt_configure_security.yaml
1250diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml b/tests/cloud_tests/testcases/modules/apt_configure_sources_key.yaml
1251index bde9398..bde9398 100644
1252--- a/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml
1253+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_key.yaml
1254diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml b/tests/cloud_tests/testcases/modules/apt_configure_sources_keyserver.yaml
1255index 2508813..2508813 100644
1256--- a/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml
1257+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_keyserver.yaml
1258diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml b/tests/cloud_tests/testcases/modules/apt_configure_sources_list.yaml
1259index 143cb08..143cb08 100644
1260--- a/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml
1261+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_list.yaml
1262diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml
1263index 9efdae5..9efdae5 100644
1264--- a/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml
1265+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml
1266diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml b/tests/cloud_tests/testcases/modules/apt_pipelining_disable.yaml
1267index bd9b5d0..bd9b5d0 100644
1268--- a/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml
1269+++ b/tests/cloud_tests/testcases/modules/apt_pipelining_disable.yaml
1270diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml b/tests/cloud_tests/testcases/modules/apt_pipelining_os.yaml
1271index cbed3ba..cbed3ba 100644
1272--- a/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml
1273+++ b/tests/cloud_tests/testcases/modules/apt_pipelining_os.yaml
1274diff --git a/tests/cloud_tests/configs/modules/bootcmd.yaml b/tests/cloud_tests/testcases/modules/bootcmd.yaml
1275index 3a73994..3a73994 100644
1276--- a/tests/cloud_tests/configs/modules/bootcmd.yaml
1277+++ b/tests/cloud_tests/testcases/modules/bootcmd.yaml
1278diff --git a/tests/cloud_tests/configs/modules/byobu.yaml b/tests/cloud_tests/testcases/modules/byobu.yaml
1279index a9aa1f3..a9aa1f3 100644
1280--- a/tests/cloud_tests/configs/modules/byobu.yaml
1281+++ b/tests/cloud_tests/testcases/modules/byobu.yaml
1282diff --git a/tests/cloud_tests/configs/modules/ca_certs.yaml b/tests/cloud_tests/testcases/modules/ca_certs.yaml
1283index d939f43..d939f43 100644
1284--- a/tests/cloud_tests/configs/modules/ca_certs.yaml
1285+++ b/tests/cloud_tests/testcases/modules/ca_certs.yaml
1286diff --git a/tests/cloud_tests/configs/modules/debug_disable.yaml b/tests/cloud_tests/testcases/modules/debug_disable.yaml
1287index 63218b1..63218b1 100644
1288--- a/tests/cloud_tests/configs/modules/debug_disable.yaml
1289+++ b/tests/cloud_tests/testcases/modules/debug_disable.yaml
1290diff --git a/tests/cloud_tests/configs/modules/debug_enable.yaml b/tests/cloud_tests/testcases/modules/debug_enable.yaml
1291index d44147d..d44147d 100644
1292--- a/tests/cloud_tests/configs/modules/debug_enable.yaml
1293+++ b/tests/cloud_tests/testcases/modules/debug_enable.yaml
1294diff --git a/tests/cloud_tests/configs/modules/final_message.yaml b/tests/cloud_tests/testcases/modules/final_message.yaml
1295index c9ed611..c9ed611 100644
1296--- a/tests/cloud_tests/configs/modules/final_message.yaml
1297+++ b/tests/cloud_tests/testcases/modules/final_message.yaml
1298diff --git a/tests/cloud_tests/configs/modules/keys_to_console.yaml b/tests/cloud_tests/testcases/modules/keys_to_console.yaml
1299index 5d86e73..5d86e73 100644
1300--- a/tests/cloud_tests/configs/modules/keys_to_console.yaml
1301+++ b/tests/cloud_tests/testcases/modules/keys_to_console.yaml
1302diff --git a/tests/cloud_tests/configs/modules/landscape.yaml b/tests/cloud_tests/testcases/modules/landscape.yaml
1303index ed2c37c..ed2c37c 100644
1304--- a/tests/cloud_tests/configs/modules/landscape.yaml
1305+++ b/tests/cloud_tests/testcases/modules/landscape.yaml
1306diff --git a/tests/cloud_tests/configs/modules/locale.yaml b/tests/cloud_tests/testcases/modules/locale.yaml
1307index e01518a..e01518a 100644
1308--- a/tests/cloud_tests/configs/modules/locale.yaml
1309+++ b/tests/cloud_tests/testcases/modules/locale.yaml
1310diff --git a/tests/cloud_tests/configs/modules/lxd_bridge.yaml b/tests/cloud_tests/testcases/modules/lxd_bridge.yaml
1311index e6b7e76..e6b7e76 100644
1312--- a/tests/cloud_tests/configs/modules/lxd_bridge.yaml
1313+++ b/tests/cloud_tests/testcases/modules/lxd_bridge.yaml
1314diff --git a/tests/cloud_tests/configs/modules/lxd_dir.yaml b/tests/cloud_tests/testcases/modules/lxd_dir.yaml
1315index f93a3fa..f93a3fa 100644
1316--- a/tests/cloud_tests/configs/modules/lxd_dir.yaml
1317+++ b/tests/cloud_tests/testcases/modules/lxd_dir.yaml
1318diff --git a/tests/cloud_tests/configs/modules/ntp.yaml b/tests/cloud_tests/testcases/modules/ntp.yaml
1319index fbef431..fbef431 100644
1320--- a/tests/cloud_tests/configs/modules/ntp.yaml
1321+++ b/tests/cloud_tests/testcases/modules/ntp.yaml
1322diff --git a/tests/cloud_tests/configs/modules/ntp_pools.yaml b/tests/cloud_tests/testcases/modules/ntp_pools.yaml
1323index 3a93faa..3a93faa 100644
1324--- a/tests/cloud_tests/configs/modules/ntp_pools.yaml
1325+++ b/tests/cloud_tests/testcases/modules/ntp_pools.yaml
1326diff --git a/tests/cloud_tests/configs/modules/ntp_servers.yaml b/tests/cloud_tests/testcases/modules/ntp_servers.yaml
1327index d59d45a..d59d45a 100644
1328--- a/tests/cloud_tests/configs/modules/ntp_servers.yaml
1329+++ b/tests/cloud_tests/testcases/modules/ntp_servers.yaml
1330diff --git a/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml b/tests/cloud_tests/testcases/modules/package_update_upgrade_install.yaml
1331index 71d24b8..71d24b8 100644
1332--- a/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml
1333+++ b/tests/cloud_tests/testcases/modules/package_update_upgrade_install.yaml
1334diff --git a/tests/cloud_tests/configs/modules/runcmd.yaml b/tests/cloud_tests/testcases/modules/runcmd.yaml
1335index 04e5a05..04e5a05 100644
1336--- a/tests/cloud_tests/configs/modules/runcmd.yaml
1337+++ b/tests/cloud_tests/testcases/modules/runcmd.yaml
1338diff --git a/tests/cloud_tests/configs/modules/salt_minion.yaml b/tests/cloud_tests/testcases/modules/salt_minion.yaml
1339index f20d24f..f20d24f 100644
1340--- a/tests/cloud_tests/configs/modules/salt_minion.yaml
1341+++ b/tests/cloud_tests/testcases/modules/salt_minion.yaml
1342diff --git a/tests/cloud_tests/configs/modules/seed_random_command.yaml b/tests/cloud_tests/testcases/modules/seed_random_command.yaml
1343index 6a9157e..6a9157e 100644
1344--- a/tests/cloud_tests/configs/modules/seed_random_command.yaml
1345+++ b/tests/cloud_tests/testcases/modules/seed_random_command.yaml
1346diff --git a/tests/cloud_tests/configs/modules/seed_random_data.yaml b/tests/cloud_tests/testcases/modules/seed_random_data.yaml
1347index a9b2c88..a9b2c88 100644
1348--- a/tests/cloud_tests/configs/modules/seed_random_data.yaml
1349+++ b/tests/cloud_tests/testcases/modules/seed_random_data.yaml
1350diff --git a/tests/cloud_tests/configs/modules/set_hostname.yaml b/tests/cloud_tests/testcases/modules/set_hostname.yaml
1351index c96344c..c96344c 100644
1352--- a/tests/cloud_tests/configs/modules/set_hostname.yaml
1353+++ b/tests/cloud_tests/testcases/modules/set_hostname.yaml
1354diff --git a/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml
1355index daf7593..daf7593 100644
1356--- a/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml
1357+++ b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml
1358diff --git a/tests/cloud_tests/configs/modules/set_password.yaml b/tests/cloud_tests/testcases/modules/set_password.yaml
1359index 04d7c58..04d7c58 100644
1360--- a/tests/cloud_tests/configs/modules/set_password.yaml
1361+++ b/tests/cloud_tests/testcases/modules/set_password.yaml
1362diff --git a/tests/cloud_tests/configs/modules/set_password_expire.yaml b/tests/cloud_tests/testcases/modules/set_password_expire.yaml
1363index 789604b..789604b 100644
1364--- a/tests/cloud_tests/configs/modules/set_password_expire.yaml
1365+++ b/tests/cloud_tests/testcases/modules/set_password_expire.yaml
1366diff --git a/tests/cloud_tests/configs/modules/set_password_list.yaml b/tests/cloud_tests/testcases/modules/set_password_list.yaml
1367index a2a89c9..a2a89c9 100644
1368--- a/tests/cloud_tests/configs/modules/set_password_list.yaml
1369+++ b/tests/cloud_tests/testcases/modules/set_password_list.yaml
1370diff --git a/tests/cloud_tests/configs/modules/set_password_list_string.yaml b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml
1371index c2a0f63..c2a0f63 100644
1372--- a/tests/cloud_tests/configs/modules/set_password_list_string.yaml
1373+++ b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml
1374diff --git a/tests/cloud_tests/configs/modules/snappy.yaml b/tests/cloud_tests/testcases/modules/snappy.yaml
1375index 43f9329..43f9329 100644
1376--- a/tests/cloud_tests/configs/modules/snappy.yaml
1377+++ b/tests/cloud_tests/testcases/modules/snappy.yaml
1378diff --git a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml
1379index 746653e..746653e 100644
1380--- a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml
1381+++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml
1382diff --git a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_enable.yaml
1383index 9f5dc34..9f5dc34 100644
1384--- a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml
1385+++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_enable.yaml
1386diff --git a/tests/cloud_tests/configs/modules/ssh_import_id.yaml b/tests/cloud_tests/testcases/modules/ssh_import_id.yaml
1387index b62d3f6..b62d3f6 100644
1388--- a/tests/cloud_tests/configs/modules/ssh_import_id.yaml
1389+++ b/tests/cloud_tests/testcases/modules/ssh_import_id.yaml
1390diff --git a/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml
1391index 659fd93..659fd93 100644
1392--- a/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml
1393+++ b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml
1394diff --git a/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml
1395index 5ceb362..5ceb362 100644
1396--- a/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml
1397+++ b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml
1398diff --git a/tests/cloud_tests/configs/modules/timezone.yaml b/tests/cloud_tests/testcases/modules/timezone.yaml
1399index 5112aa9..5112aa9 100644
1400--- a/tests/cloud_tests/configs/modules/timezone.yaml
1401+++ b/tests/cloud_tests/testcases/modules/timezone.yaml
1402diff --git a/tests/cloud_tests/configs/modules/user_groups.yaml b/tests/cloud_tests/testcases/modules/user_groups.yaml
1403index 71cc9da..71cc9da 100644
1404--- a/tests/cloud_tests/configs/modules/user_groups.yaml
1405+++ b/tests/cloud_tests/testcases/modules/user_groups.yaml
1406diff --git a/tests/cloud_tests/configs/modules/write_files.yaml b/tests/cloud_tests/testcases/modules/write_files.yaml
1407index ce936b7..ce936b7 100644
1408--- a/tests/cloud_tests/configs/modules/write_files.yaml
1409+++ b/tests/cloud_tests/testcases/modules/write_files.yaml
1410diff --git a/tests/unittests/test_datasource/test_altcloud.py b/tests/unittests/test_datasource/test_altcloud.py
1411index 3b274d9..a4dfb54 100644
1412--- a/tests/unittests/test_datasource/test_altcloud.py
1413+++ b/tests/unittests/test_datasource/test_altcloud.py
1414@@ -280,8 +280,8 @@ class TestUserDataRhevm(TestCase):
1415 pass
1416
1417 dsac.CLOUD_INFO_FILE = '/etc/sysconfig/cloud-info'
1418- dsac.CMD_PROBE_FLOPPY = ['/sbin/modprobe', 'floppy']
1419- dsac.CMD_UDEVADM_SETTLE = ['/sbin/udevadm', 'settle',
1420+ dsac.CMD_PROBE_FLOPPY = ['modprobe', 'floppy']
1421+ dsac.CMD_UDEVADM_SETTLE = ['udevadm', 'settle',
1422 '--quiet', '--timeout=5']
1423
1424 def test_mount_cb_fails(self):
1425diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py
1426index 44b99ec..b42b073 100644
1427--- a/tests/unittests/test_datasource/test_azure_helper.py
1428+++ b/tests/unittests/test_datasource/test_azure_helper.py
1429@@ -1,10 +1,12 @@
1430 # This file is part of cloud-init. See LICENSE file for license information.
1431
1432 import os
1433+from textwrap import dedent
1434
1435 from cloudinit.sources.helpers import azure as azure_helper
1436-from cloudinit.tests.helpers import ExitStack, mock, TestCase
1437+from cloudinit.tests.helpers import CiTestCase, ExitStack, mock, populate_dir
1438
1439+from cloudinit.sources.helpers.azure import WALinuxAgentShim as wa_shim
1440
1441 GOAL_STATE_TEMPLATE = """\
1442 <?xml version="1.0" encoding="utf-8"?>
1443@@ -45,7 +47,7 @@ GOAL_STATE_TEMPLATE = """\
1444 """
1445
1446
1447-class TestFindEndpoint(TestCase):
1448+class TestFindEndpoint(CiTestCase):
1449
1450 def setUp(self):
1451 super(TestFindEndpoint, self).setUp()
1452@@ -56,18 +58,19 @@ class TestFindEndpoint(TestCase):
1453 mock.patch.object(azure_helper.util, 'load_file'))
1454
1455 self.dhcp_options = patches.enter_context(
1456- mock.patch.object(azure_helper.WALinuxAgentShim,
1457- '_load_dhclient_json'))
1458+ mock.patch.object(wa_shim, '_load_dhclient_json'))
1459+
1460+ self.networkd_leases = patches.enter_context(
1461+ mock.patch.object(wa_shim, '_networkd_get_value_from_leases'))
1462+ self.networkd_leases.return_value = None
1463
1464 def test_missing_file(self):
1465- self.assertRaises(ValueError,
1466- azure_helper.WALinuxAgentShim.find_endpoint)
1467+ self.assertRaises(ValueError, wa_shim.find_endpoint)
1468
1469 def test_missing_special_azure_line(self):
1470 self.load_file.return_value = ''
1471 self.dhcp_options.return_value = {'eth0': {'key': 'value'}}
1472- self.assertRaises(ValueError,
1473- azure_helper.WALinuxAgentShim.find_endpoint)
1474+ self.assertRaises(ValueError, wa_shim.find_endpoint)
1475
1476 @staticmethod
1477 def _build_lease_content(encoded_address):
1478@@ -80,8 +83,7 @@ class TestFindEndpoint(TestCase):
1479
1480 def test_from_dhcp_client(self):
1481 self.dhcp_options.return_value = {"eth0": {"unknown_245": "5:4:3:2"}}
1482- self.assertEqual('5.4.3.2',
1483- azure_helper.WALinuxAgentShim.find_endpoint(None))
1484+ self.assertEqual('5.4.3.2', wa_shim.find_endpoint(None))
1485
1486 def test_latest_lease_used(self):
1487 encoded_addresses = ['5:4:3:2', '4:3:2:1']
1488@@ -89,53 +91,38 @@ class TestFindEndpoint(TestCase):
1489 for encoded_address in encoded_addresses])
1490 self.load_file.return_value = file_content
1491 self.assertEqual(encoded_addresses[-1].replace(':', '.'),
1492- azure_helper.WALinuxAgentShim.find_endpoint("foobar"))
1493+ wa_shim.find_endpoint("foobar"))
1494
1495
1496-class TestExtractIpAddressFromLeaseValue(TestCase):
1497+class TestExtractIpAddressFromLeaseValue(CiTestCase):
1498
1499 def test_hex_string(self):
1500 ip_address, encoded_address = '98.76.54.32', '62:4c:36:20'
1501 self.assertEqual(
1502- ip_address,
1503- azure_helper.WALinuxAgentShim.get_ip_from_lease_value(
1504- encoded_address
1505- ))
1506+ ip_address, wa_shim.get_ip_from_lease_value(encoded_address))
1507
1508 def test_hex_string_with_single_character_part(self):
1509 ip_address, encoded_address = '4.3.2.1', '4:3:2:1'
1510 self.assertEqual(
1511- ip_address,
1512- azure_helper.WALinuxAgentShim.get_ip_from_lease_value(
1513- encoded_address
1514- ))
1515+ ip_address, wa_shim.get_ip_from_lease_value(encoded_address))
1516
1517 def test_packed_string(self):
1518 ip_address, encoded_address = '98.76.54.32', 'bL6 '
1519 self.assertEqual(
1520- ip_address,
1521- azure_helper.WALinuxAgentShim.get_ip_from_lease_value(
1522- encoded_address
1523- ))
1524+ ip_address, wa_shim.get_ip_from_lease_value(encoded_address))
1525
1526 def test_packed_string_with_escaped_quote(self):
1527 ip_address, encoded_address = '100.72.34.108', 'dH\\"l'
1528 self.assertEqual(
1529- ip_address,
1530- azure_helper.WALinuxAgentShim.get_ip_from_lease_value(
1531- encoded_address
1532- ))
1533+ ip_address, wa_shim.get_ip_from_lease_value(encoded_address))
1534
1535 def test_packed_string_containing_a_colon(self):
1536 ip_address, encoded_address = '100.72.58.108', 'dH:l'
1537 self.assertEqual(
1538- ip_address,
1539- azure_helper.WALinuxAgentShim.get_ip_from_lease_value(
1540- encoded_address
1541- ))
1542+ ip_address, wa_shim.get_ip_from_lease_value(encoded_address))
1543
1544
1545-class TestGoalStateParsing(TestCase):
1546+class TestGoalStateParsing(CiTestCase):
1547
1548 default_parameters = {
1549 'incarnation': 1,
1550@@ -195,7 +182,7 @@ class TestGoalStateParsing(TestCase):
1551 self.assertIsNone(certificates_xml)
1552
1553
1554-class TestAzureEndpointHttpClient(TestCase):
1555+class TestAzureEndpointHttpClient(CiTestCase):
1556
1557 regular_headers = {
1558 'x-ms-agent-name': 'WALinuxAgent',
1559@@ -258,7 +245,7 @@ class TestAzureEndpointHttpClient(TestCase):
1560 self.read_file_or_url.call_args)
1561
1562
1563-class TestOpenSSLManager(TestCase):
1564+class TestOpenSSLManager(CiTestCase):
1565
1566 def setUp(self):
1567 super(TestOpenSSLManager, self).setUp()
1568@@ -300,7 +287,7 @@ class TestOpenSSLManager(TestCase):
1569 self.assertEqual([mock.call(manager.tmpdir)], del_dir.call_args_list)
1570
1571
1572-class TestWALinuxAgentShim(TestCase):
1573+class TestWALinuxAgentShim(CiTestCase):
1574
1575 def setUp(self):
1576 super(TestWALinuxAgentShim, self).setUp()
1577@@ -310,8 +297,7 @@ class TestWALinuxAgentShim(TestCase):
1578 self.AzureEndpointHttpClient = patches.enter_context(
1579 mock.patch.object(azure_helper, 'AzureEndpointHttpClient'))
1580 self.find_endpoint = patches.enter_context(
1581- mock.patch.object(
1582- azure_helper.WALinuxAgentShim, 'find_endpoint'))
1583+ mock.patch.object(wa_shim, 'find_endpoint'))
1584 self.GoalState = patches.enter_context(
1585 mock.patch.object(azure_helper, 'GoalState'))
1586 self.OpenSSLManager = patches.enter_context(
1587@@ -320,7 +306,7 @@ class TestWALinuxAgentShim(TestCase):
1588 mock.patch.object(azure_helper.time, 'sleep', mock.MagicMock()))
1589
1590 def test_http_client_uses_certificate(self):
1591- shim = azure_helper.WALinuxAgentShim()
1592+ shim = wa_shim()
1593 shim.register_with_azure_and_fetch_data()
1594 self.assertEqual(
1595 [mock.call(self.OpenSSLManager.return_value.certificate)],
1596@@ -328,7 +314,7 @@ class TestWALinuxAgentShim(TestCase):
1597
1598 def test_correct_url_used_for_goalstate(self):
1599 self.find_endpoint.return_value = 'test_endpoint'
1600- shim = azure_helper.WALinuxAgentShim()
1601+ shim = wa_shim()
1602 shim.register_with_azure_and_fetch_data()
1603 get = self.AzureEndpointHttpClient.return_value.get
1604 self.assertEqual(
1605@@ -340,7 +326,7 @@ class TestWALinuxAgentShim(TestCase):
1606 self.GoalState.call_args_list)
1607
1608 def test_certificates_used_to_determine_public_keys(self):
1609- shim = azure_helper.WALinuxAgentShim()
1610+ shim = wa_shim()
1611 data = shim.register_with_azure_and_fetch_data()
1612 self.assertEqual(
1613 [mock.call(self.GoalState.return_value.certificates_xml)],
1614@@ -351,13 +337,13 @@ class TestWALinuxAgentShim(TestCase):
1615
1616 def test_absent_certificates_produces_empty_public_keys(self):
1617 self.GoalState.return_value.certificates_xml = None
1618- shim = azure_helper.WALinuxAgentShim()
1619+ shim = wa_shim()
1620 data = shim.register_with_azure_and_fetch_data()
1621 self.assertEqual([], data['public-keys'])
1622
1623 def test_correct_url_used_for_report_ready(self):
1624 self.find_endpoint.return_value = 'test_endpoint'
1625- shim = azure_helper.WALinuxAgentShim()
1626+ shim = wa_shim()
1627 shim.register_with_azure_and_fetch_data()
1628 expected_url = 'http://test_endpoint/machine?comp=health'
1629 self.assertEqual(
1630@@ -368,7 +354,7 @@ class TestWALinuxAgentShim(TestCase):
1631 self.GoalState.return_value.incarnation = 'TestIncarnation'
1632 self.GoalState.return_value.container_id = 'TestContainerId'
1633 self.GoalState.return_value.instance_id = 'TestInstanceId'
1634- shim = azure_helper.WALinuxAgentShim()
1635+ shim = wa_shim()
1636 shim.register_with_azure_and_fetch_data()
1637 posted_document = (
1638 self.AzureEndpointHttpClient.return_value.post.call_args[1]['data']
1639@@ -378,11 +364,11 @@ class TestWALinuxAgentShim(TestCase):
1640 self.assertIn('TestInstanceId', posted_document)
1641
1642 def test_clean_up_can_be_called_at_any_time(self):
1643- shim = azure_helper.WALinuxAgentShim()
1644+ shim = wa_shim()
1645 shim.clean_up()
1646
1647 def test_clean_up_will_clean_up_openssl_manager_if_instantiated(self):
1648- shim = azure_helper.WALinuxAgentShim()
1649+ shim = wa_shim()
1650 shim.register_with_azure_and_fetch_data()
1651 shim.clean_up()
1652 self.assertEqual(
1653@@ -393,12 +379,12 @@ class TestWALinuxAgentShim(TestCase):
1654 pass
1655 self.AzureEndpointHttpClient.return_value.get.side_effect = (
1656 SentinelException)
1657- shim = azure_helper.WALinuxAgentShim()
1658+ shim = wa_shim()
1659 self.assertRaises(SentinelException,
1660 shim.register_with_azure_and_fetch_data)
1661
1662
1663-class TestGetMetadataFromFabric(TestCase):
1664+class TestGetMetadataFromFabric(CiTestCase):
1665
1666 @mock.patch.object(azure_helper, 'WALinuxAgentShim')
1667 def test_data_from_shim_returned(self, shim):
1668@@ -422,4 +408,65 @@ class TestGetMetadataFromFabric(TestCase):
1669 azure_helper.get_metadata_from_fabric)
1670 self.assertEqual(1, shim.return_value.clean_up.call_count)
1671
1672+
1673+class TestExtractIpAddressFromNetworkd(CiTestCase):
1674+
1675+ azure_lease = dedent("""\
1676+ # This is private data. Do not parse.
1677+ ADDRESS=10.132.0.5
1678+ NETMASK=255.255.255.255
1679+ ROUTER=10.132.0.1
1680+ SERVER_ADDRESS=169.254.169.254
1681+ NEXT_SERVER=10.132.0.1
1682+ MTU=1460
1683+ T1=43200
1684+ T2=75600
1685+ LIFETIME=86400
1686+ DNS=169.254.169.254
1687+ NTP=169.254.169.254
1688+ DOMAINNAME=c.ubuntu-foundations.internal
1689+ DOMAIN_SEARCH_LIST=c.ubuntu-foundations.internal google.internal
1690+ HOSTNAME=tribaal-test-171002-1349.c.ubuntu-foundations.internal
1691+ ROUTES=10.132.0.1/32,0.0.0.0 0.0.0.0/0,10.132.0.1
1692+ CLIENTID=ff405663a200020000ab11332859494d7a8b4c
1693+ OPTION_245=624c3620
1694+ """)
1695+
1696+ def setUp(self):
1697+ super(TestExtractIpAddressFromNetworkd, self).setUp()
1698+ self.lease_d = self.tmp_dir()
1699+
1700+ def test_no_valid_leases_is_none(self):
1701+ """No valid leases should return None."""
1702+ self.assertIsNone(
1703+ wa_shim._networkd_get_value_from_leases(self.lease_d))
1704+
1705+ def test_option_245_is_found_in_single(self):
1706+ """A single valid lease with 245 option should return it."""
1707+ populate_dir(self.lease_d, {'9': self.azure_lease})
1708+ self.assertEqual(
1709+ '624c3620', wa_shim._networkd_get_value_from_leases(self.lease_d))
1710+
1711+ def test_option_245_not_found_returns_None(self):
1712+ """A valid lease, but no option 245 should return None."""
1713+ populate_dir(
1714+ self.lease_d,
1715+ {'9': self.azure_lease.replace("OPTION_245", "OPTION_999")})
1716+ self.assertIsNone(
1717+ wa_shim._networkd_get_value_from_leases(self.lease_d))
1718+
1719+ def test_multiple_returns_first(self):
1720+ """Somewhat arbitrarily return the first address when multiple.
1721+
1722+ Most important at the moment is that this is consistent behavior
1723+ rather than changing randomly as in order of a dictionary."""
1724+ myval = "624c3601"
1725+ populate_dir(
1726+ self.lease_d,
1727+ {'9': self.azure_lease,
1728+ '2': self.azure_lease.replace("624c3620", myval)})
1729+ self.assertEqual(
1730+ myval, wa_shim._networkd_get_value_from_leases(self.lease_d))
1731+
1732+
1733 # vi: ts=4 expandtab
1734diff --git a/tests/unittests/test_datasource/test_cloudstack.py b/tests/unittests/test_datasource/test_cloudstack.py
1735index 8e98e1b..96144b6 100644
1736--- a/tests/unittests/test_datasource/test_cloudstack.py
1737+++ b/tests/unittests/test_datasource/test_cloudstack.py
1738@@ -23,13 +23,16 @@ class TestCloudStackPasswordFetching(CiTestCase):
1739 default_gw = "192.201.20.0"
1740 get_latest_lease = mock.MagicMock(return_value=None)
1741 self.patches.enter_context(mock.patch(
1742- 'cloudinit.sources.DataSourceCloudStack.get_latest_lease',
1743- get_latest_lease))
1744+ mod_name + '.get_latest_lease', get_latest_lease))
1745
1746 get_default_gw = mock.MagicMock(return_value=default_gw)
1747 self.patches.enter_context(mock.patch(
1748- 'cloudinit.sources.DataSourceCloudStack.get_default_gateway',
1749- get_default_gw))
1750+ mod_name + '.get_default_gateway', get_default_gw))
1751+
1752+ get_networkd_server_address = mock.MagicMock(return_value=None)
1753+ self.patches.enter_context(mock.patch(
1754+ mod_name + '.dhcp.networkd_get_option_from_leases',
1755+ get_networkd_server_address))
1756
1757 def _set_password_server_response(self, response_string):
1758 subp = mock.MagicMock(return_value=(response_string, ''))
1759diff --git a/tests/unittests/test_datasource/test_ovf.py b/tests/unittests/test_datasource/test_ovf.py
1760index 9dbf4dd..700da86 100644
1761--- a/tests/unittests/test_datasource/test_ovf.py
1762+++ b/tests/unittests/test_datasource/test_ovf.py
1763@@ -5,6 +5,7 @@
1764 # This file is part of cloud-init. See LICENSE file for license information.
1765
1766 import base64
1767+from collections import OrderedDict
1768
1769 from cloudinit.tests import helpers as test_helpers
1770
1771@@ -70,4 +71,167 @@ class TestReadOvfEnv(test_helpers.TestCase):
1772 self.assertEqual({'password': "passw0rd"}, cfg)
1773 self.assertIsNone(ud)
1774
1775+
1776+class TestTransportIso9660(test_helpers.CiTestCase):
1777+
1778+ def setUp(self):
1779+ super(TestTransportIso9660, self).setUp()
1780+ self.add_patch('cloudinit.util.find_devs_with',
1781+ 'm_find_devs_with')
1782+ self.add_patch('cloudinit.util.mounts', 'm_mounts')
1783+ self.add_patch('cloudinit.util.mount_cb', 'm_mount_cb')
1784+ self.add_patch('cloudinit.sources.DataSourceOVF.get_ovf_env',
1785+ 'm_get_ovf_env')
1786+ self.m_get_ovf_env.return_value = ('myfile', 'mycontent')
1787+
1788+ def test_find_already_mounted(self):
1789+ """Check we call get_ovf_env from on matching mounted devices"""
1790+ mounts = {
1791+ '/dev/sr9': {
1792+ 'fstype': 'iso9660',
1793+ 'mountpoint': 'wark/media/sr9',
1794+ 'opts': 'ro',
1795+ }
1796+ }
1797+ self.m_mounts.return_value = mounts
1798+
1799+ (contents, fullp, fname) = dsovf.transport_iso9660()
1800+ self.assertEqual("mycontent", contents)
1801+ self.assertEqual("/dev/sr9", fullp)
1802+ self.assertEqual("myfile", fname)
1803+
1804+ def test_find_already_mounted_skips_non_iso9660(self):
1805+ """Check we call get_ovf_env ignoring non iso9660"""
1806+ mounts = {
1807+ '/dev/xvdb': {
1808+ 'fstype': 'vfat',
1809+ 'mountpoint': 'wark/foobar',
1810+ 'opts': 'defaults,noatime',
1811+ },
1812+ '/dev/xvdc': {
1813+ 'fstype': 'iso9660',
1814+ 'mountpoint': 'wark/media/sr9',
1815+ 'opts': 'ro',
1816+ }
1817+ }
1818+ # We use an OrderedDict here to ensure we check xvdb before xvdc
1819+ # as we're not mocking the regex matching, however, if we place
1820+ # an entry in the results then we can be reasonably sure that
1821+ # we're skipping an entry which fails to match.
1822+ self.m_mounts.return_value = (
1823+ OrderedDict(sorted(mounts.items(), key=lambda t: t[0])))
1824+
1825+ (contents, fullp, fname) = dsovf.transport_iso9660()
1826+ self.assertEqual("mycontent", contents)
1827+ self.assertEqual("/dev/xvdc", fullp)
1828+ self.assertEqual("myfile", fname)
1829+
1830+ def test_find_already_mounted_matches_kname(self):
1831+ """Check we dont regex match on basename of the device"""
1832+ mounts = {
1833+ '/dev/foo/bar/xvdc': {
1834+ 'fstype': 'iso9660',
1835+ 'mountpoint': 'wark/media/sr9',
1836+ 'opts': 'ro',
1837+ }
1838+ }
1839+ # we're skipping an entry which fails to match.
1840+ self.m_mounts.return_value = mounts
1841+
1842+ (contents, fullp, fname) = dsovf.transport_iso9660()
1843+ self.assertEqual(False, contents)
1844+ self.assertIsNone(fullp)
1845+ self.assertIsNone(fname)
1846+
1847+ def test_mount_cb_called_on_blkdevs_with_iso9660(self):
1848+ """Check we call mount_cb on blockdevs with iso9660 only"""
1849+ self.m_mounts.return_value = {}
1850+ self.m_find_devs_with.return_value = ['/dev/sr0']
1851+ self.m_mount_cb.return_value = ("myfile", "mycontent")
1852+
1853+ (contents, fullp, fname) = dsovf.transport_iso9660()
1854+
1855+ self.m_mount_cb.assert_called_with(
1856+ "/dev/sr0", dsovf.get_ovf_env, mtype="iso9660")
1857+ self.assertEqual("mycontent", contents)
1858+ self.assertEqual("/dev/sr0", fullp)
1859+ self.assertEqual("myfile", fname)
1860+
1861+ def test_mount_cb_called_on_blkdevs_with_iso9660_check_regex(self):
1862+ """Check we call mount_cb on blockdevs with iso9660 and match regex"""
1863+ self.m_mounts.return_value = {}
1864+ self.m_find_devs_with.return_value = [
1865+ '/dev/abc', '/dev/my-cdrom', '/dev/sr0']
1866+ self.m_mount_cb.return_value = ("myfile", "mycontent")
1867+
1868+ (contents, fullp, fname) = dsovf.transport_iso9660()
1869+
1870+ self.m_mount_cb.assert_called_with(
1871+ "/dev/sr0", dsovf.get_ovf_env, mtype="iso9660")
1872+ self.assertEqual("mycontent", contents)
1873+ self.assertEqual("/dev/sr0", fullp)
1874+ self.assertEqual("myfile", fname)
1875+
1876+ def test_mount_cb_not_called_no_matches(self):
1877+ """Check we don't call mount_cb if nothing matches"""
1878+ self.m_mounts.return_value = {}
1879+ self.m_find_devs_with.return_value = ['/dev/vg/myovf']
1880+
1881+ (contents, fullp, fname) = dsovf.transport_iso9660()
1882+
1883+ self.assertEqual(0, self.m_mount_cb.call_count)
1884+ self.assertEqual(False, contents)
1885+ self.assertIsNone(fullp)
1886+ self.assertIsNone(fname)
1887+
1888+ def test_mount_cb_called_require_iso_false(self):
1889+ """Check we call mount_cb on blockdevs with require_iso=False"""
1890+ self.m_mounts.return_value = {}
1891+ self.m_find_devs_with.return_value = ['/dev/xvdz']
1892+ self.m_mount_cb.return_value = ("myfile", "mycontent")
1893+
1894+ (contents, fullp, fname) = dsovf.transport_iso9660(require_iso=False)
1895+
1896+ self.m_mount_cb.assert_called_with(
1897+ "/dev/xvdz", dsovf.get_ovf_env, mtype=None)
1898+ self.assertEqual("mycontent", contents)
1899+ self.assertEqual("/dev/xvdz", fullp)
1900+ self.assertEqual("myfile", fname)
1901+
1902+ def test_maybe_cdrom_device_none(self):
1903+ """Test maybe_cdrom_device returns False for none/empty input"""
1904+ self.assertFalse(dsovf.maybe_cdrom_device(None))
1905+ self.assertFalse(dsovf.maybe_cdrom_device(''))
1906+
1907+ def test_maybe_cdrom_device_non_string_exception(self):
1908+ """Test maybe_cdrom_device raises ValueError on non-string types"""
1909+ with self.assertRaises(ValueError):
1910+ dsovf.maybe_cdrom_device({'a': 'eleven'})
1911+
1912+ def test_maybe_cdrom_device_false_on_multi_dir_paths(self):
1913+ """Test maybe_cdrom_device is false on /dev[/.*]/* paths"""
1914+ self.assertFalse(dsovf.maybe_cdrom_device('/dev/foo/sr0'))
1915+ self.assertFalse(dsovf.maybe_cdrom_device('foo/sr0'))
1916+ self.assertFalse(dsovf.maybe_cdrom_device('../foo/sr0'))
1917+ self.assertFalse(dsovf.maybe_cdrom_device('../foo/sr0'))
1918+
1919+ def test_maybe_cdrom_device_true_on_hd_partitions(self):
1920+ """Test maybe_cdrom_device is false on /dev/hd[a-z][0-9]+ paths"""
1921+ self.assertTrue(dsovf.maybe_cdrom_device('/dev/hda1'))
1922+ self.assertTrue(dsovf.maybe_cdrom_device('hdz9'))
1923+
1924+ def test_maybe_cdrom_device_true_on_valid_relative_paths(self):
1925+ """Test maybe_cdrom_device normalizes paths"""
1926+ self.assertTrue(dsovf.maybe_cdrom_device('/dev/wark/../sr9'))
1927+ self.assertTrue(dsovf.maybe_cdrom_device('///sr0'))
1928+ self.assertTrue(dsovf.maybe_cdrom_device('/sr0'))
1929+ self.assertTrue(dsovf.maybe_cdrom_device('//dev//hda'))
1930+
1931+ def test_maybe_cdrom_device_true_on_xvd_partitions(self):
1932+ """Test maybe_cdrom_device returns true on xvd*"""
1933+ self.assertTrue(dsovf.maybe_cdrom_device('/dev/xvda'))
1934+ self.assertTrue(dsovf.maybe_cdrom_device('/dev/xvda1'))
1935+ self.assertTrue(dsovf.maybe_cdrom_device('xvdza1'))
1936+
1937+#
1938 # vi: ts=4 expandtab
1939diff --git a/tests/unittests/test_handler/test_handler_bootcmd.py b/tests/unittests/test_handler/test_handler_bootcmd.py
1940index 580017e..dbf43e0 100644
1941--- a/tests/unittests/test_handler/test_handler_bootcmd.py
1942+++ b/tests/unittests/test_handler/test_handler_bootcmd.py
1943@@ -29,6 +29,7 @@ class FakeExtendedTempFile(object):
1944
1945 def __exit__(self, exc_type, exc_value, traceback):
1946 self.handle.close()
1947+ util.del_file(self.handle.name)
1948
1949
1950 class TestBootcmd(CiTestCase):
1951diff --git a/tests/unittests/test_handler/test_handler_zypper_add_repo.py b/tests/unittests/test_handler/test_handler_zypper_add_repo.py
1952new file mode 100644
1953index 0000000..315c2a5
1954--- /dev/null
1955+++ b/tests/unittests/test_handler/test_handler_zypper_add_repo.py
1956@@ -0,0 +1,237 @@
1957+# This file is part of cloud-init. See LICENSE file for license information.
1958+
1959+import glob
1960+import os
1961+
1962+from cloudinit.config import cc_zypper_add_repo
1963+from cloudinit import util
1964+
1965+from cloudinit.tests import helpers
1966+from cloudinit.tests.helpers import mock
1967+
1968+try:
1969+ from configparser import ConfigParser
1970+except ImportError:
1971+ from ConfigParser import ConfigParser
1972+import logging
1973+from six import StringIO
1974+
1975+LOG = logging.getLogger(__name__)
1976+
1977+
1978+class TestConfig(helpers.FilesystemMockingTestCase):
1979+ def setUp(self):
1980+ super(TestConfig, self).setUp()
1981+ self.tmp = self.tmp_dir()
1982+ self.zypp_conf = 'etc/zypp/zypp.conf'
1983+
1984+ def test_bad_repo_config(self):
1985+ """Config has no baseurl, no file should be written"""
1986+ cfg = {
1987+ 'repos': [
1988+ {
1989+ 'id': 'foo',
1990+ 'name': 'suse-test',
1991+ 'enabled': '1'
1992+ },
1993+ ]
1994+ }
1995+ self.patchUtils(self.tmp)
1996+ cc_zypper_add_repo._write_repos(cfg['repos'], '/etc/zypp/repos.d')
1997+ self.assertRaises(IOError, util.load_file,
1998+ "/etc/zypp/repos.d/foo.repo")
1999+
2000+ def test_write_repos(self):
2001+ """Verify valid repos get written"""
2002+ cfg = self._get_base_config_repos()
2003+ root_d = self.tmp_dir()
2004+ cc_zypper_add_repo._write_repos(cfg['zypper']['repos'], root_d)
2005+ repos = glob.glob('%s/*.repo' % root_d)
2006+ expected_repos = ['testing-foo.repo', 'testing-bar.repo']
2007+ if len(repos) != 2:
2008+ assert 'Number of repos written is "%d" expected 2' % len(repos)
2009+ for repo in repos:
2010+ repo_name = os.path.basename(repo)
2011+ if repo_name not in expected_repos:
2012+ assert 'Found repo with name "%s"; unexpected' % repo_name
2013+ # Validation that the content gets properly written is in another test
2014+
2015+ def test_write_repo(self):
2016+ """Verify the content of a repo file"""
2017+ cfg = {
2018+ 'repos': [
2019+ {
2020+ 'baseurl': 'http://foo',
2021+ 'name': 'test-foo',
2022+ 'id': 'testing-foo'
2023+ },
2024+ ]
2025+ }
2026+ root_d = self.tmp_dir()
2027+ cc_zypper_add_repo._write_repos(cfg['repos'], root_d)
2028+ contents = util.load_file("%s/testing-foo.repo" % root_d)
2029+ parser = ConfigParser()
2030+ parser.readfp(StringIO(contents))
2031+ expected = {
2032+ 'testing-foo': {
2033+ 'name': 'test-foo',
2034+ 'baseurl': 'http://foo',
2035+ 'enabled': '1',
2036+ 'autorefresh': '1'
2037+ }
2038+ }
2039+ for section in expected:
2040+ self.assertTrue(parser.has_section(section),
2041+ "Contains section {0}".format(section))
2042+ for k, v in expected[section].items():
2043+ self.assertEqual(parser.get(section, k), v)
2044+
2045+ def test_config_write(self):
2046+ """Write valid configuration data"""
2047+ cfg = {
2048+ 'config': {
2049+ 'download.deltarpm': 'False',
2050+ 'reposdir': 'foo'
2051+ }
2052+ }
2053+ root_d = self.tmp_dir()
2054+ helpers.populate_dir(root_d, {self.zypp_conf: '# Zypp config\n'})
2055+ self.reRoot(root_d)
2056+ cc_zypper_add_repo._write_zypp_config(cfg['config'])
2057+ cfg_out = os.path.join(root_d, self.zypp_conf)
2058+ contents = util.load_file(cfg_out)
2059+ expected = [
2060+ '# Zypp config',
2061+ '# Added via cloud.cfg',
2062+ 'download.deltarpm=False',
2063+ 'reposdir=foo'
2064+ ]
2065+ for item in contents.split('\n'):
2066+ if item not in expected:
2067+ self.assertIsNone(item)
2068+
2069+ @mock.patch('cloudinit.log.logging')
2070+ def test_config_write_skip_configdir(self, mock_logging):
2071+ """Write configuration but skip writing 'configdir' setting"""
2072+ cfg = {
2073+ 'config': {
2074+ 'download.deltarpm': 'False',
2075+ 'reposdir': 'foo',
2076+ 'configdir': 'bar'
2077+ }
2078+ }
2079+ root_d = self.tmp_dir()
2080+ helpers.populate_dir(root_d, {self.zypp_conf: '# Zypp config\n'})
2081+ self.reRoot(root_d)
2082+ cc_zypper_add_repo._write_zypp_config(cfg['config'])
2083+ cfg_out = os.path.join(root_d, self.zypp_conf)
2084+ contents = util.load_file(cfg_out)
2085+ expected = [
2086+ '# Zypp config',
2087+ '# Added via cloud.cfg',
2088+ 'download.deltarpm=False',
2089+ 'reposdir=foo'
2090+ ]
2091+ for item in contents.split('\n'):
2092+ if item not in expected:
2093+ self.assertIsNone(item)
2094+ # Not finding teh right path for mocking :(
2095+ # assert mock_logging.warning.called
2096+
2097+ def test_empty_config_section_no_new_data(self):
2098+ """When the config section is empty no new data should be written to
2099+ zypp.conf"""
2100+ cfg = self._get_base_config_repos()
2101+ cfg['zypper']['config'] = None
2102+ root_d = self.tmp_dir()
2103+ helpers.populate_dir(root_d, {self.zypp_conf: '# No data'})
2104+ self.reRoot(root_d)
2105+ cc_zypper_add_repo._write_zypp_config(cfg.get('config', {}))
2106+ cfg_out = os.path.join(root_d, self.zypp_conf)
2107+ contents = util.load_file(cfg_out)
2108+ self.assertEqual(contents, '# No data')
2109+
2110+ def test_empty_config_value_no_new_data(self):
2111+ """When the config section is not empty but there are no values
2112+ no new data should be written to zypp.conf"""
2113+ cfg = self._get_base_config_repos()
2114+ cfg['zypper']['config'] = {
2115+ 'download.deltarpm': None
2116+ }
2117+ root_d = self.tmp_dir()
2118+ helpers.populate_dir(root_d, {self.zypp_conf: '# No data'})
2119+ self.reRoot(root_d)
2120+ cc_zypper_add_repo._write_zypp_config(cfg.get('config', {}))
2121+ cfg_out = os.path.join(root_d, self.zypp_conf)
2122+ contents = util.load_file(cfg_out)
2123+ self.assertEqual(contents, '# No data')
2124+
2125+ def test_handler_full_setup(self):
2126+ """Test that the handler ends up calling the renderers"""
2127+ cfg = self._get_base_config_repos()
2128+ cfg['zypper']['config'] = {
2129+ 'download.deltarpm': 'False',
2130+ }
2131+ root_d = self.tmp_dir()
2132+ os.makedirs('%s/etc/zypp/repos.d' % root_d)
2133+ helpers.populate_dir(root_d, {self.zypp_conf: '# Zypp config\n'})
2134+ self.reRoot(root_d)
2135+ cc_zypper_add_repo.handle('zypper_add_repo', cfg, None, LOG, [])
2136+ cfg_out = os.path.join(root_d, self.zypp_conf)
2137+ contents = util.load_file(cfg_out)
2138+ expected = [
2139+ '# Zypp config',
2140+ '# Added via cloud.cfg',
2141+ 'download.deltarpm=False',
2142+ ]
2143+ for item in contents.split('\n'):
2144+ if item not in expected:
2145+ self.assertIsNone(item)
2146+ repos = glob.glob('%s/etc/zypp/repos.d/*.repo' % root_d)
2147+ expected_repos = ['testing-foo.repo', 'testing-bar.repo']
2148+ if len(repos) != 2:
2149+ assert 'Number of repos written is "%d" expected 2' % len(repos)
2150+ for repo in repos:
2151+ repo_name = os.path.basename(repo)
2152+ if repo_name not in expected_repos:
2153+ assert 'Found repo with name "%s"; unexpected' % repo_name
2154+
2155+ def test_no_config_section_no_new_data(self):
2156+ """When there is no config section no new data should be written to
2157+ zypp.conf"""
2158+ cfg = self._get_base_config_repos()
2159+ root_d = self.tmp_dir()
2160+ helpers.populate_dir(root_d, {self.zypp_conf: '# No data'})
2161+ self.reRoot(root_d)
2162+ cc_zypper_add_repo._write_zypp_config(cfg.get('config', {}))
2163+ cfg_out = os.path.join(root_d, self.zypp_conf)
2164+ contents = util.load_file(cfg_out)
2165+ self.assertEqual(contents, '# No data')
2166+
2167+ def test_no_repo_data(self):
2168+ """When there is no repo data nothing should happen"""
2169+ root_d = self.tmp_dir()
2170+ self.reRoot(root_d)
2171+ cc_zypper_add_repo._write_repos(None, root_d)
2172+ content = glob.glob('%s/*' % root_d)
2173+ self.assertEqual(len(content), 0)
2174+
2175+ def _get_base_config_repos(self):
2176+ """Basic valid repo configuration"""
2177+ cfg = {
2178+ 'zypper': {
2179+ 'repos': [
2180+ {
2181+ 'baseurl': 'http://foo',
2182+ 'name': 'test-foo',
2183+ 'id': 'testing-foo'
2184+ },
2185+ {
2186+ 'baseurl': 'http://bar',
2187+ 'name': 'test-bar',
2188+ 'id': 'testing-bar'
2189+ }
2190+ ]
2191+ }
2192+ }
2193+ return cfg
2194diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py
2195index 745bb0f..b8fc893 100644
2196--- a/tests/unittests/test_handler/test_schema.py
2197+++ b/tests/unittests/test_handler/test_schema.py
2198@@ -27,7 +27,13 @@ class GetSchemaTest(CiTestCase):
2199 """Every cloudconfig module with schema is listed in allOf keyword."""
2200 schema = get_schema()
2201 self.assertItemsEqual(
2202- ['cc_bootcmd', 'cc_ntp', 'cc_resizefs', 'cc_runcmd'],
2203+ [
2204+ 'cc_bootcmd',
2205+ 'cc_ntp',
2206+ 'cc_resizefs',
2207+ 'cc_runcmd',
2208+ 'cc_zypper_add_repo'
2209+ ],
2210 [subschema['id'] for subschema in schema['allOf']])
2211 self.assertEqual('cloud-config-schema', schema['id'])
2212 self.assertEqual(
2213diff --git a/tools/build-on-freebsd b/tools/build-on-freebsd
2214index ff9153a..d23fde2 100755
2215--- a/tools/build-on-freebsd
2216+++ b/tools/build-on-freebsd
2217@@ -18,7 +18,6 @@ pkgs="
2218 py27-jsonpatch
2219 py27-jsonpointer
2220 py27-oauthlib
2221- py27-prettytable
2222 py27-requests
2223 py27-serial
2224 py27-six
2225diff --git a/tox.ini b/tox.ini
2226index 776f425..aef1f84 100644
2227--- a/tox.ini
2228+++ b/tox.ini
2229@@ -64,7 +64,6 @@ deps =
2230 # requirements
2231 jinja2==2.8
2232 pyyaml==3.11
2233- PrettyTable==0.7.2
2234 oauthlib==1.0.3
2235 pyserial==3.0.1
2236 configobj==5.0.6
2237@@ -89,7 +88,6 @@ deps =
2238 argparse==1.2.1
2239 jinja2==2.2.1
2240 pyyaml==3.10
2241- PrettyTable==0.7.2
2242 oauthlib==0.6.0
2243 configobj==4.6.0
2244 requests==2.6.0
2245@@ -105,7 +103,6 @@ deps =
2246 argparse==1.3.0
2247 jinja2==2.8
2248 PyYAML==3.11
2249- PrettyTable==0.7.2
2250 oauthlib==0.7.2
2251 configobj==5.0.6
2252 requests==2.11.1

Subscribers

People subscribed via source and target branches