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

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

Commit message

Sync bugfixes from master into Bionic for release

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

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

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

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/MANIFEST.in b/MANIFEST.in
index 1a4d771..57a85ea 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,6 @@
1include *.py MANIFEST.in LICENSE* ChangeLog1include *.py MANIFEST.in LICENSE* ChangeLog
2global-include *.txt *.rst *.ini *.in *.conf *.cfg *.sh2global-include *.txt *.rst *.ini *.in *.conf *.cfg *.sh
3graft bash_completion
3graft config4graft config
4graft doc5graft doc
5graft packages6graft packages
diff --git a/bash_completion/cloud-init b/bash_completion/cloud-init
6new file mode 1006447new file mode 100644
index 0000000..581432c
--- /dev/null
+++ b/bash_completion/cloud-init
@@ -0,0 +1,77 @@
1# Copyright (C) 2018 Canonical Ltd.
2#
3# This file is part of cloud-init. See LICENSE file for license information.
4
5# bash completion for cloud-init cli
6_cloudinit_complete()
7{
8
9 local cur_word prev_word
10 cur_word="${COMP_WORDS[COMP_CWORD]}"
11 prev_word="${COMP_WORDS[COMP_CWORD-1]}"
12
13 subcmds="analyze clean collect-logs devel dhclient-hook features init modules single status"
14 base_params="--help --file --version --debug --force"
15 case ${COMP_CWORD} in
16 1)
17 COMPREPLY=($(compgen -W "$base_params $subcmds" -- $cur_word))
18 ;;
19 2)
20 case ${prev_word} in
21 analyze)
22 COMPREPLY=($(compgen -W "--help blame dump show" -- $cur_word))
23 ;;
24 clean)
25 COMPREPLY=($(compgen -W "--help --logs --reboot --seed" -- $cur_word))
26 ;;
27 collect-logs)
28 COMPREPLY=($(compgen -W "--help --tarfile --include-userdata" -- $cur_word))
29 ;;
30 devel)
31 COMPREPLY=($(compgen -W "--help schema" -- $cur_word))
32 ;;
33 dhclient-hook|features)
34 COMPREPLY=($(compgen -W "--help" -- $cur_word))
35 ;;
36 init)
37 COMPREPLY=($(compgen -W "--help --local" -- $cur_word))
38 ;;
39 modules)
40 COMPREPLY=($(compgen -W "--help --mode" -- $cur_word))
41 ;;
42
43 single)
44 COMPREPLY=($(compgen -W "--help --name --frequency --report" -- $cur_word))
45 ;;
46 status)
47 COMPREPLY=($(compgen -W "--help --long --wait" -- $cur_word))
48 ;;
49 esac
50 ;;
51 3)
52 case ${prev_word} in
53 blame|dump)
54 COMPREPLY=($(compgen -W "--help --infile --outfile" -- $cur_word))
55 ;;
56 --mode)
57 COMPREPLY=($(compgen -W "--help init config final" -- $cur_word))
58 ;;
59 --frequency)
60 COMPREPLY=($(compgen -W "--help instance always once" -- $cur_word))
61 ;;
62 schema)
63 COMPREPLY=($(compgen -W "--help --config-file --doc --annotate" -- $cur_word))
64 ;;
65 show)
66 COMPREPLY=($(compgen -W "--help --format --infile --outfile" -- $cur_word))
67 ;;
68 esac
69 ;;
70 *)
71 COMPREPLY=()
72 ;;
73 esac
74}
75complete -F _cloudinit_complete cloud-init
76
77# vi: syntax=bash expandtab
diff --git a/cloudinit/analyze/__main__.py b/cloudinit/analyze/__main__.py
index 3ba5903..f861365 100644
--- a/cloudinit/analyze/__main__.py
+++ b/cloudinit/analyze/__main__.py
@@ -69,7 +69,7 @@ def analyze_blame(name, args):
69 """69 """
70 (infh, outfh) = configure_io(args)70 (infh, outfh) = configure_io(args)
71 blame_format = ' %ds (%n)'71 blame_format = ' %ds (%n)'
72 r = re.compile('(^\s+\d+\.\d+)', re.MULTILINE)72 r = re.compile(r'(^\s+\d+\.\d+)', re.MULTILINE)
73 for idx, record in enumerate(show.show_events(_get_events(infh),73 for idx, record in enumerate(show.show_events(_get_events(infh),
74 blame_format)):74 blame_format)):
75 srecs = sorted(filter(r.match, record), reverse=True)75 srecs = sorted(filter(r.match, record), reverse=True)
diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py
index 5b9cbca..afaca46 100644
--- a/cloudinit/config/cc_apt_configure.py
+++ b/cloudinit/config/cc_apt_configure.py
@@ -121,7 +121,7 @@ and https protocols respectively. The ``proxy`` key also exists as an alias for
121All source entries in ``apt-sources`` that match regex in121All source entries in ``apt-sources`` that match regex in
122``add_apt_repo_match`` will be added to the system using122``add_apt_repo_match`` will be added to the system using
123``add-apt-repository``. If ``add_apt_repo_match`` is not specified, it defaults123``add-apt-repository``. If ``add_apt_repo_match`` is not specified, it defaults
124to ``^[\w-]+:\w``124to ``^[\\w-]+:\\w``
125125
126**Add source list entries:**126**Add source list entries:**
127127
diff --git a/cloudinit/config/cc_disable_ec2_metadata.py b/cloudinit/config/cc_disable_ec2_metadata.py
index c56319b..885b313 100644
--- a/cloudinit/config/cc_disable_ec2_metadata.py
+++ b/cloudinit/config/cc_disable_ec2_metadata.py
@@ -32,13 +32,23 @@ from cloudinit.settings import PER_ALWAYS
3232
33frequency = PER_ALWAYS33frequency = PER_ALWAYS
3434
35REJECT_CMD = ['route', 'add', '-host', '169.254.169.254', 'reject']35REJECT_CMD_IF = ['route', 'add', '-host', '169.254.169.254', 'reject']
36REJECT_CMD_IP = ['ip', 'route', 'add', 'prohibit', '169.254.169.254']
3637
3738
38def handle(name, cfg, _cloud, log, _args):39def handle(name, cfg, _cloud, log, _args):
39 disabled = util.get_cfg_option_bool(cfg, "disable_ec2_metadata", False)40 disabled = util.get_cfg_option_bool(cfg, "disable_ec2_metadata", False)
40 if disabled:41 if disabled:
41 util.subp(REJECT_CMD, capture=False)42 reject_cmd = None
43 if util.which('ip'):
44 reject_cmd = REJECT_CMD_IP
45 elif util.which('ifconfig'):
46 reject_cmd = REJECT_CMD_IF
47 else:
48 log.error(('Neither "route" nor "ip" command found, unable to '
49 'manipulate routing table'))
50 return
51 util.subp(reject_cmd, capture=False)
42 else:52 else:
43 log.debug(("Skipping module named %s,"53 log.debug(("Skipping module named %s,"
44 " disabling the ec2 route not enabled"), name)54 " disabling the ec2 route not enabled"), name)
diff --git a/cloudinit/config/cc_power_state_change.py b/cloudinit/config/cc_power_state_change.py
index 4da3a58..50b3747 100644
--- a/cloudinit/config/cc_power_state_change.py
+++ b/cloudinit/config/cc_power_state_change.py
@@ -74,7 +74,7 @@ def givecmdline(pid):
74 if util.is_FreeBSD():74 if util.is_FreeBSD():
75 (output, _err) = util.subp(['procstat', '-c', str(pid)])75 (output, _err) = util.subp(['procstat', '-c', str(pid)])
76 line = output.splitlines()[1]76 line = output.splitlines()[1]
77 m = re.search('\d+ (\w|\.|-)+\s+(/\w.+)', line)77 m = re.search(r'\d+ (\w|\.|-)+\s+(/\w.+)', line)
78 return m.group(2)78 return m.group(2)
79 else:79 else:
80 return util.load_file("/proc/%s/cmdline" % pid)80 return util.load_file("/proc/%s/cmdline" % pid)
diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py
index af08788..27d2366 100644
--- a/cloudinit/config/cc_rsyslog.py
+++ b/cloudinit/config/cc_rsyslog.py
@@ -203,8 +203,8 @@ LOG = logging.getLogger(__name__)
203COMMENT_RE = re.compile(r'[ ]*[#]+[ ]*')203COMMENT_RE = re.compile(r'[ ]*[#]+[ ]*')
204HOST_PORT_RE = re.compile(204HOST_PORT_RE = re.compile(
205 r'^(?P<proto>[@]{0,2})'205 r'^(?P<proto>[@]{0,2})'
206 '(([[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'206 r'(([[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'
207 '([:](?P<port>[0-9]+))?$')207 r'([:](?P<port>[0-9]+))?$')
208208
209209
210def reload_syslog(command=DEF_RELOAD, systemd=False):210def reload_syslog(command=DEF_RELOAD, systemd=False):
diff --git a/cloudinit/config/tests/test_disable_ec2_metadata.py b/cloudinit/config/tests/test_disable_ec2_metadata.py
211new file mode 100644211new file mode 100644
index 0000000..67646b0
--- /dev/null
+++ b/cloudinit/config/tests/test_disable_ec2_metadata.py
@@ -0,0 +1,50 @@
1# This file is part of cloud-init. See LICENSE file for license information.
2
3"""Tests cc_disable_ec2_metadata handler"""
4
5import cloudinit.config.cc_disable_ec2_metadata as ec2_meta
6
7from cloudinit.tests.helpers import CiTestCase, mock
8
9import logging
10
11LOG = logging.getLogger(__name__)
12
13DISABLE_CFG = {'disable_ec2_metadata': 'true'}
14
15
16class TestEC2MetadataRoute(CiTestCase):
17
18 with_logs = True
19
20 @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.which')
21 @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.subp')
22 def test_disable_ifconfig(self, m_subp, m_which):
23 """Set the route if ifconfig command is available"""
24 m_which.side_effect = lambda x: x if x == 'ifconfig' else None
25 ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
26 m_subp.assert_called_with(
27 ['route', 'add', '-host', '169.254.169.254', 'reject'],
28 capture=False)
29
30 @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.which')
31 @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.subp')
32 def test_disable_ip(self, m_subp, m_which):
33 """Set the route if ip command is available"""
34 m_which.side_effect = lambda x: x if x == 'ip' else None
35 ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
36 m_subp.assert_called_with(
37 ['ip', 'route', 'add', 'prohibit', '169.254.169.254'],
38 capture=False)
39
40 @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.which')
41 @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.subp')
42 def test_disable_no_tool(self, m_subp, m_which):
43 """Log error when neither route nor ip commands are available"""
44 m_which.return_value = None # Find neither ifconfig nor ip
45 ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
46 self.assertEqual(
47 [mock.call('ip'), mock.call('ifconfig')], m_which.call_args_list)
48 m_subp.assert_not_called()
49
50# vi: ts=4 expandtab
diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py
index 754d3df..099fac5 100644
--- a/cloudinit/distros/freebsd.py
+++ b/cloudinit/distros/freebsd.py
@@ -110,7 +110,7 @@ class Distro(distros.Distro):
110 if dev.startswith('lo'):110 if dev.startswith('lo'):
111 return dev111 return dev
112112
113 n = re.search('\d+$', dev)113 n = re.search(r'\d+$', dev)
114 index = n.group(0)114 index = n.group(0)
115115
116 (out, err) = util.subp(['ifconfig', '-a'])116 (out, err) = util.subp(['ifconfig', '-a'])
@@ -118,7 +118,7 @@ class Distro(distros.Distro):
118 if len(x.split()) > 0]118 if len(x.split()) > 0]
119 bsddev = 'NOT_FOUND'119 bsddev = 'NOT_FOUND'
120 for line in ifconfigoutput:120 for line in ifconfigoutput:
121 m = re.match('^\w+', line)121 m = re.match(r'^\w+', line)
122 if m:122 if m:
123 if m.group(0).startswith('lo'):123 if m.group(0).startswith('lo'):
124 continue124 continue
@@ -128,7 +128,7 @@ class Distro(distros.Distro):
128 break128 break
129129
130 # Replace the index with the one we're after.130 # Replace the index with the one we're after.
131 bsddev = re.sub('\d+$', index, bsddev)131 bsddev = re.sub(r'\d+$', index, bsddev)
132 LOG.debug("Using network interface %s", bsddev)132 LOG.debug("Using network interface %s", bsddev)
133 return bsddev133 return bsddev
134134
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 6d63e5c..72c803e 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -7,6 +7,8 @@
7import copy7import copy
8import functools8import functools
9import logging9import logging
10import socket
11import struct
1012
11import six13import six
1214
@@ -886,12 +888,9 @@ def net_prefix_to_ipv4_mask(prefix):
886 This is the inverse of ipv4_mask_to_net_prefix.888 This is the inverse of ipv4_mask_to_net_prefix.
887 24 -> "255.255.255.0"889 24 -> "255.255.255.0"
888 Also supports input as a string."""890 Also supports input as a string."""
889891 mask = socket.inet_ntoa(
890 mask = [0, 0, 0, 0]892 struct.pack(">I", (0xffffffff << (32 - int(prefix)) & 0xffffffff)))
891 for i in list(range(0, int(prefix))):893 return mask
892 idx = int(i / 8)
893 mask[idx] = mask[idx] + (1 << (7 - i % 8))
894 return ".".join([str(x) for x in mask])
895894
896895
897def ipv4_mask_to_net_prefix(mask):896def ipv4_mask_to_net_prefix(mask):
diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
index 993b26c..f090616 100644
--- a/cloudinit/netinfo.py
+++ b/cloudinit/netinfo.py
@@ -8,9 +8,11 @@
8#8#
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.
1010
11from copy import copy, deepcopy
11import re12import re
1213
13from cloudinit import log as logging14from cloudinit import log as logging
15from cloudinit.net.network_state import net_prefix_to_ipv4_mask
14from cloudinit import util16from cloudinit import util
1517
16from cloudinit.simpletable import SimpleTable18from cloudinit.simpletable import SimpleTable
@@ -18,18 +20,90 @@ from cloudinit.simpletable import SimpleTable
18LOG = logging.getLogger()20LOG = logging.getLogger()
1921
2022
21def netdev_info(empty=""):23DEFAULT_NETDEV_INFO = {
22 fields = ("hwaddr", "addr", "bcast", "mask")24 "ipv4": [],
23 (ifcfg_out, _err) = util.subp(["ifconfig", "-a"], rcs=[0, 1])25 "ipv6": [],
26 "hwaddr": "",
27 "up": False
28}
29
30
31def _netdev_info_iproute(ipaddr_out):
32 """
33 Get network device dicts from ip route and ip link info.
34
35 @param ipaddr_out: Output string from 'ip addr show' command.
36
37 @returns: A dict of device info keyed by network device name containing
38 device configuration values.
39 @raise: TypeError if ipaddr_out isn't a string.
40 """
24 devs = {}41 devs = {}
25 for line in str(ifcfg_out).splitlines():42 dev_name = None
43 for num, line in enumerate(ipaddr_out.splitlines()):
44 m = re.match(r'^\d+:\s(?P<dev>[^:]+):\s+<(?P<flags>\S+)>\s+.*', line)
45 if m:
46 dev_name = m.group('dev').lower().split('@')[0]
47 flags = m.group('flags').split(',')
48 devs[dev_name] = {
49 'ipv4': [], 'ipv6': [], 'hwaddr': '',
50 'up': bool('UP' in flags and 'LOWER_UP' in flags),
51 }
52 elif 'inet6' in line:
53 m = re.match(
54 r'\s+inet6\s(?P<ip>\S+)\sscope\s(?P<scope6>\S+).*', line)
55 if not m:
56 LOG.warning(
57 'Could not parse ip addr show: (line:%d) %s', num, line)
58 continue
59 devs[dev_name]['ipv6'].append(m.groupdict())
60 elif 'inet' in line:
61 m = re.match(
62 r'\s+inet\s(?P<cidr4>\S+)(\sbrd\s(?P<bcast>\S+))?\sscope\s'
63 r'(?P<scope>\S+).*', line)
64 if not m:
65 LOG.warning(
66 'Could not parse ip addr show: (line:%d) %s', num, line)
67 continue
68 match = m.groupdict()
69 cidr4 = match.pop('cidr4')
70 addr, _, prefix = cidr4.partition('/')
71 if not prefix:
72 prefix = '32'
73 devs[dev_name]['ipv4'].append({
74 'ip': addr,
75 'bcast': match['bcast'] if match['bcast'] else '',
76 'mask': net_prefix_to_ipv4_mask(prefix),
77 'scope': match['scope']})
78 elif 'link' in line:
79 m = re.match(
80 r'\s+link/(?P<link_type>\S+)\s(?P<hwaddr>\S+).*', line)
81 if not m:
82 LOG.warning(
83 'Could not parse ip addr show: (line:%d) %s', num, line)
84 continue
85 if m.group('link_type') == 'ether':
86 devs[dev_name]['hwaddr'] = m.group('hwaddr')
87 else:
88 devs[dev_name]['hwaddr'] = ''
89 else:
90 continue
91 return devs
92
93
94def _netdev_info_ifconfig(ifconfig_data):
95 # fields that need to be returned in devs for each dev
96 devs = {}
97 for line in ifconfig_data.splitlines():
26 if len(line) == 0:98 if len(line) == 0:
27 continue99 continue
28 if line[0] not in ("\t", " "):100 if line[0] not in ("\t", " "):
29 curdev = line.split()[0]101 curdev = line.split()[0]
30 devs[curdev] = {"up": False}102 # current ifconfig pops a ':' on the end of the device
31 for field in fields:103 if curdev.endswith(':'):
32 devs[curdev][field] = ""104 curdev = curdev[:-1]
105 if curdev not in devs:
106 devs[curdev] = deepcopy(DEFAULT_NETDEV_INFO)
33 toks = line.lower().strip().split()107 toks = line.lower().strip().split()
34 if toks[0] == "up":108 if toks[0] == "up":
35 devs[curdev]['up'] = True109 devs[curdev]['up'] = True
@@ -39,41 +113,50 @@ def netdev_info(empty=""):
39 if re.search(r"flags=\d+<up,", toks[1]):113 if re.search(r"flags=\d+<up,", toks[1]):
40 devs[curdev]['up'] = True114 devs[curdev]['up'] = True
41115
42 fieldpost = ""
43 if toks[0] == "inet6":
44 fieldpost = "6"
45
46 for i in range(len(toks)):116 for i in range(len(toks)):
47 # older net-tools (ubuntu) show 'inet addr:xx.yy',117 if toks[i] == "inet": # Create new ipv4 addr entry
48 # newer (freebsd and fedora) show 'inet xx.yy'118 devs[curdev]['ipv4'].append(
49 # just skip this 'inet' entry. (LP: #1285185)119 {'ip': toks[i + 1].lstrip("addr:")})
50 try:120 elif toks[i].startswith("bcast:"):
51 if ((toks[i] in ("inet", "inet6") and121 devs[curdev]['ipv4'][-1]['bcast'] = toks[i].lstrip("bcast:")
52 toks[i + 1].startswith("addr:"))):122 elif toks[i] == "broadcast":
53 continue123 devs[curdev]['ipv4'][-1]['bcast'] = toks[i + 1]
54 except IndexError:124 elif toks[i].startswith("mask:"):
55 pass125 devs[curdev]['ipv4'][-1]['mask'] = toks[i].lstrip("mask:")
56126 elif toks[i] == "netmask":
57 # Couple the different items we're interested in with the correct127 devs[curdev]['ipv4'][-1]['mask'] = toks[i + 1]
58 # field since FreeBSD/CentOS/Fedora differ in the output.128 elif toks[i] == "hwaddr" or toks[i] == "ether":
59 ifconfigfields = {129 devs[curdev]['hwaddr'] = toks[i + 1]
60 "addr:": "addr", "inet": "addr",130 elif toks[i] == "inet6":
61 "bcast:": "bcast", "broadcast": "bcast",131 if toks[i + 1] == "addr:":
62 "mask:": "mask", "netmask": "mask",132 devs[curdev]['ipv6'].append({'ip': toks[i + 2]})
63 "hwaddr": "hwaddr", "ether": "hwaddr",133 else:
64 "scope": "scope",134 devs[curdev]['ipv6'].append({'ip': toks[i + 1]})
65 }135 elif toks[i] == "prefixlen": # Add prefix to current ipv6 value
66 for origfield, field in ifconfigfields.items():136 addr6 = devs[curdev]['ipv6'][-1]['ip'] + "/" + toks[i + 1]
67 target = "%s%s" % (field, fieldpost)137 devs[curdev]['ipv6'][-1]['ip'] = addr6
68 if devs[curdev].get(target, ""):138 elif toks[i].startswith("scope:"):
69 continue139 devs[curdev]['ipv6'][-1]['scope6'] = toks[i].lstrip("scope:")
70 if toks[i] == "%s" % origfield:140 elif toks[i] == "scopeid":
71 try:141 res = re.match(".*<(\S+)>", toks[i + 1])
72 devs[curdev][target] = toks[i + 1]142 if res:
73 except IndexError:143 devs[curdev]['ipv6'][-1]['scope6'] = res.group(1)
74 pass144 return devs
75 elif toks[i].startswith("%s" % origfield):145
76 devs[curdev][target] = toks[i][len(field) + 1:]146
147def netdev_info(empty=""):
148 devs = {}
149 if util.which('ip'):
150 # Try iproute first of all
151 (ipaddr_out, _err) = util.subp(["ip", "addr", "show"])
152 devs = _netdev_info_iproute(ipaddr_out)
153 elif util.which('ifconfig'):
154 # Fall back to net-tools if iproute2 is not present
155 (ifcfg_out, _err) = util.subp(["ifconfig", "-a"], rcs=[0, 1])
156 devs = _netdev_info_ifconfig(ifcfg_out)
157 else:
158 LOG.warning(
159 "Could not print networks: missing 'ip' and 'ifconfig' commands")
77160
78 if empty != "":161 if empty != "":
79 for (_devname, dev) in devs.items():162 for (_devname, dev) in devs.items():
@@ -84,14 +167,94 @@ def netdev_info(empty=""):
84 return devs167 return devs
85168
86169
87def route_info():170def _netdev_route_info_iproute(iproute_data):
88 (route_out, _err) = util.subp(["netstat", "-rn"], rcs=[0, 1])171 """
172 Get network route dicts from ip route info.
173
174 @param iproute_data: Output string from ip route command.
175
176 @returns: A dict containing ipv4 and ipv6 route entries as lists. Each
177 item in the list is a route dictionary representing destination,
178 gateway, flags, genmask and interface information.
179 """
180
181 routes = {}
182 routes['ipv4'] = []
183 routes['ipv6'] = []
184 entries = iproute_data.splitlines()
185 default_route_entry = {
186 'destination': '', 'flags': '', 'gateway': '', 'genmask': '',
187 'iface': '', 'metric': ''}
188 for line in entries:
189 entry = copy(default_route_entry)
190 if not line:
191 continue
192 toks = line.split()
193 flags = ['U']
194 if toks[0] == "default":
195 entry['destination'] = "0.0.0.0"
196 entry['genmask'] = "0.0.0.0"
197 else:
198 if '/' in toks[0]:
199 (addr, cidr) = toks[0].split("/")
200 else:
201 addr = toks[0]
202 cidr = '32'
203 flags.append("H")
204 entry['genmask'] = net_prefix_to_ipv4_mask(cidr)
205 entry['destination'] = addr
206 entry['genmask'] = net_prefix_to_ipv4_mask(cidr)
207 entry['gateway'] = "0.0.0.0"
208 for i in range(len(toks)):
209 if toks[i] == "via":
210 entry['gateway'] = toks[i + 1]
211 flags.insert(1, "G")
212 if toks[i] == "dev":
213 entry["iface"] = toks[i + 1]
214 if toks[i] == "metric":
215 entry['metric'] = toks[i + 1]
216 entry['flags'] = ''.join(flags)
217 routes['ipv4'].append(entry)
218 try:
219 (iproute_data6, _err6) = util.subp(
220 ["ip", "--oneline", "-6", "route", "list", "table", "all"],
221 rcs=[0, 1])
222 except util.ProcessExecutionError:
223 pass
224 else:
225 entries6 = iproute_data6.splitlines()
226 for line in entries6:
227 entry = {}
228 if not line:
229 continue
230 toks = line.split()
231 if toks[0] == "default":
232 entry['destination'] = "::/0"
233 entry['flags'] = "UG"
234 else:
235 entry['destination'] = toks[0]
236 entry['gateway'] = "::"
237 entry['flags'] = "U"
238 for i in range(len(toks)):
239 if toks[i] == "via":
240 entry['gateway'] = toks[i + 1]
241 entry['flags'] = "UG"
242 if toks[i] == "dev":
243 entry["iface"] = toks[i + 1]
244 if toks[i] == "metric":
245 entry['metric'] = toks[i + 1]
246 if toks[i] == "expires":
247 entry['flags'] = entry['flags'] + 'e'
248 routes['ipv6'].append(entry)
249 return routes
89250
251
252def _netdev_route_info_netstat(route_data):
90 routes = {}253 routes = {}
91 routes['ipv4'] = []254 routes['ipv4'] = []
92 routes['ipv6'] = []255 routes['ipv6'] = []
93256
94 entries = route_out.splitlines()[1:]257 entries = route_data.splitlines()
95 for line in entries:258 for line in entries:
96 if not line:259 if not line:
97 continue260 continue
@@ -101,8 +264,8 @@ def route_info():
101 # default 10.65.0.1 UGS 0 34920 vtnet0264 # default 10.65.0.1 UGS 0 34920 vtnet0
102 #265 #
103 # Linux netstat shows 2 more:266 # Linux netstat shows 2 more:
104 # Destination Gateway Genmask Flags MSS Window irtt Iface267 # Destination Gateway Genmask Flags Metric Ref Use Iface
105 # 0.0.0.0 10.65.0.1 0.0.0.0 UG 0 0 0 eth0268 # 0.0.0.0 10.65.0.1 0.0.0.0 UG 0 0 0 eth0
106 if (len(toks) < 6 or toks[0] == "Kernel" or269 if (len(toks) < 6 or toks[0] == "Kernel" or
107 toks[0] == "Destination" or toks[0] == "Internet" or270 toks[0] == "Destination" or toks[0] == "Internet" or
108 toks[0] == "Internet6" or toks[0] == "Routing"):271 toks[0] == "Internet6" or toks[0] == "Routing"):
@@ -125,31 +288,57 @@ def route_info():
125 routes['ipv4'].append(entry)288 routes['ipv4'].append(entry)
126289
127 try:290 try:
128 (route_out6, _err6) = util.subp(["netstat", "-A", "inet6", "-n"],291 (route_data6, _err6) = util.subp(
129 rcs=[0, 1])292 ["netstat", "-A", "inet6", "--route", "--numeric"], rcs=[0, 1])
130 except util.ProcessExecutionError:293 except util.ProcessExecutionError:
131 pass294 pass
132 else:295 else:
133 entries6 = route_out6.splitlines()[1:]296 entries6 = route_data6.splitlines()
134 for line in entries6:297 for line in entries6:
135 if not line:298 if not line:
136 continue299 continue
137 toks = line.split()300 toks = line.split()
138 if (len(toks) < 6 or toks[0] == "Kernel" or301 if (len(toks) < 7 or toks[0] == "Kernel" or
302 toks[0] == "Destination" or toks[0] == "Internet" or
139 toks[0] == "Proto" or toks[0] == "Active"):303 toks[0] == "Proto" or toks[0] == "Active"):
140 continue304 continue
141 entry = {305 entry = {
142 'proto': toks[0],306 'destination': toks[0],
143 'recv-q': toks[1],307 'gateway': toks[1],
144 'send-q': toks[2],308 'flags': toks[2],
145 'local address': toks[3],309 'metric': toks[3],
146 'foreign address': toks[4],310 'ref': toks[4],
147 'state': toks[5],311 'use': toks[5],
312 'iface': toks[6],
148 }313 }
314 # skip lo interface on ipv6
315 if entry['iface'] == "lo":
316 continue
317 # strip /128 from address if it's included
318 if entry['destination'].endswith('/128'):
319 entry['destination'] = re.sub(
320 r'\/128$', '', entry['destination'])
149 routes['ipv6'].append(entry)321 routes['ipv6'].append(entry)
150 return routes322 return routes
151323
152324
325def route_info():
326 routes = {}
327 if util.which('ip'):
328 # Try iproute first of all
329 (iproute_out, _err) = util.subp(["ip", "-o", "route", "list"])
330 routes = _netdev_route_info_iproute(iproute_out)
331 elif util.which('netstat'):
332 # Fall back to net-tools if iproute2 is not present
333 (route_out, _err) = util.subp(
334 ["netstat", "--route", "--numeric", "--extend"], rcs=[0, 1])
335 routes = _netdev_route_info_netstat(route_out)
336 else:
337 LOG.warning(
338 "Could not print routes: missing 'ip' and 'netstat' commands")
339 return routes
340
341
153def getgateway():342def getgateway():
154 try:343 try:
155 routes = route_info()344 routes = route_info()
@@ -166,21 +355,30 @@ def netdev_pformat():
166 lines = []355 lines = []
167 try:356 try:
168 netdev = netdev_info(empty=".")357 netdev = netdev_info(empty=".")
169 except Exception:358 except Exception as e:
170 lines.append(util.center("Net device info failed", '!', 80))359 lines.append(
360 util.center(
361 "Net device info failed ({error})".format(error=str(e)),
362 '!', 80))
171 else:363 else:
364 if not netdev:
365 return '\n'
172 fields = ['Device', 'Up', 'Address', 'Mask', 'Scope', 'Hw-Address']366 fields = ['Device', 'Up', 'Address', 'Mask', 'Scope', 'Hw-Address']
173 tbl = SimpleTable(fields)367 tbl = SimpleTable(fields)
174 for (dev, d) in sorted(netdev.items()):368 for (dev, data) in sorted(netdev.items()):
175 tbl.add_row([dev, d["up"], d["addr"], d["mask"], ".", d["hwaddr"]])369 for addr in data.get('ipv4'):
176 if d.get('addr6'):370 tbl.add_row(
177 tbl.add_row([dev, d["up"],371 [dev, data["up"], addr["ip"], addr["mask"],
178 d["addr6"], ".", d.get("scope6"), d["hwaddr"]])372 addr.get('scope', '.'), data["hwaddr"]])
373 for addr in data.get('ipv6'):
374 tbl.add_row(
375 [dev, data["up"], addr["ip"], ".", addr["scope6"],
376 data["hwaddr"]])
179 netdev_s = tbl.get_string()377 netdev_s = tbl.get_string()
180 max_len = len(max(netdev_s.splitlines(), key=len))378 max_len = len(max(netdev_s.splitlines(), key=len))
181 header = util.center("Net device info", "+", max_len)379 header = util.center("Net device info", "+", max_len)
182 lines.extend([header, netdev_s])380 lines.extend([header, netdev_s])
183 return "\n".join(lines)381 return "\n".join(lines) + "\n"
184382
185383
186def route_pformat():384def route_pformat():
@@ -188,7 +386,10 @@ def route_pformat():
188 try:386 try:
189 routes = route_info()387 routes = route_info()
190 except Exception as e:388 except Exception as e:
191 lines.append(util.center('Route info failed', '!', 80))389 lines.append(
390 util.center(
391 'Route info failed ({error})'.format(error=str(e)),
392 '!', 80))
192 util.logexc(LOG, "Route info failed: %s" % e)393 util.logexc(LOG, "Route info failed: %s" % e)
193 else:394 else:
194 if routes.get('ipv4'):395 if routes.get('ipv4'):
@@ -205,20 +406,20 @@ def route_pformat():
205 header = util.center("Route IPv4 info", "+", max_len)406 header = util.center("Route IPv4 info", "+", max_len)
206 lines.extend([header, route_s])407 lines.extend([header, route_s])
207 if routes.get('ipv6'):408 if routes.get('ipv6'):
208 fields_v6 = ['Route', 'Proto', 'Recv-Q', 'Send-Q',409 fields_v6 = ['Route', 'Destination', 'Gateway', 'Interface',
209 'Local Address', 'Foreign Address', 'State']410 'Flags']
210 tbl_v6 = SimpleTable(fields_v6)411 tbl_v6 = SimpleTable(fields_v6)
211 for (n, r) in enumerate(routes.get('ipv6')):412 for (n, r) in enumerate(routes.get('ipv6')):
212 route_id = str(n)413 route_id = str(n)
213 tbl_v6.add_row([route_id, r['proto'],414 if r['iface'] == 'lo':
214 r['recv-q'], r['send-q'],415 continue
215 r['local address'], r['foreign address'],416 tbl_v6.add_row([route_id, r['destination'],
216 r['state']])417 r['gateway'], r['iface'], r['flags']])
217 route_s = tbl_v6.get_string()418 route_s = tbl_v6.get_string()
218 max_len = len(max(route_s.splitlines(), key=len))419 max_len = len(max(route_s.splitlines(), key=len))
219 header = util.center("Route IPv6 info", "+", max_len)420 header = util.center("Route IPv6 info", "+", max_len)
220 lines.extend([header, route_s])421 lines.extend([header, route_s])
221 return "\n".join(lines)422 return "\n".join(lines) + "\n"
222423
223424
224def debug_info(prefix='ci-info: '):425def debug_info(prefix='ci-info: '):
diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py
index 86bfa5d..c8998b4 100644
--- a/cloudinit/sources/DataSourceSmartOS.py
+++ b/cloudinit/sources/DataSourceSmartOS.py
@@ -1,4 +1,5 @@
1# Copyright (C) 2013 Canonical Ltd.1# Copyright (C) 2013 Canonical Ltd.
2# Copyright (c) 2018, Joyent, Inc.
2#3#
3# Author: Ben Howard <ben.howard@canonical.com>4# Author: Ben Howard <ben.howard@canonical.com>
4#5#
@@ -21,6 +22,7 @@
2122
22import base6423import base64
23import binascii24import binascii
25import errno
24import json26import json
25import os27import os
26import random28import random
@@ -108,7 +110,7 @@ BUILTIN_CLOUD_CONFIG = {
108 'overwrite': False}110 'overwrite': False}
109 },111 },
110 'fs_setup': [{'label': 'ephemeral0',112 'fs_setup': [{'label': 'ephemeral0',
111 'filesystem': 'ext3',113 'filesystem': 'ext4',
112 'device': 'ephemeral0'}],114 'device': 'ephemeral0'}],
113}115}
114116
@@ -229,6 +231,9 @@ class DataSourceSmartOS(sources.DataSource):
229 self.md_client)231 self.md_client)
230 return False232 return False
231233
234 # Open once for many requests, rather than once for each request
235 self.md_client.open_transport()
236
232 for ci_noun, attribute in SMARTOS_ATTRIB_MAP.items():237 for ci_noun, attribute in SMARTOS_ATTRIB_MAP.items():
233 smartos_noun, strip = attribute238 smartos_noun, strip = attribute
234 md[ci_noun] = self.md_client.get(smartos_noun, strip=strip)239 md[ci_noun] = self.md_client.get(smartos_noun, strip=strip)
@@ -236,6 +241,8 @@ class DataSourceSmartOS(sources.DataSource):
236 for ci_noun, smartos_noun in SMARTOS_ATTRIB_JSON.items():241 for ci_noun, smartos_noun in SMARTOS_ATTRIB_JSON.items():
237 md[ci_noun] = self.md_client.get_json(smartos_noun)242 md[ci_noun] = self.md_client.get_json(smartos_noun)
238243
244 self.md_client.close_transport()
245
239 # @datadictionary: This key may contain a program that is written246 # @datadictionary: This key may contain a program that is written
240 # to a file in the filesystem of the guest on each boot and then247 # to a file in the filesystem of the guest on each boot and then
241 # executed. It may be of any format that would be considered248 # executed. It may be of any format that would be considered
@@ -316,6 +323,10 @@ class JoyentMetadataFetchException(Exception):
316 pass323 pass
317324
318325
326class JoyentMetadataTimeoutException(JoyentMetadataFetchException):
327 pass
328
329
319class JoyentMetadataClient(object):330class JoyentMetadataClient(object):
320 """331 """
321 A client implementing v2 of the Joyent Metadata Protocol Specification.332 A client implementing v2 of the Joyent Metadata Protocol Specification.
@@ -360,6 +371,47 @@ class JoyentMetadataClient(object):
360 LOG.debug('Value "%s" found.', value)371 LOG.debug('Value "%s" found.', value)
361 return value372 return value
362373
374 def _readline(self):
375 """
376 Reads a line a byte at a time until \n is encountered. Returns an
377 ascii string with the trailing newline removed.
378
379 If a timeout (per-byte) is set and it expires, a
380 JoyentMetadataFetchException will be thrown.
381 """
382 response = []
383
384 def as_ascii():
385 return b''.join(response).decode('ascii')
386
387 msg = "Partial response: '%s'"
388 while True:
389 try:
390 byte = self.fp.read(1)
391 if len(byte) == 0:
392 raise JoyentMetadataTimeoutException(msg % as_ascii())
393 if byte == b'\n':
394 return as_ascii()
395 response.append(byte)
396 except OSError as exc:
397 if exc.errno == errno.EAGAIN:
398 raise JoyentMetadataTimeoutException(msg % as_ascii())
399 raise
400
401 def _write(self, msg):
402 self.fp.write(msg.encode('ascii'))
403 self.fp.flush()
404
405 def _negotiate(self):
406 LOG.debug('Negotiating protocol V2')
407 self._write('NEGOTIATE V2\n')
408 response = self._readline()
409 LOG.debug('read "%s"', response)
410 if response != 'V2_OK':
411 raise JoyentMetadataFetchException(
412 'Invalid response "%s" to "NEGOTIATE V2"' % response)
413 LOG.debug('Negotiation complete')
414
363 def request(self, rtype, param=None):415 def request(self, rtype, param=None):
364 request_id = '{0:08x}'.format(random.randint(0, 0xffffffff))416 request_id = '{0:08x}'.format(random.randint(0, 0xffffffff))
365 message_body = ' '.join((request_id, rtype,))417 message_body = ' '.join((request_id, rtype,))
@@ -374,18 +426,11 @@ class JoyentMetadataClient(object):
374 self.open_transport()426 self.open_transport()
375 need_close = True427 need_close = True
376428
377 self.fp.write(msg.encode('ascii'))429 self._write(msg)
378 self.fp.flush()430 response = self._readline()
379
380 response = bytearray()
381 response.extend(self.fp.read(1))
382 while response[-1:] != b'\n':
383 response.extend(self.fp.read(1))
384
385 if need_close:431 if need_close:
386 self.close_transport()432 self.close_transport()
387433
388 response = response.rstrip().decode('ascii')
389 LOG.debug('Read "%s" from metadata transport.', response)434 LOG.debug('Read "%s" from metadata transport.', response)
390435
391 if 'SUCCESS' not in response:436 if 'SUCCESS' not in response:
@@ -450,6 +495,7 @@ class JoyentMetadataSocketClient(JoyentMetadataClient):
450 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)495 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
451 sock.connect(self.socketpath)496 sock.connect(self.socketpath)
452 self.fp = sock.makefile('rwb')497 self.fp = sock.makefile('rwb')
498 self._negotiate()
453499
454 def exists(self):500 def exists(self):
455 return os.path.exists(self.socketpath)501 return os.path.exists(self.socketpath)
@@ -459,8 +505,9 @@ class JoyentMetadataSocketClient(JoyentMetadataClient):
459505
460506
461class JoyentMetadataSerialClient(JoyentMetadataClient):507class JoyentMetadataSerialClient(JoyentMetadataClient):
462 def __init__(self, device, timeout=10, smartos_type=SMARTOS_ENV_KVM):508 def __init__(self, device, timeout=10, smartos_type=SMARTOS_ENV_KVM,
463 super(JoyentMetadataSerialClient, self).__init__(smartos_type)509 fp=None):
510 super(JoyentMetadataSerialClient, self).__init__(smartos_type, fp)
464 self.device = device511 self.device = device
465 self.timeout = timeout512 self.timeout = timeout
466513
@@ -468,10 +515,50 @@ class JoyentMetadataSerialClient(JoyentMetadataClient):
468 return os.path.exists(self.device)515 return os.path.exists(self.device)
469516
470 def open_transport(self):517 def open_transport(self):
471 ser = serial.Serial(self.device, timeout=self.timeout)518 if self.fp is None:
472 if not ser.isOpen():519 ser = serial.Serial(self.device, timeout=self.timeout)
473 raise SystemError("Unable to open %s" % self.device)520 if not ser.isOpen():
474 self.fp = ser521 raise SystemError("Unable to open %s" % self.device)
522 self.fp = ser
523 self._flush()
524 self._negotiate()
525
526 def _flush(self):
527 LOG.debug('Flushing input')
528 # Read any pending data
529 timeout = self.fp.timeout
530 self.fp.timeout = 0.1
531 while True:
532 try:
533 self._readline()
534 except JoyentMetadataTimeoutException:
535 break
536 LOG.debug('Input empty')
537
538 # Send a newline and expect "invalid command". Keep trying until
539 # successful. Retry rather frequently so that the "Is the host
540 # metadata service running" appears on the console soon after someone
541 # attaches in an effort to debug.
542 if timeout > 5:
543 self.fp.timeout = 5
544 else:
545 self.fp.timeout = timeout
546 while True:
547 LOG.debug('Writing newline, expecting "invalid command"')
548 self._write('\n')
549 try:
550 response = self._readline()
551 if response == 'invalid command':
552 break
553 if response == 'FAILURE':
554 LOG.debug('Got "FAILURE". Retrying.')
555 continue
556 LOG.warning('Unexpected response "%s" during flush', response)
557 except JoyentMetadataTimeoutException:
558 LOG.warning('Timeout while initializing metadata client. ' +
559 'Is the host metadata service running?')
560 LOG.debug('Got "invalid command". Flush complete.')
561 self.fp.timeout = timeout
475562
476 def __repr__(self):563 def __repr__(self):
477 return "%s(device=%s, timeout=%s)" % (564 return "%s(device=%s, timeout=%s)" % (
diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
index 999b1d7..82fd347 100644
--- a/cloudinit/tests/helpers.py
+++ b/cloudinit/tests/helpers.py
@@ -190,35 +190,11 @@ class ResourceUsingTestCase(CiTestCase):
190 super(ResourceUsingTestCase, self).setUp()190 super(ResourceUsingTestCase, self).setUp()
191 self.resource_path = None191 self.resource_path = None
192192
193 def resourceLocation(self, subname=None):
194 if self.resource_path is None:
195 paths = [
196 os.path.join('tests', 'data'),
197 os.path.join('data'),
198 os.path.join(os.pardir, 'tests', 'data'),
199 os.path.join(os.pardir, 'data'),
200 ]
201 for p in paths:
202 if os.path.isdir(p):
203 self.resource_path = p
204 break
205 self.assertTrue((self.resource_path and
206 os.path.isdir(self.resource_path)),
207 msg="Unable to locate test resource data path!")
208 if not subname:
209 return self.resource_path
210 return os.path.join(self.resource_path, subname)
211
212 def readResource(self, name):
213 where = self.resourceLocation(name)
214 with open(where, 'r') as fh:
215 return fh.read()
216
217 def getCloudPaths(self, ds=None):193 def getCloudPaths(self, ds=None):
218 tmpdir = tempfile.mkdtemp()194 tmpdir = tempfile.mkdtemp()
219 self.addCleanup(shutil.rmtree, tmpdir)195 self.addCleanup(shutil.rmtree, tmpdir)
220 cp = ch.Paths({'cloud_dir': tmpdir,196 cp = ch.Paths({'cloud_dir': tmpdir,
221 'templates_dir': self.resourceLocation()},197 'templates_dir': resourceLocation()},
222 ds=ds)198 ds=ds)
223 return cp199 return cp
224200
@@ -234,7 +210,7 @@ class FilesystemMockingTestCase(ResourceUsingTestCase):
234 ResourceUsingTestCase.tearDown(self)210 ResourceUsingTestCase.tearDown(self)
235211
236 def replicateTestRoot(self, example_root, target_root):212 def replicateTestRoot(self, example_root, target_root):
237 real_root = self.resourceLocation()213 real_root = resourceLocation()
238 real_root = os.path.join(real_root, 'roots', example_root)214 real_root = os.path.join(real_root, 'roots', example_root)
239 for (dir_path, _dirnames, filenames) in os.walk(real_root):215 for (dir_path, _dirnames, filenames) in os.walk(real_root):
240 real_path = dir_path216 real_path = dir_path
@@ -399,6 +375,18 @@ def wrap_and_call(prefix, mocks, func, *args, **kwargs):
399 p.stop()375 p.stop()
400376
401377
378def resourceLocation(subname=None):
379 path = os.path.join('tests', 'data')
380 if not subname:
381 return path
382 return os.path.join(path, subname)
383
384
385def readResource(name, mode='r'):
386 with open(resourceLocation(name), mode) as fh:
387 return fh.read()
388
389
402try:390try:
403 skipIf = unittest.skipIf391 skipIf = unittest.skipIf
404except AttributeError:392except AttributeError:
diff --git a/cloudinit/tests/test_netinfo.py b/cloudinit/tests/test_netinfo.py
index 7dea2e4..2537c1c 100644
--- a/cloudinit/tests/test_netinfo.py
+++ b/cloudinit/tests/test_netinfo.py
@@ -2,105 +2,121 @@
22
3"""Tests netinfo module functions and classes."""3"""Tests netinfo module functions and classes."""
44
5from copy import copy
6
5from cloudinit.netinfo import netdev_pformat, route_pformat7from cloudinit.netinfo import netdev_pformat, route_pformat
6from cloudinit.tests.helpers import CiTestCase, mock8from cloudinit.tests.helpers import CiTestCase, mock, readResource
79
810
9# Example ifconfig and route output11# Example ifconfig and route output
10SAMPLE_IFCONFIG_OUT = """\12SAMPLE_OLD_IFCONFIG_OUT = readResource("netinfo/old-ifconfig-output")
11enp0s25 Link encap:Ethernet HWaddr 50:7b:9d:2c:af:9113SAMPLE_NEW_IFCONFIG_OUT = readResource("netinfo/new-ifconfig-output")
12 inet addr:192.168.2.18 Bcast:192.168.2.255 Mask:255.255.255.014SAMPLE_IPADDRSHOW_OUT = readResource("netinfo/sample-ipaddrshow-output")
13 inet6 addr: fe80::8107:2b92:867e:f8a6/64 Scope:Link15SAMPLE_ROUTE_OUT_V4 = readResource("netinfo/sample-route-output-v4")
14 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:116SAMPLE_ROUTE_OUT_V6 = readResource("netinfo/sample-route-output-v6")
15 RX packets:8106427 errors:55 dropped:0 overruns:0 frame:3717SAMPLE_IPROUTE_OUT_V4 = readResource("netinfo/sample-iproute-output-v4")
16 TX packets:9339739 errors:0 dropped:0 overruns:0 carrier:018SAMPLE_IPROUTE_OUT_V6 = readResource("netinfo/sample-iproute-output-v6")
17 collisions:0 txqueuelen:100019NETDEV_FORMATTED_OUT = readResource("netinfo/netdev-formatted-output")
18 RX bytes:4953721719 (4.9 GB) TX bytes:7731890194 (7.7 GB)20ROUTE_FORMATTED_OUT = readResource("netinfo/route-formatted-output")
19 Interrupt:20 Memory:e1200000-e1220000
20
21lo Link encap:Local Loopback
22 inet addr:127.0.0.1 Mask:255.0.0.0
23 inet6 addr: ::1/128 Scope:Host
24 UP LOOPBACK RUNNING MTU:65536 Metric:1
25 RX packets:579230851 errors:0 dropped:0 overruns:0 frame:0
26 TX packets:579230851 errors:0 dropped:0 overruns:0 carrier:0
27 collisions:0 txqueuelen:1
28"""
29
30SAMPLE_ROUTE_OUT = '\n'.join([
31 '0.0.0.0 192.168.2.1 0.0.0.0 UG 0 0 0'
32 ' enp0s25',
33 '0.0.0.0 192.168.2.1 0.0.0.0 UG 0 0 0'
34 ' wlp3s0',
35 '192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0'
36 ' enp0s25'])
37
38
39NETDEV_FORMATTED_OUT = '\n'.join([
40 '+++++++++++++++++++++++++++++++++++++++Net device info+++++++++++++++++++'
41 '++++++++++++++++++++',
42 '+---------+------+------------------------------+---------------+-------+'
43 '-------------------+',
44 '| Device | Up | Address | Mask | Scope |'
45 ' Hw-Address |',
46 '+---------+------+------------------------------+---------------+-------+'
47 '-------------------+',
48 '| enp0s25 | True | 192.168.2.18 | 255.255.255.0 | . |'
49 ' 50:7b:9d:2c:af:91 |',
50 '| enp0s25 | True | fe80::8107:2b92:867e:f8a6/64 | . | link |'
51 ' 50:7b:9d:2c:af:91 |',
52 '| lo | True | 127.0.0.1 | 255.0.0.0 | . |'
53 ' . |',
54 '| lo | True | ::1/128 | . | host |'
55 ' . |',
56 '+---------+------+------------------------------+---------------+-------+'
57 '-------------------+'])
58
59ROUTE_FORMATTED_OUT = '\n'.join([
60 '+++++++++++++++++++++++++++++Route IPv4 info++++++++++++++++++++++++++'
61 '+++',
62 '+-------+-------------+-------------+---------------+-----------+-----'
63 '--+',
64 '| Route | Destination | Gateway | Genmask | Interface | Flags'
65 ' |',
66 '+-------+-------------+-------------+---------------+-----------+'
67 '-------+',
68 '| 0 | 0.0.0.0 | 192.168.2.1 | 0.0.0.0 | wlp3s0 |'
69 ' UG |',
70 '| 1 | 192.168.2.0 | 0.0.0.0 | 255.255.255.0 | enp0s25 |'
71 ' U |',
72 '+-------+-------------+-------------+---------------+-----------+'
73 '-------+',
74 '++++++++++++++++++++++++++++++++++++++++Route IPv6 info++++++++++'
75 '++++++++++++++++++++++++++++++',
76 '+-------+-------------+-------------+---------------+---------------+'
77 '-----------------+-------+',
78 '| Route | Proto | Recv-Q | Send-Q | Local Address |'
79 ' Foreign Address | State |',
80 '+-------+-------------+-------------+---------------+---------------+'
81 '-----------------+-------+',
82 '| 0 | 0.0.0.0 | 192.168.2.1 | 0.0.0.0 | UG |'
83 ' 0 | 0 |',
84 '| 1 | 192.168.2.0 | 0.0.0.0 | 255.255.255.0 | U |'
85 ' 0 | 0 |',
86 '+-------+-------------+-------------+---------------+---------------+'
87 '-----------------+-------+'])
8821
8922
90class TestNetInfo(CiTestCase):23class TestNetInfo(CiTestCase):
9124
92 maxDiff = None25 maxDiff = None
26 with_logs = True
27
28 @mock.patch('cloudinit.netinfo.util.which')
29 @mock.patch('cloudinit.netinfo.util.subp')
30 def test_netdev_old_nettools_pformat(self, m_subp, m_which):
31 """netdev_pformat properly rendering old nettools info."""
32 m_subp.return_value = (SAMPLE_OLD_IFCONFIG_OUT, '')
33 m_which.side_effect = lambda x: x if x == 'ifconfig' else None
34 content = netdev_pformat()
35 self.assertEqual(NETDEV_FORMATTED_OUT, content)
9336
37 @mock.patch('cloudinit.netinfo.util.which')
94 @mock.patch('cloudinit.netinfo.util.subp')38 @mock.patch('cloudinit.netinfo.util.subp')
95 def test_netdev_pformat(self, m_subp):39 def test_netdev_new_nettools_pformat(self, m_subp, m_which):
96 """netdev_pformat properly rendering network device information."""40 """netdev_pformat properly rendering netdev new nettools info."""
97 m_subp.return_value = (SAMPLE_IFCONFIG_OUT, '')41 m_subp.return_value = (SAMPLE_NEW_IFCONFIG_OUT, '')
42 m_which.side_effect = lambda x: x if x == 'ifconfig' else None
98 content = netdev_pformat()43 content = netdev_pformat()
99 self.assertEqual(NETDEV_FORMATTED_OUT, content)44 self.assertEqual(NETDEV_FORMATTED_OUT, content)
10045
46 @mock.patch('cloudinit.netinfo.util.which')
47 @mock.patch('cloudinit.netinfo.util.subp')
48 def test_netdev_iproute_pformat(self, m_subp, m_which):
49 """netdev_pformat properly rendering ip route info."""
50 m_subp.return_value = (SAMPLE_IPADDRSHOW_OUT, '')
51 m_which.side_effect = lambda x: x if x == 'ip' else None
52 content = netdev_pformat()
53 new_output = copy(NETDEV_FORMATTED_OUT)
54 # ip route show describes global scopes on ipv4 addresses
55 # whereas ifconfig does not. Add proper global/host scope to output.
56 new_output = new_output.replace('| . | 50:7b', '| global | 50:7b')
57 new_output = new_output.replace(
58 '255.0.0.0 | . |', '255.0.0.0 | host |')
59 self.assertEqual(new_output, content)
60
61 @mock.patch('cloudinit.netinfo.util.which')
62 @mock.patch('cloudinit.netinfo.util.subp')
63 def test_netdev_warn_on_missing_commands(self, m_subp, m_which):
64 """netdev_pformat warns when missing both ip and 'netstat'."""
65 m_which.return_value = None # Niether ip nor netstat found
66 content = netdev_pformat()
67 self.assertEqual('\n', content)
68 self.assertEqual(
69 "WARNING: Could not print networks: missing 'ip' and 'ifconfig'"
70 " commands\n",
71 self.logs.getvalue())
72 m_subp.assert_not_called()
73
74 @mock.patch('cloudinit.netinfo.util.which')
101 @mock.patch('cloudinit.netinfo.util.subp')75 @mock.patch('cloudinit.netinfo.util.subp')
102 def test_route_pformat(self, m_subp):76 def test_route_nettools_pformat(self, m_subp, m_which):
103 """netdev_pformat properly rendering network device information."""77 """route_pformat properly rendering nettools route info."""
104 m_subp.return_value = (SAMPLE_ROUTE_OUT, '')78
79 def subp_netstat_route_selector(*args, **kwargs):
80 if args[0] == ['netstat', '--route', '--numeric', '--extend']:
81 return (SAMPLE_ROUTE_OUT_V4, '')
82 if args[0] == ['netstat', '-A', 'inet6', '--route', '--numeric']:
83 return (SAMPLE_ROUTE_OUT_V6, '')
84 raise Exception('Unexpected subp call %s' % args[0])
85
86 m_subp.side_effect = subp_netstat_route_selector
87 m_which.side_effect = lambda x: x if x == 'netstat' else None
105 content = route_pformat()88 content = route_pformat()
106 self.assertEqual(ROUTE_FORMATTED_OUT, content)89 self.assertEqual(ROUTE_FORMATTED_OUT, content)
90
91 @mock.patch('cloudinit.netinfo.util.which')
92 @mock.patch('cloudinit.netinfo.util.subp')
93 def test_route_iproute_pformat(self, m_subp, m_which):
94 """route_pformat properly rendering ip route info."""
95
96 def subp_iproute_selector(*args, **kwargs):
97 if ['ip', '-o', 'route', 'list'] == args[0]:
98 return (SAMPLE_IPROUTE_OUT_V4, '')
99 v6cmd = ['ip', '--oneline', '-6', 'route', 'list', 'table', 'all']
100 if v6cmd == args[0]:
101 return (SAMPLE_IPROUTE_OUT_V6, '')
102 raise Exception('Unexpected subp call %s' % args[0])
103
104 m_subp.side_effect = subp_iproute_selector
105 m_which.side_effect = lambda x: x if x == 'ip' else None
106 content = route_pformat()
107 self.assertEqual(ROUTE_FORMATTED_OUT, content)
108
109 @mock.patch('cloudinit.netinfo.util.which')
110 @mock.patch('cloudinit.netinfo.util.subp')
111 def test_route_warn_on_missing_commands(self, m_subp, m_which):
112 """route_pformat warns when missing both ip and 'netstat'."""
113 m_which.return_value = None # Niether ip nor netstat found
114 content = route_pformat()
115 self.assertEqual('\n', content)
116 self.assertEqual(
117 "WARNING: Could not print routes: missing 'ip' and 'netstat'"
118 " commands\n",
119 self.logs.getvalue())
120 m_subp.assert_not_called()
121
122# vi: ts=4 expandtab
diff --git a/cloudinit/util.py b/cloudinit/util.py
index acdc0d8..1717b52 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -1446,7 +1446,7 @@ def get_config_logfiles(cfg):
1446 for fmt in get_output_cfg(cfg, None):1446 for fmt in get_output_cfg(cfg, None):
1447 if not fmt:1447 if not fmt:
1448 continue1448 continue
1449 match = re.match('(?P<type>\||>+)\s*(?P<target>.*)', fmt)1449 match = re.match(r'(?P<type>\||>+)\s*(?P<target>.*)', fmt)
1450 if not match:1450 if not match:
1451 continue1451 continue
1452 target = match.group('target')1452 target = match.group('target')
@@ -2275,8 +2275,8 @@ def parse_mount(path):
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:
2276 # https://regex101.com/r/2F6c1k/12276 # https://regex101.com/r/2F6c1k/1
2277 # https://regex101.com/r/T2en7a/12277 # https://regex101.com/r/T2en7a/1
2278 regex = r'^(/dev/[\S]+|.*zroot\S*?) on (/[\S]*) ' + \2278 regex = (r'^(/dev/[\S]+|.*zroot\S*?) on (/[\S]*) '
2279 '(?=(?:type)[\s]+([\S]+)|\(([^,]*))'2279 r'(?=(?:type)[\s]+([\S]+)|\(([^,]*))')
2280 for line in mount_locs:2280 for line in mount_locs:
2281 m = re.search(regex, line)2281 m = re.search(regex, line)
2282 if not m:2282 if not m:
diff --git a/debian/changelog b/debian/changelog
index d3a4234..45016a5 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,16 @@
1cloud-init (18.2-14-g6d48d265-0ubuntu1) bionic; urgency=medium
2
3 * New upstream snapshot.
4 - net: Depend on iproute2's ip instead of net-tools ifconfig or route
5 - DataSourceSmartOS: fix hang when metadata service is down
6 [Mike Gerdts] (LP: #1667735)
7 - DataSourceSmartOS: change default fs on ephemeral disk from ext3 to
8 ext4. [Mike Gerdts] (LP: #1763511)
9 - pycodestyle: Fix invalid escape sequences in string literals.
10 - Implement bash completion script for cloud-init command line
11
12 -- Chad Smith <chad.smith@canonical.com> Wed, 18 Apr 2018 15:25:53 -0600
13
1cloud-init (18.2-9-g49b562c9-0ubuntu1) bionic; urgency=medium14cloud-init (18.2-9-g49b562c9-0ubuntu1) bionic; urgency=medium
215
3 * New upstream snapshot.16 * New upstream snapshot.
diff --git a/doc/examples/cloud-config-disk-setup.txt b/doc/examples/cloud-config-disk-setup.txt
index dd91477..43a62a2 100644
--- a/doc/examples/cloud-config-disk-setup.txt
+++ b/doc/examples/cloud-config-disk-setup.txt
@@ -37,7 +37,7 @@ fs_setup:
37# Default disk definitions for SmartOS37# Default disk definitions for SmartOS
38# ------------------------------------38# ------------------------------------
3939
40device_aliases: {'ephemeral0': '/dev/sdb'}40device_aliases: {'ephemeral0': '/dev/vdb'}
41disk_setup:41disk_setup:
42 ephemeral0:42 ephemeral0:
43 table_type: mbr43 table_type: mbr
@@ -46,7 +46,7 @@ disk_setup:
4646
47fs_setup:47fs_setup:
48 - label: ephemeral048 - label: ephemeral0
49 filesystem: ext349 filesystem: ext4
50 device: ephemeral0.050 device: ephemeral0.0
5151
52# Cavaut for SmartOS: if ephemeral disk is not defined, then the disk will52# Cavaut for SmartOS: if ephemeral disk is not defined, then the disk will
diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in
index 6ab0d20..91faf3c 100644
--- a/packages/redhat/cloud-init.spec.in
+++ b/packages/redhat/cloud-init.spec.in
@@ -197,6 +197,7 @@ fi
197%dir %{_sysconfdir}/cloud/templates197%dir %{_sysconfdir}/cloud/templates
198%config(noreplace) %{_sysconfdir}/cloud/templates/*198%config(noreplace) %{_sysconfdir}/cloud/templates/*
199%config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf199%config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf
200%{_sysconfdir}/bash_completion.d/cloud-init
200201
201%{_libexecdir}/%{name}202%{_libexecdir}/%{name}
202%dir %{_sharedstatedir}/cloud203%dir %{_sharedstatedir}/cloud
diff --git a/packages/suse/cloud-init.spec.in b/packages/suse/cloud-init.spec.in
index 86e18b1..bbb965a 100644
--- a/packages/suse/cloud-init.spec.in
+++ b/packages/suse/cloud-init.spec.in
@@ -136,6 +136,7 @@ mkdir -p %{buildroot}/var/lib/cloud
136%config(noreplace) %{_sysconfdir}/cloud/cloud.cfg.d/README136%config(noreplace) %{_sysconfdir}/cloud/cloud.cfg.d/README
137%dir %{_sysconfdir}/cloud/templates137%dir %{_sysconfdir}/cloud/templates
138%config(noreplace) %{_sysconfdir}/cloud/templates/*138%config(noreplace) %{_sysconfdir}/cloud/templates/*
139%{_sysconfdir}/bash_completion.d/cloud-init
139140
140# Python code is here...141# Python code is here...
141%{python_sitelib}/*142%{python_sitelib}/*
diff --git a/setup.py b/setup.py
index bc3f52a..85b2337 100755
--- a/setup.py
+++ b/setup.py
@@ -228,6 +228,7 @@ if not in_virtualenv():
228 INITSYS_ROOTS[k] = "/" + INITSYS_ROOTS[k]228 INITSYS_ROOTS[k] = "/" + INITSYS_ROOTS[k]
229229
230data_files = [230data_files = [
231 (ETC + '/bash_completion.d', ['bash_completion/cloud-init']),
231 (ETC + '/cloud', [render_tmpl("config/cloud.cfg.tmpl")]),232 (ETC + '/cloud', [render_tmpl("config/cloud.cfg.tmpl")]),
232 (ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')),233 (ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')),
233 (ETC + '/cloud/templates', glob('templates/*')),234 (ETC + '/cloud/templates', glob('templates/*')),
diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py
index 7598d46..4fda8f9 100644
--- a/tests/cloud_tests/testcases/base.py
+++ b/tests/cloud_tests/testcases/base.py
@@ -235,7 +235,7 @@ class CloudTestCase(unittest.TestCase):
235 'found unexpected kvm availability-zone %s' %235 'found unexpected kvm availability-zone %s' %
236 v1_data['availability-zone'])236 v1_data['availability-zone'])
237 self.assertIsNotNone(237 self.assertIsNotNone(
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}',
239 v1_data['instance-id']),239 v1_data['instance-id']),
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'])
241 self.assertIn('ubuntu', v1_data['local-hostname'])241 self.assertIn('ubuntu', v1_data['local-hostname'])
diff --git a/tests/data/netinfo/netdev-formatted-output b/tests/data/netinfo/netdev-formatted-output
242new file mode 100644242new file mode 100644
index 0000000..283ab4a
--- /dev/null
+++ b/tests/data/netinfo/netdev-formatted-output
@@ -0,0 +1,10 @@
1+++++++++++++++++++++++++++++++++++++++Net device info++++++++++++++++++++++++++++++++++++++++
2+---------+------+------------------------------+---------------+--------+-------------------+
3| Device | Up | Address | Mask | Scope | Hw-Address |
4+---------+------+------------------------------+---------------+--------+-------------------+
5| enp0s25 | True | 192.168.2.18 | 255.255.255.0 | . | 50:7b:9d:2c:af:91 |
6| enp0s25 | True | fe80::7777:2222:1111:eeee/64 | . | global | 50:7b:9d:2c:af:91 |
7| enp0s25 | True | fe80::8107:2b92:867e:f8a6/64 | . | link | 50:7b:9d:2c:af:91 |
8| lo | True | 127.0.0.1 | 255.0.0.0 | . | . |
9| lo | True | ::1/128 | . | host | . |
10+---------+------+------------------------------+---------------+--------+-------------------+
diff --git a/tests/data/netinfo/new-ifconfig-output b/tests/data/netinfo/new-ifconfig-output
0new file mode 10064411new file mode 100644
index 0000000..83d4ad1
--- /dev/null
+++ b/tests/data/netinfo/new-ifconfig-output
@@ -0,0 +1,18 @@
1enp0s25: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
2 inet 192.168.2.18 netmask 255.255.255.0 broadcast 192.168.2.255
3 inet6 fe80::7777:2222:1111:eeee prefixlen 64 scopeid 0x30<global>
4 inet6 fe80::8107:2b92:867e:f8a6 prefixlen 64 scopeid 0x20<link>
5 ether 50:7b:9d:2c:af:91 txqueuelen 1000 (Ethernet)
6 RX packets 3017 bytes 10601563 (10.1 MiB)
7 RX errors 0 dropped 39 overruns 0 frame 0
8 TX packets 2627 bytes 196976 (192.3 KiB)
9 TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
10
11lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
12 inet 127.0.0.1 netmask 255.0.0.0
13 inet6 ::1 prefixlen 128 scopeid 0x10<host>
14 loop txqueuelen 1 (Local Loopback)
15 RX packets 0 bytes 0 (0.0 B)
16 RX errors 0 dropped 0 overruns 0 frame 0
17 TX packets 0 bytes 0 (0.0 B)
18 TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
diff --git a/tests/data/netinfo/old-ifconfig-output b/tests/data/netinfo/old-ifconfig-output
0new file mode 10064419new file mode 100644
index 0000000..e01f763
--- /dev/null
+++ b/tests/data/netinfo/old-ifconfig-output
@@ -0,0 +1,18 @@
1enp0s25 Link encap:Ethernet HWaddr 50:7b:9d:2c:af:91
2 inet addr:192.168.2.18 Bcast:192.168.2.255 Mask:255.255.255.0
3 inet6 addr: fe80::7777:2222:1111:eeee/64 Scope:Global
4 inet6 addr: fe80::8107:2b92:867e:f8a6/64 Scope:Link
5 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
6 RX packets:8106427 errors:55 dropped:0 overruns:0 frame:37
7 TX packets:9339739 errors:0 dropped:0 overruns:0 carrier:0
8 collisions:0 txqueuelen:1000
9 RX bytes:4953721719 (4.9 GB) TX bytes:7731890194 (7.7 GB)
10 Interrupt:20 Memory:e1200000-e1220000
11
12lo Link encap:Local Loopback
13 inet addr:127.0.0.1 Mask:255.0.0.0
14 inet6 addr: ::1/128 Scope:Host
15 UP LOOPBACK RUNNING MTU:65536 Metric:1
16 RX packets:579230851 errors:0 dropped:0 overruns:0 frame:0
17 TX packets:579230851 errors:0 dropped:0 overruns:0 carrier:0
18 collisions:0 txqueuelen:1
diff --git a/tests/data/netinfo/route-formatted-output b/tests/data/netinfo/route-formatted-output
0new file mode 10064419new file mode 100644
index 0000000..9d2c5dd
--- /dev/null
+++ b/tests/data/netinfo/route-formatted-output
@@ -0,0 +1,22 @@
1+++++++++++++++++++++++++++++Route IPv4 info+++++++++++++++++++++++++++++
2+-------+-------------+-------------+---------------+-----------+-------+
3| Route | Destination | Gateway | Genmask | Interface | Flags |
4+-------+-------------+-------------+---------------+-----------+-------+
5| 0 | 0.0.0.0 | 192.168.2.1 | 0.0.0.0 | enp0s25 | UG |
6| 1 | 0.0.0.0 | 192.168.2.1 | 0.0.0.0 | wlp3s0 | UG |
7| 2 | 192.168.2.0 | 0.0.0.0 | 255.255.255.0 | enp0s25 | U |
8+-------+-------------+-------------+---------------+-----------+-------+
9+++++++++++++++++++++++++++++++++++Route IPv6 info+++++++++++++++++++++++++++++++++++
10+-------+---------------------------+---------------------------+-----------+-------+
11| Route | Destination | Gateway | Interface | Flags |
12+-------+---------------------------+---------------------------+-----------+-------+
13| 0 | 2a00:abcd:82ae:cd33::657 | :: | enp0s25 | Ue |
14| 1 | 2a00:abcd:82ae:cd33::/64 | :: | enp0s25 | U |
15| 2 | 2a00:abcd:82ae:cd33::/56 | fe80::32ee:54de:cd43:b4e1 | enp0s25 | UG |
16| 3 | fd81:123f:654::657 | :: | enp0s25 | U |
17| 4 | fd81:123f:654::/64 | :: | enp0s25 | U |
18| 5 | fd81:123f:654::/48 | fe80::32ee:54de:cd43:b4e1 | enp0s25 | UG |
19| 6 | fe80::abcd:ef12:bc34:da21 | :: | enp0s25 | U |
20| 7 | fe80::/64 | :: | enp0s25 | U |
21| 8 | ::/0 | fe80::32ee:54de:cd43:b4e1 | enp0s25 | UG |
22+-------+---------------------------+---------------------------+-----------+-------+
diff --git a/tests/data/netinfo/sample-ipaddrshow-output b/tests/data/netinfo/sample-ipaddrshow-output
0new file mode 10064423new file mode 100644
index 0000000..b2fa267
--- /dev/null
+++ b/tests/data/netinfo/sample-ipaddrshow-output
@@ -0,0 +1,13 @@
11: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
2 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3 inet 127.0.0.1/8 scope host lo\ valid_lft forever preferred_lft forever
4 inet6 ::1/128 scope host \ valid_lft forever preferred_lft forever
52: enp0s25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
6 link/ether 50:7b:9d:2c:af:91 brd ff:ff:ff:ff:ff:ff
7 inet 192.168.2.18/24 brd 192.168.2.255 scope global dynamic enp0s25
8 valid_lft 84174sec preferred_lft 84174sec
9 inet6 fe80::7777:2222:1111:eeee/64 scope global
10 valid_lft forever preferred_lft forever
11 inet6 fe80::8107:2b92:867e:f8a6/64 scope link
12 valid_lft forever preferred_lft forever
13
diff --git a/tests/data/netinfo/sample-iproute-output-v4 b/tests/data/netinfo/sample-iproute-output-v4
0new file mode 10064414new file mode 100644
index 0000000..904cb03
--- /dev/null
+++ b/tests/data/netinfo/sample-iproute-output-v4
@@ -0,0 +1,3 @@
1default via 192.168.2.1 dev enp0s25 proto static metric 100
2default via 192.168.2.1 dev wlp3s0 proto static metric 150
3192.168.2.0/24 dev enp0s25 proto kernel scope link src 192.168.2.18 metric 100
diff --git a/tests/data/netinfo/sample-iproute-output-v6 b/tests/data/netinfo/sample-iproute-output-v6
0new file mode 1006444new file mode 100644
index 0000000..12bb1c1
--- /dev/null
+++ b/tests/data/netinfo/sample-iproute-output-v6
@@ -0,0 +1,11 @@
12a00:abcd:82ae:cd33::657 dev enp0s25 proto kernel metric 256 expires 2334sec pref medium
22a00:abcd:82ae:cd33::/64 dev enp0s25 proto ra metric 100 pref medium
32a00:abcd:82ae:cd33::/56 via fe80::32ee:54de:cd43:b4e1 dev enp0s25 proto ra metric 100 pref medium
4fd81:123f:654::657 dev enp0s25 proto kernel metric 256 pref medium
5fd81:123f:654::/64 dev enp0s25 proto ra metric 100 pref medium
6fd81:123f:654::/48 via fe80::32ee:54de:cd43:b4e1 dev enp0s25 proto ra metric 100 pref medium
7fe80::abcd:ef12:bc34:da21 dev enp0s25 proto static metric 100 pref medium
8fe80::/64 dev enp0s25 proto kernel metric 256 pref medium
9default via fe80::32ee:54de:cd43:b4e1 dev enp0s25 proto static metric 100 pref medium
10local ::1 dev lo table local proto none metric 0 pref medium
11local 2600:1f16:b80:ad00:90a:c915:bca6:5ff2 dev lo table local proto none metric 0 pref medium
diff --git a/tests/data/netinfo/sample-route-output-v4 b/tests/data/netinfo/sample-route-output-v4
0new file mode 10064412new file mode 100644
index 0000000..ecc31d9
--- /dev/null
+++ b/tests/data/netinfo/sample-route-output-v4
@@ -0,0 +1,5 @@
1Kernel IP routing table
2Destination Gateway Genmask Flags Metric Ref Use Iface
30.0.0.0 192.168.2.1 0.0.0.0 UG 100 0 0 enp0s25
40.0.0.0 192.168.2.1 0.0.0.0 UG 150 0 0 wlp3s0
5192.168.2.0 0.0.0.0 255.255.255.0 U 100 0 0 enp0s25
diff --git a/tests/data/netinfo/sample-route-output-v6 b/tests/data/netinfo/sample-route-output-v6
0new file mode 1006446new file mode 100644
index 0000000..4712b73
--- /dev/null
+++ b/tests/data/netinfo/sample-route-output-v6
@@ -0,0 +1,13 @@
1Kernel IPv6 routing table
2Destination Next Hop Flag Met Re Use If
32a00:abcd:82ae:cd33::657/128 :: Ue 256 1 0 enp0s25
42a00:abcd:82ae:cd33::/64 :: U 100 1 0 enp0s25
52a00:abcd:82ae:cd33::/56 fe80::32ee:54de:cd43:b4e1 UG 100 1 0 enp0s25
6fd81:123f:654::657/128 :: U 256 1 0 enp0s25
7fd81:123f:654::/64 :: U 100 1 0 enp0s25
8fd81:123f:654::/48 fe80::32ee:54de:cd43:b4e1 UG 100 1 0 enp0s25
9fe80::abcd:ef12:bc34:da21/128 :: U 100 1 2 enp0s25
10fe80::/64 :: U 256 1 16880 enp0s25
11::/0 fe80::32ee:54de:cd43:b4e1 UG 100 1 0 enp0s25
12::/0 :: !n -1 1424956 lo
13::1/128 :: Un 0 4 26289 lo
diff --git a/tests/unittests/test_datasource/test_smartos.py b/tests/unittests/test_datasource/test_smartos.py
index 88bae5f..2bea7a1 100644
--- a/tests/unittests/test_datasource/test_smartos.py
+++ b/tests/unittests/test_datasource/test_smartos.py
@@ -1,4 +1,5 @@
1# Copyright (C) 2013 Canonical Ltd.1# Copyright (C) 2013 Canonical Ltd.
2# Copyright (c) 2018, Joyent, Inc.
2#3#
3# Author: Ben Howard <ben.howard@canonical.com>4# Author: Ben Howard <ben.howard@canonical.com>
4#5#
@@ -324,6 +325,7 @@ class PsuedoJoyentClient(object):
324 if data is None:325 if data is None:
325 data = MOCK_RETURNS.copy()326 data = MOCK_RETURNS.copy()
326 self.data = data327 self.data = data
328 self._is_open = False
327 return329 return
328330
329 def get(self, key, default=None, strip=False):331 def get(self, key, default=None, strip=False):
@@ -344,6 +346,14 @@ class PsuedoJoyentClient(object):
344 def exists(self):346 def exists(self):
345 return True347 return True
346348
349 def open_transport(self):
350 assert(not self._is_open)
351 self._is_open = True
352
353 def close_transport(self):
354 assert(self._is_open)
355 self._is_open = False
356
347357
348class TestSmartOSDataSource(FilesystemMockingTestCase):358class TestSmartOSDataSource(FilesystemMockingTestCase):
349 def setUp(self):359 def setUp(self):
@@ -592,8 +602,46 @@ class TestSmartOSDataSource(FilesystemMockingTestCase):
592 mydscfg['disk_aliases']['FOO'])602 mydscfg['disk_aliases']['FOO'])
593603
594604
605class ShortReader(object):
606 """Implements a 'read' interface for bytes provided.
607 much like io.BytesIO but the 'endbyte' acts as if EOF.
608 When it is reached a short will be returned."""
609 def __init__(self, initial_bytes, endbyte=b'\0'):
610 self.data = initial_bytes
611 self.index = 0
612 self.len = len(self.data)
613 self.endbyte = endbyte
614
615 @property
616 def emptied(self):
617 return self.index >= self.len
618
619 def read(self, size=-1):
620 """Read size bytes but not past a null."""
621 if size == 0 or self.index >= self.len:
622 return b''
623
624 rsize = size
625 if size < 0 or size + self.index > self.len:
626 rsize = self.len - self.index
627
628 next_null = self.data.find(self.endbyte, self.index, rsize)
629 if next_null >= 0:
630 rsize = next_null - self.index + 1
631 i = self.index
632 self.index += rsize
633 ret = self.data[i:i + rsize]
634 if len(ret) and ret[-1:] == self.endbyte:
635 ret = ret[:-1]
636 return ret
637
638
595class TestJoyentMetadataClient(FilesystemMockingTestCase):639class TestJoyentMetadataClient(FilesystemMockingTestCase):
596640
641 invalid = b'invalid command\n'
642 failure = b'FAILURE\n'
643 v2_ok = b'V2_OK\n'
644
597 def setUp(self):645 def setUp(self):
598 super(TestJoyentMetadataClient, self).setUp()646 super(TestJoyentMetadataClient, self).setUp()
599647
@@ -636,6 +684,11 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase):
636 return DataSourceSmartOS.JoyentMetadataClient(684 return DataSourceSmartOS.JoyentMetadataClient(
637 fp=self.serial, smartos_type=DataSourceSmartOS.SMARTOS_ENV_KVM)685 fp=self.serial, smartos_type=DataSourceSmartOS.SMARTOS_ENV_KVM)
638686
687 def _get_serial_client(self):
688 self.serial.timeout = 1
689 return DataSourceSmartOS.JoyentMetadataSerialClient(None,
690 fp=self.serial)
691
639 def assertEndsWith(self, haystack, prefix):692 def assertEndsWith(self, haystack, prefix):
640 self.assertTrue(haystack.endswith(prefix),693 self.assertTrue(haystack.endswith(prefix),
641 "{0} does not end with '{1}'".format(694 "{0} does not end with '{1}'".format(
@@ -646,12 +699,14 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase):
646 "{0} does not start with '{1}'".format(699 "{0} does not start with '{1}'".format(
647 repr(haystack), prefix))700 repr(haystack), prefix))
648701
702 def assertNoMoreSideEffects(self, obj):
703 self.assertRaises(StopIteration, obj)
704
649 def test_get_metadata_writes_a_single_line(self):705 def test_get_metadata_writes_a_single_line(self):
650 client = self._get_client()706 client = self._get_client()
651 client.get('some_key')707 client.get('some_key')
652 self.assertEqual(1, self.serial.write.call_count)708 self.assertEqual(1, self.serial.write.call_count)
653 written_line = self.serial.write.call_args[0][0]709 written_line = self.serial.write.call_args[0][0]
654 print(type(written_line))
655 self.assertEndsWith(written_line.decode('ascii'),710 self.assertEndsWith(written_line.decode('ascii'),
656 b'\n'.decode('ascii'))711 b'\n'.decode('ascii'))
657 self.assertEqual(1, written_line.count(b'\n'))712 self.assertEqual(1, written_line.count(b'\n'))
@@ -737,6 +792,52 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase):
737 client._checksum = lambda _: self.response_parts['crc']792 client._checksum = lambda _: self.response_parts['crc']
738 self.assertIsNone(client.get('some_key'))793 self.assertIsNone(client.get('some_key'))
739794
795 def test_negotiate(self):
796 client = self._get_client()
797 reader = ShortReader(self.v2_ok)
798 client.fp.read.side_effect = reader.read
799 client._negotiate()
800 self.assertTrue(reader.emptied)
801
802 def test_negotiate_short_response(self):
803 client = self._get_client()
804 # chopped '\n' from v2_ok.
805 reader = ShortReader(self.v2_ok[:-1] + b'\0')
806 client.fp.read.side_effect = reader.read
807 self.assertRaises(DataSourceSmartOS.JoyentMetadataTimeoutException,
808 client._negotiate)
809 self.assertTrue(reader.emptied)
810
811 def test_negotiate_bad_response(self):
812 client = self._get_client()
813 reader = ShortReader(b'garbage\n' + self.v2_ok)
814 client.fp.read.side_effect = reader.read
815 self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,
816 client._negotiate)
817 self.assertEqual(self.v2_ok, client.fp.read())
818
819 def test_serial_open_transport(self):
820 client = self._get_serial_client()
821 reader = ShortReader(b'garbage\0' + self.invalid + self.v2_ok)
822 client.fp.read.side_effect = reader.read
823 client.open_transport()
824 self.assertTrue(reader.emptied)
825
826 def test_flush_failure(self):
827 client = self._get_serial_client()
828 reader = ShortReader(b'garbage' + b'\0' + self.failure +
829 self.invalid + self.v2_ok)
830 client.fp.read.side_effect = reader.read
831 client.open_transport()
832 self.assertTrue(reader.emptied)
833
834 def test_flush_many_timeouts(self):
835 client = self._get_serial_client()
836 reader = ShortReader(b'\0' * 100 + self.invalid + self.v2_ok)
837 client.fp.read.side_effect = reader.read
838 client.open_transport()
839 self.assertTrue(reader.emptied)
840
740841
741class TestNetworkConversion(TestCase):842class TestNetworkConversion(TestCase):
742 def test_convert_simple(self):843 def test_convert_simple(self):
diff --git a/tests/unittests/test_filters/test_launch_index.py b/tests/unittests/test_filters/test_launch_index.py
index 6364d38..e1a5d2c 100644
--- a/tests/unittests/test_filters/test_launch_index.py
+++ b/tests/unittests/test_filters/test_launch_index.py
@@ -55,7 +55,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
55 return True55 return True
5656
57 def testMultiEmailIndex(self):57 def testMultiEmailIndex(self):
58 test_data = self.readResource('filter_cloud_multipart_2.email')58 test_data = helpers.readResource('filter_cloud_multipart_2.email')
59 ud_proc = ud.UserDataProcessor(self.getCloudPaths())59 ud_proc = ud.UserDataProcessor(self.getCloudPaths())
60 message = ud_proc.process(test_data)60 message = ud_proc.process(test_data)
61 self.assertTrue(count_messages(message) > 0)61 self.assertTrue(count_messages(message) > 0)
@@ -70,7 +70,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
70 self.assertCounts(message, expected_counts)70 self.assertCounts(message, expected_counts)
7171
72 def testHeaderEmailIndex(self):72 def testHeaderEmailIndex(self):
73 test_data = self.readResource('filter_cloud_multipart_header.email')73 test_data = helpers.readResource('filter_cloud_multipart_header.email')
74 ud_proc = ud.UserDataProcessor(self.getCloudPaths())74 ud_proc = ud.UserDataProcessor(self.getCloudPaths())
75 message = ud_proc.process(test_data)75 message = ud_proc.process(test_data)
76 self.assertTrue(count_messages(message) > 0)76 self.assertTrue(count_messages(message) > 0)
@@ -85,7 +85,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
85 self.assertCounts(message, expected_counts)85 self.assertCounts(message, expected_counts)
8686
87 def testConfigEmailIndex(self):87 def testConfigEmailIndex(self):
88 test_data = self.readResource('filter_cloud_multipart_1.email')88 test_data = helpers.readResource('filter_cloud_multipart_1.email')
89 ud_proc = ud.UserDataProcessor(self.getCloudPaths())89 ud_proc = ud.UserDataProcessor(self.getCloudPaths())
90 message = ud_proc.process(test_data)90 message = ud_proc.process(test_data)
91 self.assertTrue(count_messages(message) > 0)91 self.assertTrue(count_messages(message) > 0)
@@ -99,7 +99,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
99 self.assertCounts(message, expected_counts)99 self.assertCounts(message, expected_counts)
100100
101 def testNoneIndex(self):101 def testNoneIndex(self):
102 test_data = self.readResource('filter_cloud_multipart.yaml')102 test_data = helpers.readResource('filter_cloud_multipart.yaml')
103 ud_proc = ud.UserDataProcessor(self.getCloudPaths())103 ud_proc = ud.UserDataProcessor(self.getCloudPaths())
104 message = ud_proc.process(test_data)104 message = ud_proc.process(test_data)
105 start_count = count_messages(message)105 start_count = count_messages(message)
@@ -108,7 +108,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
108 self.assertTrue(self.equivalentMessage(message, filtered_message))108 self.assertTrue(self.equivalentMessage(message, filtered_message))
109109
110 def testIndexes(self):110 def testIndexes(self):
111 test_data = self.readResource('filter_cloud_multipart.yaml')111 test_data = helpers.readResource('filter_cloud_multipart.yaml')
112 ud_proc = ud.UserDataProcessor(self.getCloudPaths())112 ud_proc = ud.UserDataProcessor(self.getCloudPaths())
113 message = ud_proc.process(test_data)113 message = ud_proc.process(test_data)
114 start_count = count_messages(message)114 start_count = count_messages(message)
diff --git a/tests/unittests/test_merging.py b/tests/unittests/test_merging.py
index f51358d..3a5072c 100644
--- a/tests/unittests/test_merging.py
+++ b/tests/unittests/test_merging.py
@@ -100,7 +100,7 @@ def make_dict(max_depth, seed=None):
100100
101class TestSimpleRun(helpers.ResourceUsingTestCase):101class TestSimpleRun(helpers.ResourceUsingTestCase):
102 def _load_merge_files(self):102 def _load_merge_files(self):
103 merge_root = self.resourceLocation('merge_sources')103 merge_root = helpers.resourceLocation('merge_sources')
104 tests = []104 tests = []
105 source_ids = collections.defaultdict(list)105 source_ids = collections.defaultdict(list)
106 expected_files = {}106 expected_files = {}
diff --git a/tests/unittests/test_runs/test_merge_run.py b/tests/unittests/test_runs/test_merge_run.py
index 5d3f1ca..d1ac494 100644
--- a/tests/unittests/test_runs/test_merge_run.py
+++ b/tests/unittests/test_runs/test_merge_run.py
@@ -25,7 +25,7 @@ class TestMergeRun(helpers.FilesystemMockingTestCase):
25 'cloud_init_modules': ['write-files'],25 'cloud_init_modules': ['write-files'],
26 'system_info': {'paths': {'run_dir': new_root}}26 'system_info': {'paths': {'run_dir': new_root}}
27 }27 }
28 ud = self.readResource('user_data.1.txt')28 ud = helpers.readResource('user_data.1.txt')
29 cloud_cfg = util.yaml_dumps(cfg)29 cloud_cfg = util.yaml_dumps(cfg)
30 util.ensure_dir(os.path.join(new_root, 'etc', 'cloud'))30 util.ensure_dir(os.path.join(new_root, 'etc', 'cloud'))
31 util.write_file(os.path.join(new_root, 'etc',31 util.write_file(os.path.join(new_root, 'etc',
diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
index 5010190..e04ea03 100644
--- a/tests/unittests/test_util.py
+++ b/tests/unittests/test_util.py
@@ -325,7 +325,7 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
325325
326 def test_precise_ext4_root(self):326 def test_precise_ext4_root(self):
327327
328 lines = self.readResource('mountinfo_precise_ext4.txt').splitlines()328 lines = helpers.readResource('mountinfo_precise_ext4.txt').splitlines()
329329
330 expected = ('/dev/mapper/vg0-root', 'ext4', '/')330 expected = ('/dev/mapper/vg0-root', 'ext4', '/')
331 self.assertEqual(expected, util.parse_mount_info('/', lines))331 self.assertEqual(expected, util.parse_mount_info('/', lines))
@@ -347,7 +347,7 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
347 self.assertEqual(expected, util.parse_mount_info('/run/lock', lines))347 self.assertEqual(expected, util.parse_mount_info('/run/lock', lines))
348348
349 def test_raring_btrfs_root(self):349 def test_raring_btrfs_root(self):
350 lines = self.readResource('mountinfo_raring_btrfs.txt').splitlines()350 lines = helpers.readResource('mountinfo_raring_btrfs.txt').splitlines()
351351
352 expected = ('/dev/vda1', 'btrfs', '/')352 expected = ('/dev/vda1', 'btrfs', '/')
353 self.assertEqual(expected, util.parse_mount_info('/', lines))353 self.assertEqual(expected, util.parse_mount_info('/', lines))
@@ -373,7 +373,7 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
373 m_os.path.exists.return_value = True373 m_os.path.exists.return_value = True
374 # mock subp command from util.get_mount_info_fs_on_zpool374 # mock subp command from util.get_mount_info_fs_on_zpool
375 zpool_output.return_value = (375 zpool_output.return_value = (
376 self.readResource('zpool_status_simple.txt'), ''376 helpers.readResource('zpool_status_simple.txt'), ''
377 )377 )
378 # save function return values and do asserts378 # save function return values and do asserts
379 ret = util.get_device_info_from_zpool('vmzroot')379 ret = util.get_device_info_from_zpool('vmzroot')
@@ -406,7 +406,7 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
406 m_os.path.exists.return_value = True406 m_os.path.exists.return_value = True
407 # mock subp command from util.get_mount_info_fs_on_zpool407 # mock subp command from util.get_mount_info_fs_on_zpool
408 zpool_output.return_value = (408 zpool_output.return_value = (
409 self.readResource('zpool_status_simple.txt'), 'error'409 helpers.readResource('zpool_status_simple.txt'), 'error'
410 )410 )
411 # save function return values and do asserts411 # save function return values and do asserts
412 ret = util.get_device_info_from_zpool('vmzroot')412 ret = util.get_device_info_from_zpool('vmzroot')
@@ -414,7 +414,8 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
414414
415 @mock.patch('cloudinit.util.subp')415 @mock.patch('cloudinit.util.subp')
416 def test_parse_mount_with_ext(self, mount_out):416 def test_parse_mount_with_ext(self, mount_out):
417 mount_out.return_value = (self.readResource('mount_parse_ext.txt'), '')417 mount_out.return_value = (
418 helpers.readResource('mount_parse_ext.txt'), '')
418 # this one is valid and exists in mount_parse_ext.txt419 # this one is valid and exists in mount_parse_ext.txt
419 ret = util.parse_mount('/var')420 ret = util.parse_mount('/var')
420 self.assertEqual(('/dev/mapper/vg00-lv_var', 'ext4', '/var'), ret)421 self.assertEqual(('/dev/mapper/vg00-lv_var', 'ext4', '/var'), ret)
@@ -430,7 +431,8 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
430431
431 @mock.patch('cloudinit.util.subp')432 @mock.patch('cloudinit.util.subp')
432 def test_parse_mount_with_zfs(self, mount_out):433 def test_parse_mount_with_zfs(self, mount_out):
433 mount_out.return_value = (self.readResource('mount_parse_zfs.txt'), '')434 mount_out.return_value = (
435 helpers.readResource('mount_parse_zfs.txt'), '')
434 # this one is valid and exists in mount_parse_zfs.txt436 # this one is valid and exists in mount_parse_zfs.txt
435 ret = util.parse_mount('/var')437 ret = util.parse_mount('/var')
436 self.assertEqual(('vmzroot/ROOT/freebsd/var', 'zfs', '/var'), ret)438 self.assertEqual(('vmzroot/ROOT/freebsd/var', 'zfs', '/var'), ret)
@@ -800,7 +802,7 @@ class TestSubp(helpers.CiTestCase):
800802
801 os.chmod(noshebang, os.stat(noshebang).st_mode | stat.S_IEXEC)803 os.chmod(noshebang, os.stat(noshebang).st_mode | stat.S_IEXEC)
802 self.assertRaisesRegex(util.ProcessExecutionError,804 self.assertRaisesRegex(util.ProcessExecutionError,
803 'Missing #! in script\?',805 r'Missing #! in script\?',
804 util.subp, (noshebang,))806 util.subp, (noshebang,))
805807
806 def test_returns_none_if_no_capture(self):808 def test_returns_none_if_no_capture(self):

Subscribers

People subscribed via source and target branches