Merge ~chad.smith/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel
- Git
- lp:~chad.smith/cloud-init
- ubuntu/devel
- Merge into 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) |
||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
cloud-init Commiters | Pending | ||
Review via email:
|
Commit message
Sync bugfixes from master into Bionic for release
Description of the change
To post a comment you must log in.
Revision history for this message

Server Team CI bot (server-team-bot) wrote : | # |
review:
Approve
(continuous-integration)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/MANIFEST.in b/MANIFEST.in |
2 | index 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 |
12 | diff --git a/bash_completion/cloud-init b/bash_completion/cloud-init |
13 | new file mode 100644 |
14 | index 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 |
95 | diff --git a/cloudinit/analyze/__main__.py b/cloudinit/analyze/__main__.py |
96 | index 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) |
108 | diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py |
109 | index 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 | |
121 | diff --git a/cloudinit/config/cc_disable_ec2_metadata.py b/cloudinit/config/cc_disable_ec2_metadata.py |
122 | index 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) |
151 | diff --git a/cloudinit/config/cc_power_state_change.py b/cloudinit/config/cc_power_state_change.py |
152 | index 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) |
164 | diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py |
165 | index 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): |
179 | diff --git a/cloudinit/config/tests/test_disable_ec2_metadata.py b/cloudinit/config/tests/test_disable_ec2_metadata.py |
180 | new file mode 100644 |
181 | index 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 |
235 | diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py |
236 | index 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 | |
266 | diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py |
267 | index 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): |
295 | diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py |
296 | index 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: '): |
750 | diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py |
751 | index 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)" % ( |
951 | diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py |
952 | index 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: |
1020 | diff --git a/cloudinit/tests/test_netinfo.py b/cloudinit/tests/test_netinfo.py |
1021 | index 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 |
1231 | diff --git a/cloudinit/util.py b/cloudinit/util.py |
1232 | index 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: |
1255 | diff --git a/debian/changelog b/debian/changelog |
1256 | index 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. |
1276 | diff --git a/doc/examples/cloud-config-disk-setup.txt b/doc/examples/cloud-config-disk-setup.txt |
1277 | index 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 |
1298 | diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in |
1299 | index 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 |
1310 | diff --git a/packages/suse/cloud-init.spec.in b/packages/suse/cloud-init.spec.in |
1311 | index 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}/* |
1322 | diff --git a/setup.py b/setup.py |
1323 | index 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/*')), |
1334 | diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py |
1335 | index 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']) |
1347 | diff --git a/tests/data/netinfo/netdev-formatted-output b/tests/data/netinfo/netdev-formatted-output |
1348 | new file mode 100644 |
1349 | index 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 | ++---------+------+------------------------------+---------------+--------+-------------------+ |
1363 | diff --git a/tests/data/netinfo/new-ifconfig-output b/tests/data/netinfo/new-ifconfig-output |
1364 | new file mode 100644 |
1365 | index 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 |
1387 | diff --git a/tests/data/netinfo/old-ifconfig-output b/tests/data/netinfo/old-ifconfig-output |
1388 | new file mode 100644 |
1389 | index 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 |
1411 | diff --git a/tests/data/netinfo/route-formatted-output b/tests/data/netinfo/route-formatted-output |
1412 | new file mode 100644 |
1413 | index 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 | ++-------+---------------------------+---------------------------+-----------+-------+ |
1439 | diff --git a/tests/data/netinfo/sample-ipaddrshow-output b/tests/data/netinfo/sample-ipaddrshow-output |
1440 | new file mode 100644 |
1441 | index 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 | + |
1458 | diff --git a/tests/data/netinfo/sample-iproute-output-v4 b/tests/data/netinfo/sample-iproute-output-v4 |
1459 | new file mode 100644 |
1460 | index 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 |
1467 | diff --git a/tests/data/netinfo/sample-iproute-output-v6 b/tests/data/netinfo/sample-iproute-output-v6 |
1468 | new file mode 100644 |
1469 | index 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 |
1484 | diff --git a/tests/data/netinfo/sample-route-output-v4 b/tests/data/netinfo/sample-route-output-v4 |
1485 | new file mode 100644 |
1486 | index 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 |
1495 | diff --git a/tests/data/netinfo/sample-route-output-v6 b/tests/data/netinfo/sample-route-output-v6 |
1496 | new file mode 100644 |
1497 | index 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 |
1514 | diff --git a/tests/unittests/test_datasource/test_smartos.py b/tests/unittests/test_datasource/test_smartos.py |
1515 | index 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): |
1675 | diff --git a/tests/unittests/test_filters/test_launch_index.py b/tests/unittests/test_filters/test_launch_index.py |
1676 | index 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) |
1724 | diff --git a/tests/unittests/test_merging.py b/tests/unittests/test_merging.py |
1725 | index 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 = {} |
1737 | diff --git a/tests/unittests/test_runs/test_merge_run.py b/tests/unittests/test_runs/test_merge_run.py |
1738 | index 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', |
1750 | diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py |
1751 | index 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): |
PASSED: Continuous integration, rev:555b756bde6 81bcabd68cc5fab 5a177bd73c6049 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 1027/
https:/
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: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 1027/rebuild
https:/