Merge ~chad.smith/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel

Proposed by Chad Smith
Status: Merged
Merged at revision: 555b756bde681bcabd68cc5fab5a177bd73c6049
Proposed branch: ~chad.smith/cloud-init:ubuntu/devel
Merge into: cloud-init:ubuntu/devel
Diff against target: 1818 lines (+897/-236)
35 files modified
MANIFEST.in (+1/-0)
bash_completion/cloud-init (+77/-0)
cloudinit/analyze/__main__.py (+1/-1)
cloudinit/config/cc_apt_configure.py (+1/-1)
cloudinit/config/cc_disable_ec2_metadata.py (+12/-2)
cloudinit/config/cc_power_state_change.py (+1/-1)
cloudinit/config/cc_rsyslog.py (+2/-2)
cloudinit/config/tests/test_disable_ec2_metadata.py (+50/-0)
cloudinit/distros/freebsd.py (+3/-3)
cloudinit/net/network_state.py (+5/-6)
cloudinit/netinfo.py (+273/-72)
cloudinit/sources/DataSourceSmartOS.py (+103/-16)
cloudinit/tests/helpers.py (+14/-26)
cloudinit/tests/test_netinfo.py (+101/-85)
cloudinit/util.py (+3/-3)
debian/changelog (+13/-0)
doc/examples/cloud-config-disk-setup.txt (+2/-2)
packages/redhat/cloud-init.spec.in (+1/-0)
packages/suse/cloud-init.spec.in (+1/-0)
setup.py (+1/-0)
tests/cloud_tests/testcases/base.py (+1/-1)
tests/data/netinfo/netdev-formatted-output (+10/-0)
tests/data/netinfo/new-ifconfig-output (+18/-0)
tests/data/netinfo/old-ifconfig-output (+18/-0)
tests/data/netinfo/route-formatted-output (+22/-0)
tests/data/netinfo/sample-ipaddrshow-output (+13/-0)
tests/data/netinfo/sample-iproute-output-v4 (+3/-0)
tests/data/netinfo/sample-iproute-output-v6 (+11/-0)
tests/data/netinfo/sample-route-output-v4 (+5/-0)
tests/data/netinfo/sample-route-output-v6 (+13/-0)
tests/unittests/test_datasource/test_smartos.py (+102/-1)
tests/unittests/test_filters/test_launch_index.py (+5/-5)
tests/unittests/test_merging.py (+1/-1)
tests/unittests/test_runs/test_merge_run.py (+1/-1)
tests/unittests/test_util.py (+9/-7)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
cloud-init Commiters Pending
Review via email: mp+343562@code.launchpad.net

Commit message

