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