Sync bugfixes from master into Bionic for release

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:555b756bde681bcabd68cc5fab5a177bd73c6049
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1027/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/MANIFEST.in b/MANIFEST.in
2index 1a4d771..57a85ea 100644
3--- a/MANIFEST.in
4+++ b/MANIFEST.in
5@@ -1,5 +1,6 @@
6 include *.py MANIFEST.in LICENSE* ChangeLog
7 global-include *.txt *.rst *.ini *.in *.conf *.cfg *.sh
8+graft bash_completion
9 graft config
10 graft doc
11 graft packages
12diff --git a/bash_completion/cloud-init b/bash_completion/cloud-init
13new file mode 100644
14index 0000000..581432c
15--- /dev/null
16+++ b/bash_completion/cloud-init
17@@ -0,0 +1,77 @@
18+# Copyright (C) 2018 Canonical Ltd.
19+#
20+# This file is part of cloud-init. See LICENSE file for license information.
21+
22+# bash completion for cloud-init cli
23+_cloudinit_complete()
24+{
25+
26+ local cur_word prev_word
27+ cur_word="${COMP_WORDS[COMP_CWORD]}"
28+ prev_word="${COMP_WORDS[COMP_CWORD-1]}"
29+
30+ subcmds="analyze clean collect-logs devel dhclient-hook features init modules single status"
31+ base_params="--help --file --version --debug --force"
32+ case ${COMP_CWORD} in
33+ 1)
34+ COMPREPLY=($(compgen -W "$base_params $subcmds" -- $cur_word))
35+ ;;
36+ 2)
37+ case ${prev_word} in
38+ analyze)
39+ COMPREPLY=($(compgen -W "--help blame dump show" -- $cur_word))
40+ ;;
41+ clean)
42+ COMPREPLY=($(compgen -W "--help --logs --reboot --seed" -- $cur_word))
43+ ;;
44+ collect-logs)
45+ COMPREPLY=($(compgen -W "--help --tarfile --include-userdata" -- $cur_word))
46+ ;;
47+ devel)
48+ COMPREPLY=($(compgen -W "--help schema" -- $cur_word))
49+ ;;
50+ dhclient-hook|features)
51+ COMPREPLY=($(compgen -W "--help" -- $cur_word))
52+ ;;
53+ init)
54+ COMPREPLY=($(compgen -W "--help --local" -- $cur_word))
55+ ;;
56+ modules)
57+ COMPREPLY=($(compgen -W "--help --mode" -- $cur_word))
58+ ;;
59+
60+ single)
61+ COMPREPLY=($(compgen -W "--help --name --frequency --report" -- $cur_word))
62+ ;;
63+ status)
64+ COMPREPLY=($(compgen -W "--help --long --wait" -- $cur_word))
65+ ;;
66+ esac
67+ ;;
68+ 3)
69+ case ${prev_word} in
70+ blame|dump)
71+ COMPREPLY=($(compgen -W "--help --infile --outfile" -- $cur_word))
72+ ;;
73+ --mode)
74+ COMPREPLY=($(compgen -W "--help init config final" -- $cur_word))
75+ ;;
76+ --frequency)
77+ COMPREPLY=($(compgen -W "--help instance always once" -- $cur_word))
78+ ;;
79+ schema)
80+ COMPREPLY=($(compgen -W "--help --config-file --doc --annotate" -- $cur_word))
81+ ;;
82+ show)
83+ COMPREPLY=($(compgen -W "--help --format --infile --outfile" -- $cur_word))
84+ ;;
85+ esac
86+ ;;
87+ *)
88+ COMPREPLY=()
89+ ;;
90+ esac
91+}
92+complete -F _cloudinit_complete cloud-init
93+
94+# vi: syntax=bash expandtab
95diff --git a/cloudinit/analyze/__main__.py b/cloudinit/analyze/__main__.py
96index 3ba5903..f861365 100644
97--- a/cloudinit/analyze/__main__.py
98+++ b/cloudinit/analyze/__main__.py
99@@ -69,7 +69,7 @@ def analyze_blame(name, args):
100 """
101 (infh, outfh) = configure_io(args)
102 blame_format = ' %ds (%n)'
103- r = re.compile('(^\s+\d+\.\d+)', re.MULTILINE)
104+ r = re.compile(r'(^\s+\d+\.\d+)', re.MULTILINE)
105 for idx, record in enumerate(show.show_events(_get_events(infh),
106 blame_format)):
107 srecs = sorted(filter(r.match, record), reverse=True)
108diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py
109index 5b9cbca..afaca46 100644
110--- a/cloudinit/config/cc_apt_configure.py
111+++ b/cloudinit/config/cc_apt_configure.py
112@@ -121,7 +121,7 @@ and https protocols respectively. The ``proxy`` key also exists as an alias for
113 All source entries in ``apt-sources`` that match regex in
114 ``add_apt_repo_match`` will be added to the system using
115 ``add-apt-repository``. If ``add_apt_repo_match`` is not specified, it defaults
116-to ``^[\w-]+:\w``
117+to ``^[\\w-]+:\\w``
118
119 **Add source list entries:**
120
121diff --git a/cloudinit/config/cc_disable_ec2_metadata.py b/cloudinit/config/cc_disable_ec2_metadata.py
122index c56319b..885b313 100644
123--- a/cloudinit/config/cc_disable_ec2_metadata.py
124+++ b/cloudinit/config/cc_disable_ec2_metadata.py
125@@ -32,13 +32,23 @@ from cloudinit.settings import PER_ALWAYS
126
127 frequency = PER_ALWAYS
128
129-REJECT_CMD = ['route', 'add', '-host', '169.254.169.254', 'reject']
130+REJECT_CMD_IF = ['route', 'add', '-host', '169.254.169.254', 'reject']
131+REJECT_CMD_IP = ['ip', 'route', 'add', 'prohibit', '169.254.169.254']
132
133
134 def handle(name, cfg, _cloud, log, _args):
135 disabled = util.get_cfg_option_bool(cfg, "disable_ec2_metadata", False)
136 if disabled:
137- util.subp(REJECT_CMD, capture=False)
138+ reject_cmd = None
139+ if util.which('ip'):
140+ reject_cmd = REJECT_CMD_IP
141+ elif util.which('ifconfig'):
142+ reject_cmd = REJECT_CMD_IF
143+ else:
144+ log.error(('Neither "route" nor "ip" command found, unable to '
145+ 'manipulate routing table'))
146+ return
147+ util.subp(reject_cmd, capture=False)
148 else:
149 log.debug(("Skipping module named %s,"
150 " disabling the ec2 route not enabled"), name)
151diff --git a/cloudinit/config/cc_power_state_change.py b/cloudinit/config/cc_power_state_change.py
152index 4da3a58..50b3747 100644
153--- a/cloudinit/config/cc_power_state_change.py
154+++ b/cloudinit/config/cc_power_state_change.py
155@@ -74,7 +74,7 @@ def givecmdline(pid):
156 if util.is_FreeBSD():
157 (output, _err) = util.subp(['procstat', '-c', str(pid)])
158 line = output.splitlines()[1]
159- m = re.search('\d+ (\w|\.|-)+\s+(/\w.+)', line)
160+ m = re.search(r'\d+ (\w|\.|-)+\s+(/\w.+)', line)
161 return m.group(2)
162 else:
163 return util.load_file("/proc/%s/cmdline" % pid)
164diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py
165index af08788..27d2366 100644
166--- a/cloudinit/config/cc_rsyslog.py
167+++ b/cloudinit/config/cc_rsyslog.py
168@@ -203,8 +203,8 @@ LOG = logging.getLogger(__name__)
169 COMMENT_RE = re.compile(r'[ ]*[#]+[ ]*')
170 HOST_PORT_RE = re.compile(
171 r'^(?P<proto>[@]{0,2})'
172- '(([[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'
173- '([:](?P<port>[0-9]+))?$')
174+ r'(([[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'
175+ r'([:](?P<port>[0-9]+))?$')
176
177
178 def reload_syslog(command=DEF_RELOAD, systemd=False):
179diff --git a/cloudinit/config/tests/test_disable_ec2_metadata.py b/cloudinit/config/tests/test_disable_ec2_metadata.py
180new file mode 100644
181index 0000000..67646b0
182--- /dev/null
183+++ b/cloudinit/config/tests/test_disable_ec2_metadata.py
184@@ -0,0 +1,50 @@
185+# This file is part of cloud-init. See LICENSE file for license information.
186+
187+"""Tests cc_disable_ec2_metadata handler"""
188+
189+import cloudinit.config.cc_disable_ec2_metadata as ec2_meta
190+
191+from cloudinit.tests.helpers import CiTestCase, mock
192+
193+import logging
194+
195+LOG = logging.getLogger(__name__)
196+
197+DISABLE_CFG = {'disable_ec2_metadata': 'true'}
198+
199+
200+class TestEC2MetadataRoute(CiTestCase):
201+
202+ with_logs = True
203+
204+ @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.which')
205+ @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.subp')
206+ def test_disable_ifconfig(self, m_subp, m_which):
207+ """Set the route if ifconfig command is available"""
208+ m_which.side_effect = lambda x: x if x == 'ifconfig' else None
209+ ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
210+ m_subp.assert_called_with(
211+ ['route', 'add', '-host', '169.254.169.254', 'reject'],
212+ capture=False)
213+
214+ @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.which')
215+ @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.subp')
216+ def test_disable_ip(self, m_subp, m_which):
217+ """Set the route if ip command is available"""
218+ m_which.side_effect = lambda x: x if x == 'ip' else None
219+ ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
220+ m_subp.assert_called_with(
221+ ['ip', 'route', 'add', 'prohibit', '169.254.169.254'],
222+ capture=False)
223+
224+ @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.which')
225+ @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.subp')
226+ def test_disable_no_tool(self, m_subp, m_which):
227+ """Log error when neither route nor ip commands are available"""
228+ m_which.return_value = None # Find neither ifconfig nor ip
229+ ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
230+ self.assertEqual(
231+ [mock.call('ip'), mock.call('ifconfig')], m_which.call_args_list)
232+ m_subp.assert_not_called()
233+
234+# vi: ts=4 expandtab
235diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py
236index 754d3df..099fac5 100644
237--- a/cloudinit/distros/freebsd.py
238+++ b/cloudinit/distros/freebsd.py
239@@ -110,7 +110,7 @@ class Distro(distros.Distro):
240 if dev.startswith('lo'):
241 return dev
242
243- n = re.search('\d+$', dev)
244+ n = re.search(r'\d+$', dev)
245 index = n.group(0)
246
247 (out, err) = util.subp(['ifconfig', '-a'])
248@@ -118,7 +118,7 @@ class Distro(distros.Distro):
249 if len(x.split()) > 0]
250 bsddev = 'NOT_FOUND'
251 for line in ifconfigoutput:
252- m = re.match('^\w+', line)
253+ m = re.match(r'^\w+', line)
254 if m:
255 if m.group(0).startswith('lo'):
256 continue
257@@ -128,7 +128,7 @@ class Distro(distros.Distro):
258 break
259
260 # Replace the index with the one we're after.
261- bsddev = re.sub('\d+$', index, bsddev)
262+ bsddev = re.sub(r'\d+$', index, bsddev)
263 LOG.debug("Using network interface %s", bsddev)
264 return bsddev
265
266diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
267index 6d63e5c..72c803e 100644
268--- a/cloudinit/net/network_state.py
269+++ b/cloudinit/net/network_state.py
270@@ -7,6 +7,8 @@
271 import copy
272 import functools
273 import logging
274+import socket
275+import struct
276
277 import six
278
279@@ -886,12 +888,9 @@ def net_prefix_to_ipv4_mask(prefix):
280 This is the inverse of ipv4_mask_to_net_prefix.
281 24 -> "255.255.255.0"
282 Also supports input as a string."""
283-
284- mask = [0, 0, 0, 0]
285- for i in list(range(0, int(prefix))):
286- idx = int(i / 8)
287- mask[idx] = mask[idx] + (1 << (7 - i % 8))
288- return ".".join([str(x) for x in mask])
289+ mask = socket.inet_ntoa(
290+ struct.pack(">I", (0xffffffff << (32 - int(prefix)) & 0xffffffff)))
291+ return mask
292
293
294 def ipv4_mask_to_net_prefix(mask):
295diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
296index 993b26c..f090616 100644
297--- a/cloudinit/netinfo.py
298+++ b/cloudinit/netinfo.py
299@@ -8,9 +8,11 @@
300 #
301 # This file is part of cloud-init. See LICENSE file for license information.
302
303+from copy import copy, deepcopy
304 import re
305
306 from cloudinit import log as logging
307+from cloudinit.net.network_state import net_prefix_to_ipv4_mask
308 from cloudinit import util
309
310 from cloudinit.simpletable import SimpleTable
311@@ -18,18 +20,90 @@ from cloudinit.simpletable import SimpleTable
312 LOG = logging.getLogger()
313
314
315-def netdev_info(empty=""):
316- fields = ("hwaddr", "addr", "bcast", "mask")
317- (ifcfg_out, _err) = util.subp(["ifconfig", "-a"], rcs=[0, 1])
318+DEFAULT_NETDEV_INFO = {
319+ "ipv4": [],
320+ "ipv6": [],
321+ "hwaddr": "",
322+ "up": False
323+}
324+
325+
326+def _netdev_info_iproute(ipaddr_out):
327+ """
328+ Get network device dicts from ip route and ip link info.
329+
330+ @param ipaddr_out: Output string from 'ip addr show' command.
331+
332+ @returns: A dict of device info keyed by network device name containing
333+ device configuration values.
334+ @raise: TypeError if ipaddr_out isn't a string.
335+ """
336 devs = {}
337- for line in str(ifcfg_out).splitlines():
338+ dev_name = None
339+ for num, line in enumerate(ipaddr_out.splitlines()):
340+ m = re.match(r'^\d+:\s(?P<dev>[^:]+):\s+<(?P<flags>\S+)>\s+.*', line)
341+ if m:
342+ dev_name = m.group('dev').lower().split('@')[0]
343+ flags = m.group('flags').split(',')
344+ devs[dev_name] = {
345+ 'ipv4': [], 'ipv6': [], 'hwaddr': '',
346+ 'up': bool('UP' in flags and 'LOWER_UP' in flags),
347+ }
348+ elif 'inet6' in line:
349+ m = re.match(
350+ r'\s+inet6\s(?P<ip>\S+)\sscope\s(?P<scope6>\S+).*', line)
351+ if not m:
352+ LOG.warning(
353+ 'Could not parse ip addr show: (line:%d) %s', num, line)
354+ continue
355+ devs[dev_name]['ipv6'].append(m.groupdict())
356+ elif 'inet' in line:
357+ m = re.match(
358+ r'\s+inet\s(?P<cidr4>\S+)(\sbrd\s(?P<bcast>\S+))?\sscope\s'
359+ r'(?P<scope>\S+).*', line)
360+ if not m:
361+ LOG.warning(
362+ 'Could not parse ip addr show: (line:%d) %s', num, line)
363+ continue
364+ match = m.groupdict()
365+ cidr4 = match.pop('cidr4')
366+ addr, _, prefix = cidr4.partition('/')
367+ if not prefix:
368+ prefix = '32'
369+ devs[dev_name]['ipv4'].append({
370+ 'ip': addr,
371+ 'bcast': match['bcast'] if match['bcast'] else '',
372+ 'mask': net_prefix_to_ipv4_mask(prefix),
373+ 'scope': match['scope']})
374+ elif 'link' in line:
375+ m = re.match(
376+ r'\s+link/(?P<link_type>\S+)\s(?P<hwaddr>\S+).*', line)
377+ if not m:
378+ LOG.warning(
379+ 'Could not parse ip addr show: (line:%d) %s', num, line)
380+ continue
381+ if m.group('link_type') == 'ether':
382+ devs[dev_name]['hwaddr'] = m.group('hwaddr')
383+ else:
384+ devs[dev_name]['hwaddr'] = ''
385+ else:
386+ continue
387+ return devs
388+
389+
390+def _netdev_info_ifconfig(ifconfig_data):
391+ # fields that need to be returned in devs for each dev
392+ devs = {}
393+ for line in ifconfig_data.splitlines():
394 if len(line) == 0:
395 continue
396 if line[0] not in ("\t", " "):
397 curdev = line.split()[0]
398- devs[curdev] = {"up": False}
399- for field in fields:
400- devs[curdev][field] = ""
401+ # current ifconfig pops a ':' on the end of the device
402+ if curdev.endswith(':'):
403+ curdev = curdev[:-1]
404+ if curdev not in devs:
405+ devs[curdev] = deepcopy(DEFAULT_NETDEV_INFO)
406 toks = line.lower().strip().split()
407 if toks[0] == "up":
408 devs[curdev]['up'] = True
409@@ -39,41 +113,50 @@ def netdev_info(empty=""):
410 if re.search(r"flags=\d+<up,", toks[1]):
411 devs[curdev]['up'] = True
412
413- fieldpost = ""
414- if toks[0] == "inet6":
415- fieldpost = "6"
416-
417 for i in range(len(toks)):
418- # older net-tools (ubuntu) show 'inet addr:xx.yy',
419- # newer (freebsd and fedora) show 'inet xx.yy'
420- # just skip this 'inet' entry. (LP: #1285185)
421- try:
422- if ((toks[i] in ("inet", "inet6") and
423- toks[i + 1].startswith("addr:"))):
424- continue
425- except IndexError:
426- pass
427-
428- # Couple the different items we're interested in with the correct
429- # field since FreeBSD/CentOS/Fedora differ in the output.
430- ifconfigfields = {
431- "addr:": "addr", "inet": "addr",
432- "bcast:": "bcast", "broadcast": "bcast",
433- "mask:": "mask", "netmask": "mask",
434- "hwaddr": "hwaddr", "ether": "hwaddr",
435- "scope": "scope",
436- }
437- for origfield, field in ifconfigfields.items():
438- target = "%s%s" % (field, fieldpost)
439- if devs[curdev].get(target, ""):
440- continue
441- if toks[i] == "%s" % origfield:
442- try:
443- devs[curdev][target] = toks[i + 1]
444- except IndexError:
445- pass
446- elif toks[i].startswith("%s" % origfield):
447- devs[curdev][target] = toks[i][len(field) + 1:]
448+ if toks[i] == "inet": # Create new ipv4 addr entry
449+ devs[curdev]['ipv4'].append(
450+ {'ip': toks[i + 1].lstrip("addr:")})
451+ elif toks[i].startswith("bcast:"):
452+ devs[curdev]['ipv4'][-1]['bcast'] = toks[i].lstrip("bcast:")
453+ elif toks[i] == "broadcast":
454+ devs[curdev]['ipv4'][-1]['bcast'] = toks[i + 1]
455+ elif toks[i].startswith("mask:"):
456+ devs[curdev]['ipv4'][-1]['mask'] = toks[i].lstrip("mask:")
457+ elif toks[i] == "netmask":
458+ devs[curdev]['ipv4'][-1]['mask'] = toks[i + 1]
459+ elif toks[i] == "hwaddr" or toks[i] == "ether":
460+ devs[curdev]['hwaddr'] = toks[i + 1]
461+ elif toks[i] == "inet6":
462+ if toks[i + 1] == "addr:":
463+ devs[curdev]['ipv6'].append({'ip': toks[i + 2]})
464+ else:
465+ devs[curdev]['ipv6'].append({'ip': toks[i + 1]})
466+ elif toks[i] == "prefixlen": # Add prefix to current ipv6 value
467+ addr6 = devs[curdev]['ipv6'][-1]['ip'] + "/" + toks[i + 1]
468+ devs[curdev]['ipv6'][-1]['ip'] = addr6
469+ elif toks[i].startswith("scope:"):
470+ devs[curdev]['ipv6'][-1]['scope6'] = toks[i].lstrip("scope:")
471+ elif toks[i] == "scopeid":
472+ res = re.match(".*<(\S+)>", toks[i + 1])
473+ if res:
474+ devs[curdev]['ipv6'][-1]['scope6'] = res.group(1)
475+ return devs
476+
477+
478+def netdev_info(empty=""):
479+ devs = {}
480+ if util.which('ip'):
481+ # Try iproute first of all
482+ (ipaddr_out, _err) = util.subp(["ip", "addr", "show"])
483+ devs = _netdev_info_iproute(ipaddr_out)
484+ elif util.which('ifconfig'):
485+ # Fall back to net-tools if iproute2 is not present
486+ (ifcfg_out, _err) = util.subp(["ifconfig", "-a"], rcs=[0, 1])
487+ devs = _netdev_info_ifconfig(ifcfg_out)
488+ else:
489+ LOG.warning(
490+ "Could not print networks: missing 'ip' and 'ifconfig' commands")
491
492 if empty != "":
493 for (_devname, dev) in devs.items():
494@@ -84,14 +167,94 @@ def netdev_info(empty=""):
495 return devs
496
497
498-def route_info():
499- (route_out, _err) = util.subp(["netstat", "-rn"], rcs=[0, 1])
500+def _netdev_route_info_iproute(iproute_data):
501+ """
502+ Get network route dicts from ip route info.
503+
504+ @param iproute_data: Output string from ip route command.
505+
506+ @returns: A dict containing ipv4 and ipv6 route entries as lists. Each
507+ item in the list is a route dictionary representing destination,
508+ gateway, flags, genmask and interface information.
509+ """
510+
511+ routes = {}
512+ routes['ipv4'] = []
513+ routes['ipv6'] = []
514+ entries = iproute_data.splitlines()
515+ default_route_entry = {
516+ 'destination': '', 'flags': '', 'gateway': '', 'genmask': '',
517+ 'iface': '', 'metric': ''}
518+ for line in entries:
519+ entry = copy(default_route_entry)
520+ if not line:
521+ continue
522+ toks = line.split()
523+ flags = ['U']
524+ if toks[0] == "default":
525+ entry['destination'] = "0.0.0.0"
526+ entry['genmask'] = "0.0.0.0"
527+ else:
528+ if '/' in toks[0]:
529+ (addr, cidr) = toks[0].split("/")
530+ else:
531+ addr = toks[0]
532+ cidr = '32'
533+ flags.append("H")
534+ entry['genmask'] = net_prefix_to_ipv4_mask(cidr)
535+ entry['destination'] = addr
536+ entry['genmask'] = net_prefix_to_ipv4_mask(cidr)
537+ entry['gateway'] = "0.0.0.0"
538+ for i in range(len(toks)):
539+ if toks[i] == "via":
540+ entry['gateway'] = toks[i + 1]
541+ flags.insert(1, "G")
542+ if toks[i] == "dev":
543+ entry["iface"] = toks[i + 1]
544+ if toks[i] == "metric":
545+ entry['metric'] = toks[i + 1]
546+ entry['flags'] = ''.join(flags)
547+ routes['ipv4'].append(entry)
548+ try:
549+ (iproute_data6, _err6) = util.subp(
550+ ["ip", "--oneline", "-6", "route", "list", "table", "all"],
551+ rcs=[0, 1])
552+ except util.ProcessExecutionError:
553+ pass
554+ else:
555+ entries6 = iproute_data6.splitlines()
556+ for line in entries6:
557+ entry = {}
558+ if not line:
559+ continue
560+ toks = line.split()
561+ if toks[0] == "default":
562+ entry['destination'] = "::/0"
563+ entry['flags'] = "UG"
564+ else:
565+ entry['destination'] = toks[0]
566+ entry['gateway'] = "::"
567+ entry['flags'] = "U"
568+ for i in range(len(toks)):
569+ if toks[i] == "via":
570+ entry['gateway'] = toks[i + 1]
571+ entry['flags'] = "UG"
572+ if toks[i] == "dev":
573+ entry["iface"] = toks[i + 1]
574+ if toks[i] == "metric":
575+ entry['metric'] = toks[i + 1]
576+ if toks[i] == "expires":
577+ entry['flags'] = entry['flags'] + 'e'
578+ routes['ipv6'].append(entry)
579+ return routes
580
581+
582+def _netdev_route_info_netstat(route_data):
583 routes = {}
584 routes['ipv4'] = []
585 routes['ipv6'] = []
586
587- entries = route_out.splitlines()[1:]
588+ entries = route_data.splitlines()
589 for line in entries:
590 if not line:
591 continue
592@@ -101,8 +264,8 @@ def route_info():
593 # default 10.65.0.1 UGS 0 34920 vtnet0
594 #
595 # Linux netstat shows 2 more:
596- # Destination Gateway Genmask Flags MSS Window irtt Iface
597- # 0.0.0.0 10.65.0.1 0.0.0.0 UG 0 0 0 eth0
598+ # Destination Gateway Genmask Flags Metric Ref Use Iface
599+ # 0.0.0.0 10.65.0.1 0.0.0.0 UG 0 0 0 eth0
600 if (len(toks) < 6 or toks[0] == "Kernel" or
601 toks[0] == "Destination" or toks[0] == "Internet" or
602 toks[0] == "Internet6" or toks[0] == "Routing"):
603@@ -125,31 +288,57 @@ def route_info():
604 routes['ipv4'].append(entry)
605
606 try:
607- (route_out6, _err6) = util.subp(["netstat", "-A", "inet6", "-n"],
608- rcs=[0, 1])
609+ (route_data6, _err6) = util.subp(
610+ ["netstat", "-A", "inet6", "--route", "--numeric"], rcs=[0, 1])
611 except util.ProcessExecutionError:
612 pass
613 else:
614- entries6 = route_out6.splitlines()[1:]
615+ entries6 = route_data6.splitlines()
616 for line in entries6:
617 if not line:
618 continue
619 toks = line.split()
620- if (len(toks) < 6 or toks[0] == "Kernel" or
621+ if (len(toks) < 7 or toks[0] == "Kernel" or
622+ toks[0] == "Destination" or toks[0] == "Internet" or
623 toks[0] == "Proto" or toks[0] == "Active"):
624 continue
625 entry = {
626- 'proto': toks[0],
627- 'recv-q': toks[1],
628- 'send-q': toks[2],
629- 'local address': toks[3],
630- 'foreign address': toks[4],
631- 'state': toks[5],
632+ 'destination': toks[0],
633+ 'gateway': toks[1],
634+ 'flags': toks[2],
635+ 'metric': toks[3],
636+ 'ref': toks[4],
637+ 'use': toks[5],
638+ 'iface': toks[6],
639 }
640+ # skip lo interface on ipv6
641+ if entry['iface'] == "lo":
642+ continue
643+ # strip /128 from address if it's included
644+ if entry['destination'].endswith('/128'):
645+ entry['destination'] = re.sub(
646+ r'\/128$', '', entry['destination'])
647 routes['ipv6'].append(entry)
648 return routes
649
650
651+def route_info():
652+ routes = {}
653+ if util.which('ip'):
654+ # Try iproute first of all
655+ (iproute_out, _err) = util.subp(["ip", "-o", "route", "list"])
656+ routes = _netdev_route_info_iproute(iproute_out)
657+ elif util.which('netstat'):
658+ # Fall back to net-tools if iproute2 is not present
659+ (route_out, _err) = util.subp(
660+ ["netstat", "--route", "--numeric", "--extend"], rcs=[0, 1])
661+ routes = _netdev_route_info_netstat(route_out)
662+ else:
663+ LOG.warning(
664+ "Could not print routes: missing 'ip' and 'netstat' commands")
665+ return routes
666+
667+
668 def getgateway():
669 try:
670 routes = route_info()
671@@ -166,21 +355,30 @@ def netdev_pformat():
672 lines = []
673 try:
674 netdev = netdev_info(empty=".")
675- except Exception:
676- lines.append(util.center("Net device info failed", '!', 80))
677+ except Exception as e:
678+ lines.append(
679+ util.center(
680+ "Net device info failed ({error})".format(error=str(e)),
681+ '!', 80))
682 else:
683+ if not netdev:
684+ return '\n'
685 fields = ['Device', 'Up', 'Address', 'Mask', 'Scope', 'Hw-Address']
686 tbl = SimpleTable(fields)
687- for (dev, d) in sorted(netdev.items()):
688- tbl.add_row([dev, d["up"], d["addr"], d["mask"], ".", d["hwaddr"]])
689- if d.get('addr6'):
690- tbl.add_row([dev, d["up"],
691- d["addr6"], ".", d.get("scope6"), d["hwaddr"]])
692+ for (dev, data) in sorted(netdev.items()):
693+ for addr in data.get('ipv4'):
694+ tbl.add_row(
695+ [dev, data["up"], addr["ip"], addr["mask"],
696+ addr.get('scope', '.'), data["hwaddr"]])
697+ for addr in data.get('ipv6'):
698+ tbl.add_row(
699+ [dev, data["up"], addr["ip"], ".", addr["scope6"],
700+ data["hwaddr"]])
701 netdev_s = tbl.get_string()
702 max_len = len(max(netdev_s.splitlines(), key=len))
703 header = util.center("Net device info", "+", max_len)
704 lines.extend([header, netdev_s])
705- return "\n".join(lines)
706+ return "\n".join(lines) + "\n"
707
708
709 def route_pformat():
710@@ -188,7 +386,10 @@ def route_pformat():
711 try:
712 routes = route_info()
713 except Exception as e:
714- lines.append(util.center('Route info failed', '!', 80))
715+ lines.append(
716+ util.center(
717+ 'Route info failed ({error})'.format(error=str(e)),
718+ '!', 80))
719 util.logexc(LOG, "Route info failed: %s" % e)
720 else:
721 if routes.get('ipv4'):
722@@ -205,20 +406,20 @@ def route_pformat():
723 header = util.center("Route IPv4 info", "+", max_len)
724 lines.extend([header, route_s])
725 if routes.get('ipv6'):
726- fields_v6 = ['Route', 'Proto', 'Recv-Q', 'Send-Q',
727- 'Local Address', 'Foreign Address', 'State']
728+ fields_v6 = ['Route', 'Destination', 'Gateway', 'Interface',
729+ 'Flags']
730 tbl_v6 = SimpleTable(fields_v6)
731 for (n, r) in enumerate(routes.get('ipv6')):
732 route_id = str(n)
733- tbl_v6.add_row([route_id, r['proto'],
734- r['recv-q'], r['send-q'],
735- r['local address'], r['foreign address'],
736- r['state']])
737+ if r['iface'] == 'lo':
738+ continue
739+ tbl_v6.add_row([route_id, r['destination'],
740+ r['gateway'], r['iface'], r['flags']])
741 route_s = tbl_v6.get_string()
742 max_len = len(max(route_s.splitlines(), key=len))
743 header = util.center("Route IPv6 info", "+", max_len)
744 lines.extend([header, route_s])
745- return "\n".join(lines)
746+ return "\n".join(lines) + "\n"
747
748
749 def debug_info(prefix='ci-info: '):
750diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py
751index 86bfa5d..c8998b4 100644
752--- a/cloudinit/sources/DataSourceSmartOS.py
753+++ b/cloudinit/sources/DataSourceSmartOS.py
754@@ -1,4 +1,5 @@
755 # Copyright (C) 2013 Canonical Ltd.
756+# Copyright (c) 2018, Joyent, Inc.
757 #
758 # Author: Ben Howard <ben.howard@canonical.com>
759 #
760@@ -21,6 +22,7 @@
761
762 import base64
763 import binascii
764+import errno
765 import json
766 import os
767 import random
768@@ -108,7 +110,7 @@ BUILTIN_CLOUD_CONFIG = {
769 'overwrite': False}
770 },
771 'fs_setup': [{'label': 'ephemeral0',
772- 'filesystem': 'ext3',
773+ 'filesystem': 'ext4',
774 'device': 'ephemeral0'}],
775 }
776
777@@ -229,6 +231,9 @@ class DataSourceSmartOS(sources.DataSource):
778 self.md_client)
779 return False
780
781+ # Open once for many requests, rather than once for each request
782+ self.md_client.open_transport()
783+
784 for ci_noun, attribute in SMARTOS_ATTRIB_MAP.items():
785 smartos_noun, strip = attribute
786 md[ci_noun] = self.md_client.get(smartos_noun, strip=strip)
787@@ -236,6 +241,8 @@ class DataSourceSmartOS(sources.DataSource):
788 for ci_noun, smartos_noun in SMARTOS_ATTRIB_JSON.items():
789 md[ci_noun] = self.md_client.get_json(smartos_noun)
790
791+ self.md_client.close_transport()
792+
793 # @datadictionary: This key may contain a program that is written
794 # to a file in the filesystem of the guest on each boot and then
795 # executed. It may be of any format that would be considered
796@@ -316,6 +323,10 @@ class JoyentMetadataFetchException(Exception):
797 pass
798
799
800+class JoyentMetadataTimeoutException(JoyentMetadataFetchException):
801+ pass
802+
803+
804 class JoyentMetadataClient(object):
805 """
806 A client implementing v2 of the Joyent Metadata Protocol Specification.
807@@ -360,6 +371,47 @@ class JoyentMetadataClient(object):
808 LOG.debug('Value "%s" found.', value)
809 return value
810
811+ def _readline(self):
812+ """
813+ Reads a line a byte at a time until \n is encountered. Returns an
814+ ascii string with the trailing newline removed.
815+
816+ If a timeout (per-byte) is set and it expires, a
817+ JoyentMetadataFetchException will be thrown.
818+ """
819+ response = []
820+
821+ def as_ascii():
822+ return b''.join(response).decode('ascii')
823+
824+ msg = "Partial response: '%s'"
825+ while True:
826+ try:
827+ byte = self.fp.read(1)
828+ if len(byte) == 0:
829+ raise JoyentMetadataTimeoutException(msg % as_ascii())
830+ if byte == b'\n':
831+ return as_ascii()
832+ response.append(byte)
833+ except OSError as exc:
834+ if exc.errno == errno.EAGAIN:
835+ raise JoyentMetadataTimeoutException(msg % as_ascii())
836+ raise
837+
838+ def _write(self, msg):
839+ self.fp.write(msg.encode('ascii'))
840+ self.fp.flush()
841+
842+ def _negotiate(self):
843+ LOG.debug('Negotiating protocol V2')
844+ self._write('NEGOTIATE V2\n')
845+ response = self._readline()
846+ LOG.debug('read "%s"', response)
847+ if response != 'V2_OK':
848+ raise JoyentMetadataFetchException(
849+ 'Invalid response "%s" to "NEGOTIATE V2"' % response)
850+ LOG.debug('Negotiation complete')
851+
852 def request(self, rtype, param=None):
853 request_id = '{0:08x}'.format(random.randint(0, 0xffffffff))
854 message_body = ' '.join((request_id, rtype,))
855@@ -374,18 +426,11 @@ class JoyentMetadataClient(object):
856 self.open_transport()
857 need_close = True
858
859- self.fp.write(msg.encode('ascii'))
860- self.fp.flush()
861-
862- response = bytearray()
863- response.extend(self.fp.read(1))
864- while response[-1:] != b'\n':
865- response.extend(self.fp.read(1))
866-
867+ self._write(msg)
868+ response = self._readline()
869 if need_close:
870 self.close_transport()
871
872- response = response.rstrip().decode('ascii')
873 LOG.debug('Read "%s" from metadata transport.', response)
874
875 if 'SUCCESS' not in response:
876@@ -450,6 +495,7 @@ class JoyentMetadataSocketClient(JoyentMetadataClient):
877 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
878 sock.connect(self.socketpath)
879 self.fp = sock.makefile('rwb')
880+ self._negotiate()
881
882 def exists(self):
883 return os.path.exists(self.socketpath)
884@@ -459,8 +505,9 @@ class JoyentMetadataSocketClient(JoyentMetadataClient):
885
886
887 class JoyentMetadataSerialClient(JoyentMetadataClient):
888- def __init__(self, device, timeout=10, smartos_type=SMARTOS_ENV_KVM):
889- super(JoyentMetadataSerialClient, self).__init__(smartos_type)
890+ def __init__(self, device, timeout=10, smartos_type=SMARTOS_ENV_KVM,
891+ fp=None):
892+ super(JoyentMetadataSerialClient, self).__init__(smartos_type, fp)
893 self.device = device
894 self.timeout = timeout
895
896@@ -468,10 +515,50 @@ class JoyentMetadataSerialClient(JoyentMetadataClient):
897 return os.path.exists(self.device)
898
899 def open_transport(self):
900- ser = serial.Serial(self.device, timeout=self.timeout)
901- if not ser.isOpen():
902- raise SystemError("Unable to open %s" % self.device)
903- self.fp = ser
904+ if self.fp is None:
905+ ser = serial.Serial(self.device, timeout=self.timeout)
906+ if not ser.isOpen():
907+ raise SystemError("Unable to open %s" % self.device)
908+ self.fp = ser
909+ self._flush()
910+ self._negotiate()
911+
912+ def _flush(self):
913+ LOG.debug('Flushing input')
914+ # Read any pending data
915+ timeout = self.fp.timeout
916+ self.fp.timeout = 0.1
917+ while True:
918+ try:
919+ self._readline()
920+ except JoyentMetadataTimeoutException:
921+ break
922+ LOG.debug('Input empty')
923+
924+ # Send a newline and expect "invalid command". Keep trying until
925+ # successful. Retry rather frequently so that the "Is the host
926+ # metadata service running" appears on the console soon after someone
927+ # attaches in an effort to debug.
928+ if timeout > 5:
929+ self.fp.timeout = 5
930+ else:
931+ self.fp.timeout = timeout
932+ while True:
933+ LOG.debug('Writing newline, expecting "invalid command"')
934+ self._write('\n')
935+ try:
936+ response = self._readline()
937+ if response == 'invalid command':
938+ break
939+ if response == 'FAILURE':
940+ LOG.debug('Got "FAILURE". Retrying.')
941+ continue
942+ LOG.warning('Unexpected response "%s" during flush', response)
943+ except JoyentMetadataTimeoutException:
944+ LOG.warning('Timeout while initializing metadata client. ' +
945+ 'Is the host metadata service running?')
946+ LOG.debug('Got "invalid command". Flush complete.')
947+ self.fp.timeout = timeout
948
949 def __repr__(self):
950 return "%s(device=%s, timeout=%s)" % (
951diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
952index 999b1d7..82fd347 100644
953--- a/cloudinit/tests/helpers.py
954+++ b/cloudinit/tests/helpers.py
955@@ -190,35 +190,11 @@ class ResourceUsingTestCase(CiTestCase):
956 super(ResourceUsingTestCase, self).setUp()
957 self.resource_path = None
958
959- def resourceLocation(self, subname=None):
960- if self.resource_path is None:
961- paths = [
962- os.path.join('tests', 'data'),
963- os.path.join('data'),
964- os.path.join(os.pardir, 'tests', 'data'),
965- os.path.join(os.pardir, 'data'),
966- ]
967- for p in paths:
968- if os.path.isdir(p):
969- self.resource_path = p
970- break
971- self.assertTrue((self.resource_path and
972- os.path.isdir(self.resource_path)),
973- msg="Unable to locate test resource data path!")
974- if not subname:
975- return self.resource_path
976- return os.path.join(self.resource_path, subname)
977-
978- def readResource(self, name):
979- where = self.resourceLocation(name)
980- with open(where, 'r') as fh:
981- return fh.read()
982-
983 def getCloudPaths(self, ds=None):
984 tmpdir = tempfile.mkdtemp()
985 self.addCleanup(shutil.rmtree, tmpdir)
986 cp = ch.Paths({'cloud_dir': tmpdir,
987- 'templates_dir': self.resourceLocation()},
988+ 'templates_dir': resourceLocation()},
989 ds=ds)
990 return cp
991
992@@ -234,7 +210,7 @@ class FilesystemMockingTestCase(ResourceUsingTestCase):
993 ResourceUsingTestCase.tearDown(self)
994
995 def replicateTestRoot(self, example_root, target_root):
996- real_root = self.resourceLocation()
997+ real_root = resourceLocation()
998 real_root = os.path.join(real_root, 'roots', example_root)
999 for (dir_path, _dirnames, filenames) in os.walk(real_root):
1000 real_path = dir_path
1001@@ -399,6 +375,18 @@ def wrap_and_call(prefix, mocks, func, *args, **kwargs):
1002 p.stop()
1003
1004
1005+def resourceLocation(subname=None):
1006+ path = os.path.join('tests', 'data')
1007+ if not subname:
1008+ return path
1009+ return os.path.join(path, subname)
1010+
1011+
1012+def readResource(name, mode='r'):
1013+ with open(resourceLocation(name), mode) as fh:
1014+ return fh.read()
1015+
1016+
1017 try:
1018 skipIf = unittest.skipIf
1019 except AttributeError:
1020diff --git a/cloudinit/tests/test_netinfo.py b/cloudinit/tests/test_netinfo.py
1021index 7dea2e4..2537c1c 100644
1022--- a/cloudinit/tests/test_netinfo.py
1023+++ b/cloudinit/tests/test_netinfo.py
1024@@ -2,105 +2,121 @@
1025
1026 """Tests netinfo module functions and classes."""
1027
1028+from copy import copy
1029+
1030 from cloudinit.netinfo import netdev_pformat, route_pformat
1031-from cloudinit.tests.helpers import CiTestCase, mock
1032+from cloudinit.tests.helpers import CiTestCase, mock, readResource
1033
1034
1035 # Example ifconfig and route output
1036-SAMPLE_IFCONFIG_OUT = """\
1037-enp0s25 Link encap:Ethernet HWaddr 50:7b:9d:2c:af:91
1038- inet addr:192.168.2.18 Bcast:192.168.2.255 Mask:255.255.255.0
1039- inet6 addr: fe80::8107:2b92:867e:f8a6/64 Scope:Link
1040- UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
1041- RX packets:8106427 errors:55 dropped:0 overruns:0 frame:37
1042- TX packets:9339739 errors:0 dropped:0 overruns:0 carrier:0
1043- collisions:0 txqueuelen:1000
1044- RX bytes:4953721719 (4.9 GB) TX bytes:7731890194 (7.7 GB)
1045- Interrupt:20 Memory:e1200000-e1220000
1046-
1047-lo Link encap:Local Loopback
1048- inet addr:127.0.0.1 Mask:255.0.0.0
1049- inet6 addr: ::1/128 Scope:Host
1050- UP LOOPBACK RUNNING MTU:65536 Metric:1
1051- RX packets:579230851 errors:0 dropped:0 overruns:0 frame:0
1052- TX packets:579230851 errors:0 dropped:0 overruns:0 carrier:0
1053- collisions:0 txqueuelen:1
1054-"""
1055-
1056-SAMPLE_ROUTE_OUT = '\n'.join([
1057- '0.0.0.0 192.168.2.1 0.0.0.0 UG 0 0 0'
1058- ' enp0s25',
1059- '0.0.0.0 192.168.2.1 0.0.0.0 UG 0 0 0'
1060- ' wlp3s0',
1061- '192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0'
1062- ' enp0s25'])
1063-
1064-
1065-NETDEV_FORMATTED_OUT = '\n'.join([
1066- '+++++++++++++++++++++++++++++++++++++++Net device info+++++++++++++++++++'
1067- '++++++++++++++++++++',
1068- '+---------+------+------------------------------+---------------+-------+'
1069- '-------------------+',
1070- '| Device | Up | Address | Mask | Scope |'
1071- ' Hw-Address |',
1072- '+---------+------+------------------------------+---------------+-------+'
1073- '-------------------+',
1074- '| enp0s25 | True | 192.168.2.18 | 255.255.255.0 | . |'
1075- ' 50:7b:9d:2c:af:91 |',
1076- '| enp0s25 | True | fe80::8107:2b92:867e:f8a6/64 | . | link |'
1077- ' 50:7b:9d:2c:af:91 |',
1078- '| lo | True | 127.0.0.1 | 255.0.0.0 | . |'
1079- ' . |',
1080- '| lo | True | ::1/128 | . | host |'
1081- ' . |',
1082- '+---------+------+------------------------------+---------------+-------+'
1083- '-------------------+'])
1084-
1085-ROUTE_FORMATTED_OUT = '\n'.join([
1086- '+++++++++++++++++++++++++++++Route IPv4 info++++++++++++++++++++++++++'
1087- '+++',
1088- '+-------+-------------+-------------+---------------+-----------+-----'
1089- '--+',
1090- '| Route | Destination | Gateway | Genmask | Interface | Flags'
1091- ' |',
1092- '+-------+-------------+-------------+---------------+-----------+'
1093- '-------+',
1094- '| 0 | 0.0.0.0 | 192.168.2.1 | 0.0.0.0 | wlp3s0 |'
1095- ' UG |',
1096- '| 1 | 192.168.2.0 | 0.0.0.0 | 255.255.255.0 | enp0s25 |'
1097- ' U |',
1098- '+-------+-------------+-------------+---------------+-----------+'
1099- '-------+',
1100- '++++++++++++++++++++++++++++++++++++++++Route IPv6 info++++++++++'
1101- '++++++++++++++++++++++++++++++',
1102- '+-------+-------------+-------------+---------------+---------------+'
1103- '-----------------+-------+',
1104- '| Route | Proto | Recv-Q | Send-Q | Local Address |'
1105- ' Foreign Address | State |',
1106- '+-------+-------------+-------------+---------------+---------------+'
1107- '-----------------+-------+',
1108- '| 0 | 0.0.0.0 | 192.168.2.1 | 0.0.0.0 | UG |'
1109- ' 0 | 0 |',
1110- '| 1 | 192.168.2.0 | 0.0.0.0 | 255.255.255.0 | U |'
1111- ' 0 | 0 |',
1112- '+-------+-------------+-------------+---------------+---------------+'
1113- '-----------------+-------+'])
1114+SAMPLE_OLD_IFCONFIG_OUT = readResource("netinfo/old-ifconfig-output")
1115+SAMPLE_NEW_IFCONFIG_OUT = readResource("netinfo/new-ifconfig-output")
1116+SAMPLE_IPADDRSHOW_OUT = readResource("netinfo/sample-ipaddrshow-output")
1117+SAMPLE_ROUTE_OUT_V4 = readResource("netinfo/sample-route-output-v4")
1118+SAMPLE_ROUTE_OUT_V6 = readResource("netinfo/sample-route-output-v6")
1119+SAMPLE_IPROUTE_OUT_V4 = readResource("netinfo/sample-iproute-output-v4")
1120+SAMPLE_IPROUTE_OUT_V6 = readResource("netinfo/sample-iproute-output-v6")
1121+NETDEV_FORMATTED_OUT = readResource("netinfo/netdev-formatted-output")
1122+ROUTE_FORMATTED_OUT = readResource("netinfo/route-formatted-output")
1123
1124
1125 class TestNetInfo(CiTestCase):
1126
1127 maxDiff = None
1128+ with_logs = True
1129+
1130+ @mock.patch('cloudinit.netinfo.util.which')
1131+ @mock.patch('cloudinit.netinfo.util.subp')
1132+ def test_netdev_old_nettools_pformat(self, m_subp, m_which):
1133+ """netdev_pformat properly rendering old nettools info."""
1134+ m_subp.return_value = (SAMPLE_OLD_IFCONFIG_OUT, '')
1135+ m_which.side_effect = lambda x: x if x == 'ifconfig' else None
1136+ content = netdev_pformat()
1137+ self.assertEqual(NETDEV_FORMATTED_OUT, content)
1138
1139+ @mock.patch('cloudinit.netinfo.util.which')
1140 @mock.patch('cloudinit.netinfo.util.subp')
1141- def test_netdev_pformat(self, m_subp):
1142- """netdev_pformat properly rendering network device information."""
1143- m_subp.return_value = (SAMPLE_IFCONFIG_OUT, '')
1144+ def test_netdev_new_nettools_pformat(self, m_subp, m_which):
1145+ """netdev_pformat properly rendering netdev new nettools info."""
1146+ m_subp.return_value = (SAMPLE_NEW_IFCONFIG_OUT, '')
1147+ m_which.side_effect = lambda x: x if x == 'ifconfig' else None
1148 content = netdev_pformat()
1149 self.assertEqual(NETDEV_FORMATTED_OUT, content)
1150
1151+ @mock.patch('cloudinit.netinfo.util.which')
1152+ @mock.patch('cloudinit.netinfo.util.subp')
1153+ def test_netdev_iproute_pformat(self, m_subp, m_which):
1154+ """netdev_pformat properly rendering ip route info."""
1155+ m_subp.return_value = (SAMPLE_IPADDRSHOW_OUT, '')
1156+ m_which.side_effect = lambda x: x if x == 'ip' else None
1157+ content = netdev_pformat()
1158+ new_output = copy(NETDEV_FORMATTED_OUT)
1159+ # ip route show describes global scopes on ipv4 addresses
1160+ # whereas ifconfig does not. Add proper global/host scope to output.
1161+ new_output = new_output.replace('| . | 50:7b', '| global | 50:7b')
1162+ new_output = new_output.replace(
1163+ '255.0.0.0 | . |', '255.0.0.0 | host |')
1164+ self.assertEqual(new_output, content)
1165+
1166+ @mock.patch('cloudinit.netinfo.util.which')
1167+ @mock.patch('cloudinit.netinfo.util.subp')
1168+ def test_netdev_warn_on_missing_commands(self, m_subp, m_which):
1169+ """netdev_pformat warns when missing both ip and 'netstat'."""
1170+ m_which.return_value = None # Niether ip nor netstat found
1171+ content = netdev_pformat()
1172+ self.assertEqual('\n', content)
1173+ self.assertEqual(
1174+ "WARNING: Could not print networks: missing 'ip' and 'ifconfig'"
1175+ " commands\n",
1176+ self.logs.getvalue())
1177+ m_subp.assert_not_called()
1178+
1179+ @mock.patch('cloudinit.netinfo.util.which')
1180 @mock.patch('cloudinit.netinfo.util.subp')
1181- def test_route_pformat(self, m_subp):
1182- """netdev_pformat properly rendering network device information."""
1183- m_subp.return_value = (SAMPLE_ROUTE_OUT, '')
1184+ def test_route_nettools_pformat(self, m_subp, m_which):
1185+ """route_pformat properly rendering nettools route info."""
1186+
1187+ def subp_netstat_route_selector(*args, **kwargs):
1188+ if args[0] == ['netstat', '--route', '--numeric', '--extend']:
1189+ return (SAMPLE_ROUTE_OUT_V4, '')
1190+ if args[0] == ['netstat', '-A', 'inet6', '--route', '--numeric']:
1191+ return (SAMPLE_ROUTE_OUT_V6, '')
1192+ raise Exception('Unexpected subp call %s' % args[0])
1193+
1194+ m_subp.side_effect = subp_netstat_route_selector
1195+ m_which.side_effect = lambda x: x if x == 'netstat' else None
1196 content = route_pformat()
1197 self.assertEqual(ROUTE_FORMATTED_OUT, content)
1198+
1199+ @mock.patch('cloudinit.netinfo.util.which')
1200+ @mock.patch('cloudinit.netinfo.util.subp')
1201+ def test_route_iproute_pformat(self, m_subp, m_which):
1202+ """route_pformat properly rendering ip route info."""
1203+
1204+ def subp_iproute_selector(*args, **kwargs):
1205+ if ['ip', '-o', 'route', 'list'] == args[0]:
1206+ return (SAMPLE_IPROUTE_OUT_V4, '')
1207+ v6cmd = ['ip', '--oneline', '-6', 'route', 'list', 'table', 'all']
1208+ if v6cmd == args[0]:
1209+ return (SAMPLE_IPROUTE_OUT_V6, '')
1210+ raise Exception('Unexpected subp call %s' % args[0])
1211+
1212+ m_subp.side_effect = subp_iproute_selector
1213+ m_which.side_effect = lambda x: x if x == 'ip' else None
1214+ content = route_pformat()
1215+ self.assertEqual(ROUTE_FORMATTED_OUT, content)
1216+
1217+ @mock.patch('cloudinit.netinfo.util.which')
1218+ @mock.patch('cloudinit.netinfo.util.subp')
1219+ def test_route_warn_on_missing_commands(self, m_subp, m_which):
1220+ """route_pformat warns when missing both ip and 'netstat'."""
1221+ m_which.return_value = None # Niether ip nor netstat found
1222+ content = route_pformat()
1223+ self.assertEqual('\n', content)
1224+ self.assertEqual(
1225+ "WARNING: Could not print routes: missing 'ip' and 'netstat'"
1226+ " commands\n",
1227+ self.logs.getvalue())
1228+ m_subp.assert_not_called()
1229+
1230+# vi: ts=4 expandtab
1231diff --git a/cloudinit/util.py b/cloudinit/util.py
1232index acdc0d8..1717b52 100644
1233--- a/cloudinit/util.py
1234+++ b/cloudinit/util.py
1235@@ -1446,7 +1446,7 @@ def get_config_logfiles(cfg):
1236 for fmt in get_output_cfg(cfg, None):
1237 if not fmt:
1238 continue
1239- match = re.match('(?P<type>\||>+)\s*(?P<target>.*)', fmt)
1240+ match = re.match(r'(?P<type>\||>+)\s*(?P<target>.*)', fmt)
1241 if not match:
1242 continue
1243 target = match.group('target')
1244@@ -2275,8 +2275,8 @@ def parse_mount(path):
1245 # the regex is a bit complex. to better understand this regex see:
1246 # https://regex101.com/r/2F6c1k/1
1247 # https://regex101.com/r/T2en7a/1
1248- regex = r'^(/dev/[\S]+|.*zroot\S*?) on (/[\S]*) ' + \
1249- '(?=(?:type)[\s]+([\S]+)|\(([^,]*))'
1250+ regex = (r'^(/dev/[\S]+|.*zroot\S*?) on (/[\S]*) '
1251+ r'(?=(?:type)[\s]+([\S]+)|\(([^,]*))')
1252 for line in mount_locs:
1253 m = re.search(regex, line)
1254 if not m:
1255diff --git a/debian/changelog b/debian/changelog
1256index d3a4234..45016a5 100644
1257--- a/debian/changelog
1258+++ b/debian/changelog
1259@@ -1,3 +1,16 @@
1260+cloud-init (18.2-14-g6d48d265-0ubuntu1) bionic; urgency=medium
1261+
1262+ * New upstream snapshot.
1263+ - net: Depend on iproute2's ip instead of net-tools ifconfig or route
1264+ - DataSourceSmartOS: fix hang when metadata service is down
1265+ [Mike Gerdts] (LP: #1667735)
1266+ - DataSourceSmartOS: change default fs on ephemeral disk from ext3 to
1267+ ext4. [Mike Gerdts] (LP: #1763511)
1268+ - pycodestyle: Fix invalid escape sequences in string literals.
1269+ - Implement bash completion script for cloud-init command line
1270+
1271+ -- Chad Smith <chad.smith@canonical.com> Wed, 18 Apr 2018 15:25:53 -0600
1272+
1273 cloud-init (18.2-9-g49b562c9-0ubuntu1) bionic; urgency=medium
1274
1275 * New upstream snapshot.
1276diff --git a/doc/examples/cloud-config-disk-setup.txt b/doc/examples/cloud-config-disk-setup.txt
1277index dd91477..43a62a2 100644
1278--- a/doc/examples/cloud-config-disk-setup.txt
1279+++ b/doc/examples/cloud-config-disk-setup.txt
1280@@ -37,7 +37,7 @@ fs_setup:
1281 # Default disk definitions for SmartOS
1282 # ------------------------------------
1283
1284-device_aliases: {'ephemeral0': '/dev/sdb'}
1285+device_aliases: {'ephemeral0': '/dev/vdb'}
1286 disk_setup:
1287 ephemeral0:
1288 table_type: mbr
1289@@ -46,7 +46,7 @@ disk_setup:
1290
1291 fs_setup:
1292 - label: ephemeral0
1293- filesystem: ext3
1294+ filesystem: ext4
1295 device: ephemeral0.0
1296
1297 # Cavaut for SmartOS: if ephemeral disk is not defined, then the disk will
1298diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in
1299index 6ab0d20..91faf3c 100644
1300--- a/packages/redhat/cloud-init.spec.in
1301+++ b/packages/redhat/cloud-init.spec.in
1302@@ -197,6 +197,7 @@ fi
1303 %dir %{_sysconfdir}/cloud/templates
1304 %config(noreplace) %{_sysconfdir}/cloud/templates/*
1305 %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf
1306+%{_sysconfdir}/bash_completion.d/cloud-init
1307
1308 %{_libexecdir}/%{name}
1309 %dir %{_sharedstatedir}/cloud
1310diff --git a/packages/suse/cloud-init.spec.in b/packages/suse/cloud-init.spec.in
1311index 86e18b1..bbb965a 100644
1312--- a/packages/suse/cloud-init.spec.in
1313+++ b/packages/suse/cloud-init.spec.in
1314@@ -136,6 +136,7 @@ mkdir -p %{buildroot}/var/lib/cloud
1315 %config(noreplace) %{_sysconfdir}/cloud/cloud.cfg.d/README
1316 %dir %{_sysconfdir}/cloud/templates
1317 %config(noreplace) %{_sysconfdir}/cloud/templates/*
1318+%{_sysconfdir}/bash_completion.d/cloud-init
1319
1320 # Python code is here...
1321 %{python_sitelib}/*
1322diff --git a/setup.py b/setup.py
1323index bc3f52a..85b2337 100755
1324--- a/setup.py
1325+++ b/setup.py
1326@@ -228,6 +228,7 @@ if not in_virtualenv():
1327 INITSYS_ROOTS[k] = "/" + INITSYS_ROOTS[k]
1328
1329 data_files = [
1330+ (ETC + '/bash_completion.d', ['bash_completion/cloud-init']),
1331 (ETC + '/cloud', [render_tmpl("config/cloud.cfg.tmpl")]),
1332 (ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')),
1333 (ETC + '/cloud/templates', glob('templates/*')),
1334diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py
1335index 7598d46..4fda8f9 100644
1336--- a/tests/cloud_tests/testcases/base.py
1337+++ b/tests/cloud_tests/testcases/base.py
1338@@ -235,7 +235,7 @@ class CloudTestCase(unittest.TestCase):
1339 'found unexpected kvm availability-zone %s' %
1340 v1_data['availability-zone'])
1341 self.assertIsNotNone(
1342- re.match('[\da-f]{8}(-[\da-f]{4}){3}-[\da-f]{12}',
1343+ re.match(r'[\da-f]{8}(-[\da-f]{4}){3}-[\da-f]{12}',
1344 v1_data['instance-id']),
1345 'kvm instance-id is not a UUID: %s' % v1_data['instance-id'])
1346 self.assertIn('ubuntu', v1_data['local-hostname'])
1347diff --git a/tests/data/netinfo/netdev-formatted-output b/tests/data/netinfo/netdev-formatted-output
1348new file mode 100644
1349index 0000000..283ab4a
1350--- /dev/null
1351+++ b/tests/data/netinfo/netdev-formatted-output
1352@@ -0,0 +1,10 @@
1353++++++++++++++++++++++++++++++++++++++++Net device info++++++++++++++++++++++++++++++++++++++++
1354++---------+------+------------------------------+---------------+--------+-------------------+
1355+| Device | Up | Address | Mask | Scope | Hw-Address |
1356++---------+------+------------------------------+---------------+--------+-------------------+
1357+| enp0s25 | True | 192.168.2.18 | 255.255.255.0 | . | 50:7b:9d:2c:af:91 |
1358+| enp0s25 | True | fe80::7777:2222:1111:eeee/64 | . | global | 50:7b:9d:2c:af:91 |
1359+| enp0s25 | True | fe80::8107:2b92:867e:f8a6/64 | . | link | 50:7b:9d:2c:af:91 |
1360+| lo | True | 127.0.0.1 | 255.0.0.0 | . | . |
1361+| lo | True | ::1/128 | . | host | . |
1362++---------+------+------------------------------+---------------+--------+-------------------+
1363diff --git a/tests/data/netinfo/new-ifconfig-output b/tests/data/netinfo/new-ifconfig-output
1364new file mode 100644
1365index 0000000..83d4ad1
1366--- /dev/null
1367+++ b/tests/data/netinfo/new-ifconfig-output
1368@@ -0,0 +1,18 @@
1369+enp0s25: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
1370+ inet 192.168.2.18 netmask 255.255.255.0 broadcast 192.168.2.255
1371+ inet6 fe80::7777:2222:1111:eeee prefixlen 64 scopeid 0x30<global>
1372+ inet6 fe80::8107:2b92:867e:f8a6 prefixlen 64 scopeid 0x20<link>
1373+ ether 50:7b:9d:2c:af:91 txqueuelen 1000 (Ethernet)
1374+ RX packets 3017 bytes 10601563 (10.1 MiB)
1375+ RX errors 0 dropped 39 overruns 0 frame 0
1376+ TX packets 2627 bytes 196976 (192.3 KiB)
1377+ TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
1378+
1379+lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
1380+ inet 127.0.0.1 netmask 255.0.0.0
1381+ inet6 ::1 prefixlen 128 scopeid 0x10<host>
1382+ loop txqueuelen 1 (Local Loopback)
1383+ RX packets 0 bytes 0 (0.0 B)
1384+ RX errors 0 dropped 0 overruns 0 frame 0
1385+ TX packets 0 bytes 0 (0.0 B)
1386+ TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
1387diff --git a/tests/data/netinfo/old-ifconfig-output b/tests/data/netinfo/old-ifconfig-output
1388new file mode 100644
1389index 0000000..e01f763
1390--- /dev/null
1391+++ b/tests/data/netinfo/old-ifconfig-output
1392@@ -0,0 +1,18 @@
1393+enp0s25 Link encap:Ethernet HWaddr 50:7b:9d:2c:af:91
1394+ inet addr:192.168.2.18 Bcast:192.168.2.255 Mask:255.255.255.0
1395+ inet6 addr: fe80::7777:2222:1111:eeee/64 Scope:Global
1396+ inet6 addr: fe80::8107:2b92:867e:f8a6/64 Scope:Link
1397+ UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
1398+ RX packets:8106427 errors:55 dropped:0 overruns:0 frame:37
1399+ TX packets:9339739 errors:0 dropped:0 overruns:0 carrier:0
1400+ collisions:0 txqueuelen:1000
1401+ RX bytes:4953721719 (4.9 GB) TX bytes:7731890194 (7.7 GB)
1402+ Interrupt:20 Memory:e1200000-e1220000
1403+
1404+lo Link encap:Local Loopback
1405+ inet addr:127.0.0.1 Mask:255.0.0.0
1406+ inet6 addr: ::1/128 Scope:Host
1407+ UP LOOPBACK RUNNING MTU:65536 Metric:1
1408+ RX packets:579230851 errors:0 dropped:0 overruns:0 frame:0
1409+ TX packets:579230851 errors:0 dropped:0 overruns:0 carrier:0
1410+ collisions:0 txqueuelen:1
1411diff --git a/tests/data/netinfo/route-formatted-output b/tests/data/netinfo/route-formatted-output
1412new file mode 100644
1413index 0000000..9d2c5dd
1414--- /dev/null
1415+++ b/tests/data/netinfo/route-formatted-output
1416@@ -0,0 +1,22 @@
1417++++++++++++++++++++++++++++++Route IPv4 info+++++++++++++++++++++++++++++
1418++-------+-------------+-------------+---------------+-----------+-------+
1419+| Route | Destination | Gateway | Genmask | Interface | Flags |
1420++-------+-------------+-------------+---------------+-----------+-------+
1421+| 0 | 0.0.0.0 | 192.168.2.1 | 0.0.0.0 | enp0s25 | UG |
1422+| 1 | 0.0.0.0 | 192.168.2.1 | 0.0.0.0 | wlp3s0 | UG |
1423+| 2 | 192.168.2.0 | 0.0.0.0 | 255.255.255.0 | enp0s25 | U |
1424++-------+-------------+-------------+---------------+-----------+-------+
1425++++++++++++++++++++++++++++++++++++Route IPv6 info+++++++++++++++++++++++++++++++++++
1426++-------+---------------------------+---------------------------+-----------+-------+
1427+| Route | Destination | Gateway | Interface | Flags |
1428++-------+---------------------------+---------------------------+-----------+-------+
1429+| 0 | 2a00:abcd:82ae:cd33::657 | :: | enp0s25 | Ue |
1430+| 1 | 2a00:abcd:82ae:cd33::/64 | :: | enp0s25 | U |
1431+| 2 | 2a00:abcd:82ae:cd33::/56 | fe80::32ee:54de:cd43:b4e1 | enp0s25 | UG |
1432+| 3 | fd81:123f:654::657 | :: | enp0s25 | U |
1433+| 4 | fd81:123f:654::/64 | :: | enp0s25 | U |
1434+| 5 | fd81:123f:654::/48 | fe80::32ee:54de:cd43:b4e1 | enp0s25 | UG |
1435+| 6 | fe80::abcd:ef12:bc34:da21 | :: | enp0s25 | U |
1436+| 7 | fe80::/64 | :: | enp0s25 | U |
1437+| 8 | ::/0 | fe80::32ee:54de:cd43:b4e1 | enp0s25 | UG |
1438++-------+---------------------------+---------------------------+-----------+-------+
1439diff --git a/tests/data/netinfo/sample-ipaddrshow-output b/tests/data/netinfo/sample-ipaddrshow-output
1440new file mode 100644
1441index 0000000..b2fa267
1442--- /dev/null
1443+++ b/tests/data/netinfo/sample-ipaddrshow-output
1444@@ -0,0 +1,13 @@
1445+1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
1446+ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
1447+ inet 127.0.0.1/8 scope host lo\ valid_lft forever preferred_lft forever
1448+ inet6 ::1/128 scope host \ valid_lft forever preferred_lft forever
1449+2: enp0s25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
1450+ link/ether 50:7b:9d:2c:af:91 brd ff:ff:ff:ff:ff:ff
1451+ inet 192.168.2.18/24 brd 192.168.2.255 scope global dynamic enp0s25
1452+ valid_lft 84174sec preferred_lft 84174sec
1453+ inet6 fe80::7777:2222:1111:eeee/64 scope global
1454+ valid_lft forever preferred_lft forever
1455+ inet6 fe80::8107:2b92:867e:f8a6/64 scope link
1456+ valid_lft forever preferred_lft forever
1457+
1458diff --git a/tests/data/netinfo/sample-iproute-output-v4 b/tests/data/netinfo/sample-iproute-output-v4
1459new file mode 100644
1460index 0000000..904cb03
1461--- /dev/null
1462+++ b/tests/data/netinfo/sample-iproute-output-v4
1463@@ -0,0 +1,3 @@
1464+default via 192.168.2.1 dev enp0s25 proto static metric 100
1465+default via 192.168.2.1 dev wlp3s0 proto static metric 150
1466+192.168.2.0/24 dev enp0s25 proto kernel scope link src 192.168.2.18 metric 100
1467diff --git a/tests/data/netinfo/sample-iproute-output-v6 b/tests/data/netinfo/sample-iproute-output-v6
1468new file mode 100644
1469index 0000000..12bb1c1
1470--- /dev/null
1471+++ b/tests/data/netinfo/sample-iproute-output-v6
1472@@ -0,0 +1,11 @@
1473+2a00:abcd:82ae:cd33::657 dev enp0s25 proto kernel metric 256 expires 2334sec pref medium
1474+2a00:abcd:82ae:cd33::/64 dev enp0s25 proto ra metric 100 pref medium
1475+2a00:abcd:82ae:cd33::/56 via fe80::32ee:54de:cd43:b4e1 dev enp0s25 proto ra metric 100 pref medium
1476+fd81:123f:654::657 dev enp0s25 proto kernel metric 256 pref medium
1477+fd81:123f:654::/64 dev enp0s25 proto ra metric 100 pref medium
1478+fd81:123f:654::/48 via fe80::32ee:54de:cd43:b4e1 dev enp0s25 proto ra metric 100 pref medium
1479+fe80::abcd:ef12:bc34:da21 dev enp0s25 proto static metric 100 pref medium
1480+fe80::/64 dev enp0s25 proto kernel metric 256 pref medium
1481+default via fe80::32ee:54de:cd43:b4e1 dev enp0s25 proto static metric 100 pref medium
1482+local ::1 dev lo table local proto none metric 0 pref medium
1483+local 2600:1f16:b80:ad00:90a:c915:bca6:5ff2 dev lo table local proto none metric 0 pref medium
1484diff --git a/tests/data/netinfo/sample-route-output-v4 b/tests/data/netinfo/sample-route-output-v4
1485new file mode 100644
1486index 0000000..ecc31d9
1487--- /dev/null
1488+++ b/tests/data/netinfo/sample-route-output-v4
1489@@ -0,0 +1,5 @@
1490+Kernel IP routing table
1491+Destination Gateway Genmask Flags Metric Ref Use Iface
1492+0.0.0.0 192.168.2.1 0.0.0.0 UG 100 0 0 enp0s25
1493+0.0.0.0 192.168.2.1 0.0.0.0 UG 150 0 0 wlp3s0
1494+192.168.2.0 0.0.0.0 255.255.255.0 U 100 0 0 enp0s25
1495diff --git a/tests/data/netinfo/sample-route-output-v6 b/tests/data/netinfo/sample-route-output-v6
1496new file mode 100644
1497index 0000000..4712b73
1498--- /dev/null
1499+++ b/tests/data/netinfo/sample-route-output-v6
1500@@ -0,0 +1,13 @@
1501+Kernel IPv6 routing table
1502+Destination Next Hop Flag Met Re Use If
1503+2a00:abcd:82ae:cd33::657/128 :: Ue 256 1 0 enp0s25
1504+2a00:abcd:82ae:cd33::/64 :: U 100 1 0 enp0s25
1505+2a00:abcd:82ae:cd33::/56 fe80::32ee:54de:cd43:b4e1 UG 100 1 0 enp0s25
1506+fd81:123f:654::657/128 :: U 256 1 0 enp0s25
1507+fd81:123f:654::/64 :: U 100 1 0 enp0s25
1508+fd81:123f:654::/48 fe80::32ee:54de:cd43:b4e1 UG 100 1 0 enp0s25
1509+fe80::abcd:ef12:bc34:da21/128 :: U 100 1 2 enp0s25
1510+fe80::/64 :: U 256 1 16880 enp0s25
1511+::/0 fe80::32ee:54de:cd43:b4e1 UG 100 1 0 enp0s25
1512+::/0 :: !n -1 1424956 lo
1513+::1/128 :: Un 0 4 26289 lo
1514diff --git a/tests/unittests/test_datasource/test_smartos.py b/tests/unittests/test_datasource/test_smartos.py
1515index 88bae5f..2bea7a1 100644
1516--- a/tests/unittests/test_datasource/test_smartos.py
1517+++ b/tests/unittests/test_datasource/test_smartos.py
1518@@ -1,4 +1,5 @@
1519 # Copyright (C) 2013 Canonical Ltd.
1520+# Copyright (c) 2018, Joyent, Inc.
1521 #
1522 # Author: Ben Howard <ben.howard@canonical.com>
1523 #
1524@@ -324,6 +325,7 @@ class PsuedoJoyentClient(object):
1525 if data is None:
1526 data = MOCK_RETURNS.copy()
1527 self.data = data
1528+ self._is_open = False
1529 return
1530
1531 def get(self, key, default=None, strip=False):
1532@@ -344,6 +346,14 @@ class PsuedoJoyentClient(object):
1533 def exists(self):
1534 return True
1535
1536+ def open_transport(self):
1537+ assert(not self._is_open)
1538+ self._is_open = True
1539+
1540+ def close_transport(self):
1541+ assert(self._is_open)
1542+ self._is_open = False
1543+
1544
1545 class TestSmartOSDataSource(FilesystemMockingTestCase):
1546 def setUp(self):
1547@@ -592,8 +602,46 @@ class TestSmartOSDataSource(FilesystemMockingTestCase):
1548 mydscfg['disk_aliases']['FOO'])
1549
1550
1551+class ShortReader(object):
1552+ """Implements a 'read' interface for bytes provided.
1553+ much like io.BytesIO but the 'endbyte' acts as if EOF.
1554+ When it is reached a short will be returned."""
1555+ def __init__(self, initial_bytes, endbyte=b'\0'):
1556+ self.data = initial_bytes
1557+ self.index = 0
1558+ self.len = len(self.data)
1559+ self.endbyte = endbyte
1560+
1561+ @property
1562+ def emptied(self):
1563+ return self.index >= self.len
1564+
1565+ def read(self, size=-1):
1566+ """Read size bytes but not past a null."""
1567+ if size == 0 or self.index >= self.len:
1568+ return b''
1569+
1570+ rsize = size
1571+ if size < 0 or size + self.index > self.len:
1572+ rsize = self.len - self.index
1573+
1574+ next_null = self.data.find(self.endbyte, self.index, rsize)
1575+ if next_null >= 0:
1576+ rsize = next_null - self.index + 1
1577+ i = self.index
1578+ self.index += rsize
1579+ ret = self.data[i:i + rsize]
1580+ if len(ret) and ret[-1:] == self.endbyte:
1581+ ret = ret[:-1]
1582+ return ret
1583+
1584+
1585 class TestJoyentMetadataClient(FilesystemMockingTestCase):
1586
1587+ invalid = b'invalid command\n'
1588+ failure = b'FAILURE\n'
1589+ v2_ok = b'V2_OK\n'
1590+
1591 def setUp(self):
1592 super(TestJoyentMetadataClient, self).setUp()
1593
1594@@ -636,6 +684,11 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase):
1595 return DataSourceSmartOS.JoyentMetadataClient(
1596 fp=self.serial, smartos_type=DataSourceSmartOS.SMARTOS_ENV_KVM)
1597
1598+ def _get_serial_client(self):
1599+ self.serial.timeout = 1
1600+ return DataSourceSmartOS.JoyentMetadataSerialClient(None,
1601+ fp=self.serial)
1602+
1603 def assertEndsWith(self, haystack, prefix):
1604 self.assertTrue(haystack.endswith(prefix),
1605 "{0} does not end with '{1}'".format(
1606@@ -646,12 +699,14 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase):
1607 "{0} does not start with '{1}'".format(
1608 repr(haystack), prefix))
1609
1610+ def assertNoMoreSideEffects(self, obj):
1611+ self.assertRaises(StopIteration, obj)
1612+
1613 def test_get_metadata_writes_a_single_line(self):
1614 client = self._get_client()
1615 client.get('some_key')
1616 self.assertEqual(1, self.serial.write.call_count)
1617 written_line = self.serial.write.call_args[0][0]
1618- print(type(written_line))
1619 self.assertEndsWith(written_line.decode('ascii'),
1620 b'\n'.decode('ascii'))
1621 self.assertEqual(1, written_line.count(b'\n'))
1622@@ -737,6 +792,52 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase):
1623 client._checksum = lambda _: self.response_parts['crc']
1624 self.assertIsNone(client.get('some_key'))
1625
1626+ def test_negotiate(self):
1627+ client = self._get_client()
1628+ reader = ShortReader(self.v2_ok)
1629+ client.fp.read.side_effect = reader.read
1630+ client._negotiate()
1631+ self.assertTrue(reader.emptied)
1632+
1633+ def test_negotiate_short_response(self):
1634+ client = self._get_client()
1635+ # chopped '\n' from v2_ok.
1636+ reader = ShortReader(self.v2_ok[:-1] + b'\0')
1637+ client.fp.read.side_effect = reader.read
1638+ self.assertRaises(DataSourceSmartOS.JoyentMetadataTimeoutException,
1639+ client._negotiate)
1640+ self.assertTrue(reader.emptied)
1641+
1642+ def test_negotiate_bad_response(self):
1643+ client = self._get_client()
1644+ reader = ShortReader(b'garbage\n' + self.v2_ok)
1645+ client.fp.read.side_effect = reader.read
1646+ self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,
1647+ client._negotiate)
1648+ self.assertEqual(self.v2_ok, client.fp.read())
1649+
1650+ def test_serial_open_transport(self):
1651+ client = self._get_serial_client()
1652+ reader = ShortReader(b'garbage\0' + self.invalid + self.v2_ok)
1653+ client.fp.read.side_effect = reader.read
1654+ client.open_transport()
1655+ self.assertTrue(reader.emptied)
1656+
1657+ def test_flush_failure(self):
1658+ client = self._get_serial_client()
1659+ reader = ShortReader(b'garbage' + b'\0' + self.failure +
1660+ self.invalid + self.v2_ok)
1661+ client.fp.read.side_effect = reader.read
1662+ client.open_transport()
1663+ self.assertTrue(reader.emptied)
1664+
1665+ def test_flush_many_timeouts(self):
1666+ client = self._get_serial_client()
1667+ reader = ShortReader(b'\0' * 100 + self.invalid + self.v2_ok)
1668+ client.fp.read.side_effect = reader.read
1669+ client.open_transport()
1670+ self.assertTrue(reader.emptied)
1671+
1672
1673 class TestNetworkConversion(TestCase):
1674 def test_convert_simple(self):
1675diff --git a/tests/unittests/test_filters/test_launch_index.py b/tests/unittests/test_filters/test_launch_index.py
1676index 6364d38..e1a5d2c 100644
1677--- a/tests/unittests/test_filters/test_launch_index.py
1678+++ b/tests/unittests/test_filters/test_launch_index.py
1679@@ -55,7 +55,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
1680 return True
1681
1682 def testMultiEmailIndex(self):
1683- test_data = self.readResource('filter_cloud_multipart_2.email')
1684+ test_data = helpers.readResource('filter_cloud_multipart_2.email')
1685 ud_proc = ud.UserDataProcessor(self.getCloudPaths())
1686 message = ud_proc.process(test_data)
1687 self.assertTrue(count_messages(message) > 0)
1688@@ -70,7 +70,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
1689 self.assertCounts(message, expected_counts)
1690
1691 def testHeaderEmailIndex(self):
1692- test_data = self.readResource('filter_cloud_multipart_header.email')
1693+ test_data = helpers.readResource('filter_cloud_multipart_header.email')
1694 ud_proc = ud.UserDataProcessor(self.getCloudPaths())
1695 message = ud_proc.process(test_data)
1696 self.assertTrue(count_messages(message) > 0)
1697@@ -85,7 +85,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
1698 self.assertCounts(message, expected_counts)
1699
1700 def testConfigEmailIndex(self):
1701- test_data = self.readResource('filter_cloud_multipart_1.email')
1702+ test_data = helpers.readResource('filter_cloud_multipart_1.email')
1703 ud_proc = ud.UserDataProcessor(self.getCloudPaths())
1704 message = ud_proc.process(test_data)
1705 self.assertTrue(count_messages(message) > 0)
1706@@ -99,7 +99,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
1707 self.assertCounts(message, expected_counts)
1708
1709 def testNoneIndex(self):
1710- test_data = self.readResource('filter_cloud_multipart.yaml')
1711+ test_data = helpers.readResource('filter_cloud_multipart.yaml')
1712 ud_proc = ud.UserDataProcessor(self.getCloudPaths())
1713 message = ud_proc.process(test_data)
1714 start_count = count_messages(message)
1715@@ -108,7 +108,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
1716 self.assertTrue(self.equivalentMessage(message, filtered_message))
1717
1718 def testIndexes(self):
1719- test_data = self.readResource('filter_cloud_multipart.yaml')
1720+ test_data = helpers.readResource('filter_cloud_multipart.yaml')
1721 ud_proc = ud.UserDataProcessor(self.getCloudPaths())
1722 message = ud_proc.process(test_data)
1723 start_count = count_messages(message)
1724diff --git a/tests/unittests/test_merging.py b/tests/unittests/test_merging.py
1725index f51358d..3a5072c 100644
1726--- a/tests/unittests/test_merging.py
1727+++ b/tests/unittests/test_merging.py
1728@@ -100,7 +100,7 @@ def make_dict(max_depth, seed=None):
1729
1730 class TestSimpleRun(helpers.ResourceUsingTestCase):
1731 def _load_merge_files(self):
1732- merge_root = self.resourceLocation('merge_sources')
1733+ merge_root = helpers.resourceLocation('merge_sources')
1734 tests = []
1735 source_ids = collections.defaultdict(list)
1736 expected_files = {}
1737diff --git a/tests/unittests/test_runs/test_merge_run.py b/tests/unittests/test_runs/test_merge_run.py
1738index 5d3f1ca..d1ac494 100644
1739--- a/tests/unittests/test_runs/test_merge_run.py
1740+++ b/tests/unittests/test_runs/test_merge_run.py
1741@@ -25,7 +25,7 @@ class TestMergeRun(helpers.FilesystemMockingTestCase):
1742 'cloud_init_modules': ['write-files'],
1743 'system_info': {'paths': {'run_dir': new_root}}
1744 }
1745- ud = self.readResource('user_data.1.txt')
1746+ ud = helpers.readResource('user_data.1.txt')
1747 cloud_cfg = util.yaml_dumps(cfg)
1748 util.ensure_dir(os.path.join(new_root, 'etc', 'cloud'))
1749 util.write_file(os.path.join(new_root, 'etc',
1750diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
1751index 5010190..e04ea03 100644
1752--- a/tests/unittests/test_util.py
1753+++ b/tests/unittests/test_util.py
1754@@ -325,7 +325,7 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
1755
1756 def test_precise_ext4_root(self):
1757
1758- lines = self.readResource('mountinfo_precise_ext4.txt').splitlines()
1759+ lines = helpers.readResource('mountinfo_precise_ext4.txt').splitlines()
1760
1761 expected = ('/dev/mapper/vg0-root', 'ext4', '/')
1762 self.assertEqual(expected, util.parse_mount_info('/', lines))
1763@@ -347,7 +347,7 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
1764 self.assertEqual(expected, util.parse_mount_info('/run/lock', lines))
1765
1766 def test_raring_btrfs_root(self):
1767- lines = self.readResource('mountinfo_raring_btrfs.txt').splitlines()
1768+ lines = helpers.readResource('mountinfo_raring_btrfs.txt').splitlines()
1769
1770 expected = ('/dev/vda1', 'btrfs', '/')
1771 self.assertEqual(expected, util.parse_mount_info('/', lines))
1772@@ -373,7 +373,7 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
1773 m_os.path.exists.return_value = True
1774 # mock subp command from util.get_mount_info_fs_on_zpool
1775 zpool_output.return_value = (
1776- self.readResource('zpool_status_simple.txt'), ''
1777+ helpers.readResource('zpool_status_simple.txt'), ''
1778 )
1779 # save function return values and do asserts
1780 ret = util.get_device_info_from_zpool('vmzroot')
1781@@ -406,7 +406,7 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
1782 m_os.path.exists.return_value = True
1783 # mock subp command from util.get_mount_info_fs_on_zpool
1784 zpool_output.return_value = (
1785- self.readResource('zpool_status_simple.txt'), 'error'
1786+ helpers.readResource('zpool_status_simple.txt'), 'error'
1787 )
1788 # save function return values and do asserts
1789 ret = util.get_device_info_from_zpool('vmzroot')
1790@@ -414,7 +414,8 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
1791
1792 @mock.patch('cloudinit.util.subp')
1793 def test_parse_mount_with_ext(self, mount_out):
1794- mount_out.return_value = (self.readResource('mount_parse_ext.txt'), '')
1795+ mount_out.return_value = (
1796+ helpers.readResource('mount_parse_ext.txt'), '')
1797 # this one is valid and exists in mount_parse_ext.txt
1798 ret = util.parse_mount('/var')
1799 self.assertEqual(('/dev/mapper/vg00-lv_var', 'ext4', '/var'), ret)
1800@@ -430,7 +431,8 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
1801
1802 @mock.patch('cloudinit.util.subp')
1803 def test_parse_mount_with_zfs(self, mount_out):
1804- mount_out.return_value = (self.readResource('mount_parse_zfs.txt'), '')
1805+ mount_out.return_value = (
1806+ helpers.readResource('mount_parse_zfs.txt'), '')
1807 # this one is valid and exists in mount_parse_zfs.txt
1808 ret = util.parse_mount('/var')
1809 self.assertEqual(('vmzroot/ROOT/freebsd/var', 'zfs', '/var'), ret)
1810@@ -800,7 +802,7 @@ class TestSubp(helpers.CiTestCase):
1811
1812 os.chmod(noshebang, os.stat(noshebang).st_mode | stat.S_IEXEC)
1813 self.assertRaisesRegex(util.ProcessExecutionError,
1814- 'Missing #! in script\?',
1815+ r'Missing #! in script\?',
1816 util.subp, (noshebang,))
1817
1818 def test_returns_none_if_no_capture(self):

Subscribers

People subscribed via source and target branches