Merge ~chad.smith/cloud-init:unittests-in-cloudinit-package into cloud-init:master

Proposed by Chad Smith
Status: Merged
Approved by: Scott Moser
Approved revision: 49a4b8327125b4e5105c8508c08cc333480ff7e1
Merged at revision: e586fe35a692b7519000005c8024ebd2bcbc82e0
Proposed branch: ~chad.smith/cloud-init:unittests-in-cloudinit-package
Merge into: cloud-init:master
Diff against target: 772 lines (+648/-21)
5 files modified
cloudinit/net/__init__.py (+114/-17)
cloudinit/net/tests/__init__.py (+0/-0)
cloudinit/net/tests/test_init.py (+522/-0)
setup.py (+1/-1)
tox.ini (+11/-3)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Scott Moser Approve
Review via email: mp+327827@code.launchpad.net

Commit message

cloudinit.net: add initialize_network_device function and tests

This is not yet called, but will be called in a subsequent Ec2-related branch to manually initialize a network interface with the responses using dhcp discovery without any dhcp-script side-effects. The functionality has been tested on Ec2 ubuntu and CentOS vms to ensure that network interface initialization works in both OS-types.

Since there was poor unit test coverage for the cloudinit.net.__init__ module, this branch adds a bunch of coverage to the functions in cloudinit.net.__init. We can also now have unit tests local to the cloudinit modules. The benefits of having unittests under cloudinit module:
 - Proximity of unittest to cloudinit module makes it easier for ongoing devs to know where to augment unit tests. The tests.unittest directory is organizated such that it
 - Allows for 1 to 1 name mapping module -> tests/test_module.py
 - Improved test and module isolation, if we find unit tests have to import from a number of modules besides the module under test, it will better prompt resturcturing of the module.

This also branch touches:
 - tox.ini to run unit tests found in cloudinit as well as include all test-requirements for pylint since we now have unit tests living within cloudinit package
 - setup.py to exclude any test modules under cloudinit when packaging

Description of the change

cloudinit.net: add initialize_network_device function and unittest.

This is not yet called, but will be called in a subsequent Ec2-related branch to manually initialize a network interface with the responses using dhcp discovery without any dhcp-script side-effects. The functionality has been tested on Ec2 ubuntu and CentOS vms to ensure that network interface initialization works in both OS-types.

Since there was poor unit test coverage for the cloudinit.net.__init__ module, this branch adds a bunch of coverage to the functions in cloudinit.net.__init. We can also now have unit tests local to the cloudinit modules. The benefits of having unittests under cloudinit module:
 - Proximity of unittest to cloudinit module makes it easier for ongoing devs to know where to augment unit tests. The tests.unittest directory is organizated such that it
 - Allows for 1 to 1 name mapping module -> tests/test_module.py
 - Improved test and module isolation, if we find unit tests have to import from a number of modules besides the module under test, it will better prompt resturcturing of the module.

This also branch touches:
 - tox.ini to run unit tests found in cloudinit as well as include mock test-requirement for pylint since we now have unit tests living within cloudinit package
 - setup.py to exclude any test modules under cloudinit when packaging

To test:
   make deb
   dpkg -c cloud-init_all.deb | grep test # make sure our package didn't include tests
   tox
   # on an lxc: ifconfig eth0 0.0.0.0; python -c 'from cloudinit.net import initialize_network_device; initialize_network_device("eth0", "ip-addr", "netmask", "broadcast")'

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

PASSED: Continuous integration, rev:a3692fcda19267d6d63b0317cdfb40d6df8b08ed
https://jenkins.ubuntu.com/server/job/cloud-init-ci/64/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

why did you have to add mock to tox's pylint entries ?
also some inline.
this does look really nice though.
thank you.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:ed950d276972c869bbeb87dead0cc4ba55e2fc88
https://jenkins.ubuntu.com/server/job/cloud-init-ci/83/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:4b4989d894331e07041c2656ec7f02e6ed68b140
https://jenkins.ubuntu.com/server/job/cloud-init-ci/84/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:a3692fcda19267d6d63b0317cdfb40d6df8b08ed
https://jenkins.ubuntu.com/server/job/cloud-init-ci/85/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)
0f476ef... by Chad Smith

For realsies: it makes more sense that EphemeralIPv4Network is a context manager than DhcpCleanDiscovery handling all the setup an teardown of ips. Also allow EphemeralIPv4Network to accept prefix or netmask

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:0f476ef54a97487dc913027f6be3a071aaeb9575
https://jenkins.ubuntu.com/server/job/cloud-init-ci/86/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

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

review: Needs Fixing (continuous-integration)
4811785... by Chad Smith

mock get_sys_class_path function in unit tests to allow for cleanup

a0bd7a7... by Chad Smith

lints

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:a0bd7a79f0869fc3bb09272cb030f84f1bb335f1
https://jenkins.ubuntu.com/server/job/cloud-init-ci/87/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

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

review: Needs Fixing (continuous-integration)
e37c259... by Chad Smith

check is_up instead of is_connected before configuring a device

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:e37c259841bc6d902e9fec9c4a5dc1db3ebac70f
https://jenkins.ubuntu.com/server/job/cloud-init-ci/89/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

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

review: Needs Fixing (continuous-integration)
93ace28... by Chad Smith

fixup leaky unittest mocks

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:93ace2828ef808bf32208e325de125ab40e4cefd
https://jenkins.ubuntu.com/server/job/cloud-init-ci/91/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    FAILED: CentOS 6 & 7: Build & Test

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

small questions in line.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:93ace2828ef808bf32208e325de125ab40e4cefd
https://jenkins.ubuntu.com/server/job/cloud-init-ci/92/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)
b6369bd... by Chad Smith

add get_ip_info helper function to allow inspecting ip addrs before setup or teardown in EphemeralIPv4Network context manager.

787586e... by Chad Smith

add exception handling if ip command or specified interface does not exist. Add unit tests for setup and teardown ip commands

cdba67c... by Chad Smith

add cleanup to remove ip addr from device during exit of EphemeralIPv4Network context manager

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:787586e45a5860c9be2ac13847b61855e29f7623
https://jenkins.ubuntu.com/server/job/cloud-init-ci/100/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    FAILED: Ubuntu LTS: Build

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:cdba67c453c1da7b80d8d07274bc2d1c4995cb43
https://jenkins.ubuntu.com/server/job/cloud-init-ci/101/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
c25b725... by Chad Smith

drop get_ip_info utility function and handle ProcessExecutionError mentioning File Exists when addr is already set

9cdbb64... by Chad Smith

lints/flakes

8ef6575... by Chad Smith

include all unit test-requirements for tox lint targets

Revision history for this message
Chad Smith (chad.smith) wrote :

Per our discussion today, I simplified logic in EphemeralIPv4Network context manager to first attempt to add a network address and handle a non-zero exit code if the address already exits.

All changes have been made per review comments, I separated out get_ip_info utility function so we can propose it as a separate branch to replace ifconfig parsing.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:8ef6575c63eb4aaf08f1b0091d96cf1b8c9819e6
https://jenkins.ubuntu.com/server/job/cloud-init-ci/102/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)
3d85180... by Chad Smith

route del needs to happen before ip addr del

Revision history for this message
Chad Smith (chad.smith) wrote :

Just tested on ubuntu AWS. minor fix needed to remove the route before removing the address, because the side-effect of removing the addr is the route is auto-deleted.

Revision history for this message
Chad Smith (chad.smith) wrote :

Just validated on cent6 too:
2017-07-27 21:33:19,762 - handlers.py[DEBUG]: finish: init-local/search-Ec2Local: SUCCESS: found local data from DataSourceEc2Local

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:3d851806873f3f050cbe29206ab32c5fe7fa5cc9
https://jenkins.ubuntu.com/server/job/cloud-init-ci/103/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

tiny things.
fix that comment, and i approve.
thank you Chad.

review: Approve
49a4b83... by Chad Smith

docstring fixup

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:49a4b8327125b4e5105c8508c08cc333480ff7e1
https://jenkins.ubuntu.com/server/job/cloud-init-ci/104/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)

Update scan failed

At least one of the branches involved have failed to scan. You can manually schedule a rescan if required.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
2index d1740e5..46cb9c8 100644
3--- a/cloudinit/net/__init__.py
4+++ b/cloudinit/net/__init__.py
5@@ -10,6 +10,7 @@ import logging
6 import os
7 import re
8
9+from cloudinit.net.network_state import mask_to_net_prefix
10 from cloudinit import util
11
12 LOG = logging.getLogger(__name__)
13@@ -28,8 +29,13 @@ def _natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
14 for text in re.split(_nsre, s)]
15
16
17+def get_sys_class_path():
18+ """Simple function to return the global SYS_CLASS_NET."""
19+ return SYS_CLASS_NET
20+
21+
22 def sys_dev_path(devname, path=""):
23- return SYS_CLASS_NET + devname + "/" + path
24+ return get_sys_class_path() + devname + "/" + path
25
26
27 def read_sys_net(devname, path, translate=None,
28@@ -77,7 +83,7 @@ def read_sys_net_int(iface, field):
29 return None
30 try:
31 return int(val)
32- except TypeError:
33+ except ValueError:
34 return None
35
36
37@@ -149,7 +155,14 @@ def device_devid(devname):
38
39
40 def get_devicelist():
41- return os.listdir(SYS_CLASS_NET)
42+ try:
43+ devs = os.listdir(get_sys_class_path())
44+ except OSError as e:
45+ if e.errno == errno.ENOENT:
46+ devs = []
47+ else:
48+ raise
49+ return devs
50
51
52 class ParserError(Exception):
53@@ -497,14 +510,8 @@ def get_interfaces_by_mac():
54 """Build a dictionary of tuples {mac: name}.
55
56 Bridges and any devices that have a 'stolen' mac are excluded."""
57- try:
58- devs = get_devicelist()
59- except OSError as e:
60- if e.errno == errno.ENOENT:
61- devs = []
62- else:
63- raise
64 ret = {}
65+ devs = get_devicelist()
66 empty_mac = '00:00:00:00:00:00'
67 for name in devs:
68 if not interface_has_own_mac(name):
69@@ -531,14 +538,8 @@ def get_interfaces():
70 """Return list of interface tuples (name, mac, driver, device_id)
71
72 Bridges and any devices that have a 'stolen' mac are excluded."""
73- try:
74- devs = get_devicelist()
75- except OSError as e:
76- if e.errno == errno.ENOENT:
77- devs = []
78- else:
79- raise
80 ret = []
81+ devs = get_devicelist()
82 empty_mac = '00:00:00:00:00:00'
83 for name in devs:
84 if not interface_has_own_mac(name):
85@@ -557,6 +558,102 @@ def get_interfaces():
86 return ret
87
88
89+class EphemeralIPv4Network(object):
90+ """Context manager which sets up temporary static network configuration.
91+
92+ No operations are performed if the provided interface is already connected.
93+ If unconnected, bring up the interface with valid ip, prefix and broadcast.
94+ If router is provided setup a default route for that interface. Upon
95+ context exit, clean up the interface leaving no configuration behind.
96+ """
97+
98+ def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None):
99+ """Setup context manager and validate call signature.
100+
101+ @param interface: Name of the network interface to bring up.
102+ @param ip: IP address to assign to the interface.
103+ @param prefix_or_mask: Either netmask of the format X.X.X.X or an int
104+ prefix.
105+ @param broadcast: Broadcast address for the IPv4 network.
106+ @param router: Optionally the default gateway IP.
107+ """
108+ if not all([interface, ip, prefix_or_mask, broadcast]):
109+ raise ValueError(
110+ 'Cannot init network on {0} with {1}/{2} and bcast {3}'.format(
111+ interface, ip, prefix_or_mask, broadcast))
112+ try:
113+ self.prefix = mask_to_net_prefix(prefix_or_mask)
114+ except ValueError as e:
115+ raise ValueError(
116+ 'Cannot setup network: {0}'.format(e))
117+ self.interface = interface
118+ self.ip = ip
119+ self.broadcast = broadcast
120+ self.router = router
121+ self.cleanup_cmds = [] # List of commands to run to cleanup state.
122+
123+ def __enter__(self):
124+ """Perform ephemeral network setup if interface is not connected."""
125+ self._bringup_device()
126+ if self.router:
127+ self._bringup_router()
128+
129+ def __exit__(self, excp_type, excp_value, excp_traceback):
130+ for cmd in self.cleanup_cmds:
131+ util.subp(cmd, capture=True)
132+
133+ def _delete_address(self, address, prefix):
134+ """Perform the ip command to remove the specified address."""
135+ util.subp(
136+ ['ip', '-family', 'inet', 'addr', 'del',
137+ '%s/%s' % (address, prefix), 'dev', self.interface],
138+ capture=True)
139+
140+ def _bringup_device(self):
141+ """Perform the ip comands to fully setup the device."""
142+ cidr = '{0}/{1}'.format(self.ip, self.prefix)
143+ LOG.debug(
144+ 'Attempting setup of ephemeral network on %s with %s brd %s',
145+ self.interface, cidr, self.broadcast)
146+ try:
147+ util.subp(
148+ ['ip', '-family', 'inet', 'addr', 'add', cidr, 'broadcast',
149+ self.broadcast, 'dev', self.interface],
150+ capture=True, update_env={'LANG': 'C'})
151+ except util.ProcessExecutionError as e:
152+ if "File exists" not in e.stderr:
153+ raise
154+ LOG.debug(
155+ 'Skip ephemeral network setup, %s already has address %s',
156+ self.interface, self.ip)
157+ else:
158+ # Address creation success, bring up device and queue cleanup
159+ util.subp(
160+ ['ip', '-family', 'inet', 'link', 'set', 'dev', self.interface,
161+ 'up'], capture=True)
162+ self.cleanup_cmds.append(
163+ ['ip', '-family', 'inet', 'link', 'set', 'dev', self.interface,
164+ 'down'])
165+ self.cleanup_cmds.append(
166+ ['ip', '-family', 'inet', 'addr', 'del', cidr, 'dev',
167+ self.interface])
168+
169+ def _bringup_router(self):
170+ """Perform the ip commands to fully setup the router if needed."""
171+ # Check if a default route exists and exit if it does
172+ out, _ = util.subp(['ip', 'route', 'show', '0.0.0.0/0'], capture=True)
173+ if 'default' in out:
174+ LOG.debug(
175+ 'Skip ephemeral route setup. %s already has default route: %s',
176+ self.interface, out.strip())
177+ return
178+ util.subp(
179+ ['ip', '-4', 'route', 'add', 'default', 'via', self.router,
180+ 'dev', self.interface], capture=True)
181+ self.cleanup_cmds.insert(
182+ 0, ['ip', '-4', 'route', 'del', 'default', 'dev', self.interface])
183+
184+
185 class RendererNotFoundError(RuntimeError):
186 pass
187
188diff --git a/cloudinit/net/tests/__init__.py b/cloudinit/net/tests/__init__.py
189new file mode 100644
190index 0000000..e69de29
191--- /dev/null
192+++ b/cloudinit/net/tests/__init__.py
193diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
194new file mode 100644
195index 0000000..272a6eb
196--- /dev/null
197+++ b/cloudinit/net/tests/test_init.py
198@@ -0,0 +1,522 @@
199+# This file is part of cloud-init. See LICENSE file for license information.
200+
201+import copy
202+import errno
203+import mock
204+import os
205+
206+import cloudinit.net as net
207+from cloudinit.util import ensure_file, write_file, ProcessExecutionError
208+from tests.unittests.helpers import CiTestCase
209+
210+
211+class TestSysDevPath(CiTestCase):
212+
213+ def test_sys_dev_path(self):
214+ """sys_dev_path returns a path under SYS_CLASS_NET for a device."""
215+ dev = 'something'
216+ path = 'attribute'
217+ expected = net.SYS_CLASS_NET + dev + '/' + path
218+ self.assertEqual(expected, net.sys_dev_path(dev, path))
219+
220+ def test_sys_dev_path_without_path(self):
221+ """When path param isn't provided it defaults to empty string."""
222+ dev = 'something'
223+ expected = net.SYS_CLASS_NET + dev + '/'
224+ self.assertEqual(expected, net.sys_dev_path(dev))
225+
226+
227+class TestReadSysNet(CiTestCase):
228+ with_logs = True
229+
230+ def setUp(self):
231+ super(TestReadSysNet, self).setUp()
232+ sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
233+ self.m_sys_path = sys_mock.start()
234+ self.sysdir = self.tmp_dir() + '/'
235+ self.m_sys_path.return_value = self.sysdir
236+ self.addCleanup(sys_mock.stop)
237+
238+ def test_read_sys_net_strips_contents_of_sys_path(self):
239+ """read_sys_net strips whitespace from the contents of a sys file."""
240+ content = 'some stuff with trailing whitespace\t\r\n'
241+ write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
242+ self.assertEqual(content.strip(), net.read_sys_net('dev', 'attr'))
243+
244+ def test_read_sys_net_reraises_oserror(self):
245+ """read_sys_net raises OSError/IOError when file doesn't exist."""
246+ # Non-specific Exception because versions of python OSError vs IOError.
247+ with self.assertRaises(Exception) as context_manager: # noqa: H202
248+ net.read_sys_net('dev', 'attr')
249+ error = context_manager.exception
250+ self.assertIn('No such file or directory', str(error))
251+
252+ def test_read_sys_net_handles_error_with_on_enoent(self):
253+ """read_sys_net handles OSError/IOError with on_enoent if provided."""
254+ handled_errors = []
255+
256+ def on_enoent(e):
257+ handled_errors.append(e)
258+
259+ net.read_sys_net('dev', 'attr', on_enoent=on_enoent)
260+ error = handled_errors[0]
261+ self.assertIsInstance(error, Exception)
262+ self.assertIn('No such file or directory', str(error))
263+
264+ def test_read_sys_net_translates_content(self):
265+ """read_sys_net translates content when translate dict is provided."""
266+ content = "you're welcome\n"
267+ write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
268+ translate = {"you're welcome": 'de nada'}
269+ self.assertEqual(
270+ 'de nada',
271+ net.read_sys_net('dev', 'attr', translate=translate))
272+
273+ def test_read_sys_net_errors_on_translation_failures(self):
274+ """read_sys_net raises a KeyError and logs details on failure."""
275+ content = "you're welcome\n"
276+ write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
277+ with self.assertRaises(KeyError) as context_manager:
278+ net.read_sys_net('dev', 'attr', translate={})
279+ error = context_manager.exception
280+ self.assertEqual('"you\'re welcome"', str(error))
281+ self.assertIn(
282+ "Found unexpected (not translatable) value 'you're welcome' in "
283+ "'{0}dev/attr".format(self.sysdir),
284+ self.logs.getvalue())
285+
286+ def test_read_sys_net_handles_handles_with_onkeyerror(self):
287+ """read_sys_net handles translation errors calling on_keyerror."""
288+ content = "you're welcome\n"
289+ write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
290+ handled_errors = []
291+
292+ def on_keyerror(e):
293+ handled_errors.append(e)
294+
295+ net.read_sys_net('dev', 'attr', translate={}, on_keyerror=on_keyerror)
296+ error = handled_errors[0]
297+ self.assertIsInstance(error, KeyError)
298+ self.assertEqual('"you\'re welcome"', str(error))
299+
300+ def test_read_sys_net_safe_false_on_translate_failure(self):
301+ """read_sys_net_safe returns False on translation failures."""
302+ content = "you're welcome\n"
303+ write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
304+ self.assertFalse(net.read_sys_net_safe('dev', 'attr', translate={}))
305+
306+ def test_read_sys_net_safe_returns_false_on_noent_failure(self):
307+ """read_sys_net_safe returns False on file not found failures."""
308+ self.assertFalse(net.read_sys_net_safe('dev', 'attr'))
309+
310+ def test_read_sys_net_int_returns_none_on_error(self):
311+ """read_sys_net_safe returns None on failures."""
312+ self.assertFalse(net.read_sys_net_int('dev', 'attr'))
313+
314+ def test_read_sys_net_int_returns_none_on_valueerror(self):
315+ """read_sys_net_safe returns None when content is not an int."""
316+ write_file(os.path.join(self.sysdir, 'dev', 'attr'), 'NOTINT\n')
317+ self.assertFalse(net.read_sys_net_int('dev', 'attr'))
318+
319+ def test_read_sys_net_int_returns_integer_from_content(self):
320+ """read_sys_net_safe returns None on failures."""
321+ write_file(os.path.join(self.sysdir, 'dev', 'attr'), '1\n')
322+ self.assertEqual(1, net.read_sys_net_int('dev', 'attr'))
323+
324+ def test_is_up_true(self):
325+ """is_up is True if sys/net/devname/operstate is 'up' or 'unknown'."""
326+ for state in ['up', 'unknown']:
327+ write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
328+ self.assertTrue(net.is_up('eth0'))
329+
330+ def test_is_up_false(self):
331+ """is_up is False if sys/net/devname/operstate is 'down' or invalid."""
332+ for state in ['down', 'incomprehensible']:
333+ write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
334+ self.assertFalse(net.is_up('eth0'))
335+
336+ def test_is_wireless(self):
337+ """is_wireless is True when /sys/net/devname/wireless exists."""
338+ self.assertFalse(net.is_wireless('eth0'))
339+ ensure_file(os.path.join(self.sysdir, 'eth0', 'wireless'))
340+ self.assertTrue(net.is_wireless('eth0'))
341+
342+ def test_is_bridge(self):
343+ """is_bridge is True when /sys/net/devname/bridge exists."""
344+ self.assertFalse(net.is_bridge('eth0'))
345+ ensure_file(os.path.join(self.sysdir, 'eth0', 'bridge'))
346+ self.assertTrue(net.is_bridge('eth0'))
347+
348+ def test_is_bond(self):
349+ """is_bond is True when /sys/net/devname/bonding exists."""
350+ self.assertFalse(net.is_bond('eth0'))
351+ ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
352+ self.assertTrue(net.is_bond('eth0'))
353+
354+ def test_is_vlan(self):
355+ """is_vlan is True when /sys/net/devname/uevent has DEVTYPE=vlan."""
356+ ensure_file(os.path.join(self.sysdir, 'eth0', 'uevent'))
357+ self.assertFalse(net.is_vlan('eth0'))
358+ content = 'junk\nDEVTYPE=vlan\njunk\n'
359+ write_file(os.path.join(self.sysdir, 'eth0', 'uevent'), content)
360+ self.assertTrue(net.is_vlan('eth0'))
361+
362+ def test_is_connected_when_physically_connected(self):
363+ """is_connected is True when /sys/net/devname/iflink reports 2."""
364+ self.assertFalse(net.is_connected('eth0'))
365+ write_file(os.path.join(self.sysdir, 'eth0', 'iflink'), "2")
366+ self.assertTrue(net.is_connected('eth0'))
367+
368+ def test_is_connected_when_wireless_and_carrier_active(self):
369+ """is_connected is True if wireless /sys/net/devname/carrier is 1."""
370+ self.assertFalse(net.is_connected('eth0'))
371+ ensure_file(os.path.join(self.sysdir, 'eth0', 'wireless'))
372+ self.assertFalse(net.is_connected('eth0'))
373+ write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), "1")
374+ self.assertTrue(net.is_connected('eth0'))
375+
376+ def test_is_physical(self):
377+ """is_physical is True when /sys/net/devname/device exists."""
378+ self.assertFalse(net.is_physical('eth0'))
379+ ensure_file(os.path.join(self.sysdir, 'eth0', 'device'))
380+ self.assertTrue(net.is_physical('eth0'))
381+
382+ def test_is_present(self):
383+ """is_present is True when /sys/net/devname exists."""
384+ self.assertFalse(net.is_present('eth0'))
385+ ensure_file(os.path.join(self.sysdir, 'eth0', 'device'))
386+ self.assertTrue(net.is_present('eth0'))
387+
388+
389+class TestGenerateFallbackConfig(CiTestCase):
390+
391+ def setUp(self):
392+ super(TestGenerateFallbackConfig, self).setUp()
393+ sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
394+ self.m_sys_path = sys_mock.start()
395+ self.sysdir = self.tmp_dir() + '/'
396+ self.m_sys_path.return_value = self.sysdir
397+ self.addCleanup(sys_mock.stop)
398+
399+ def test_generate_fallback_finds_connected_eth_with_mac(self):
400+ """generate_fallback_config finds any connected device with a mac."""
401+ write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
402+ write_file(os.path.join(self.sysdir, 'eth1', 'carrier'), '1')
403+ mac = 'aa:bb:cc:aa:bb:cc'
404+ write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
405+ expected = {
406+ 'config': [{'type': 'physical', 'mac_address': mac,
407+ 'name': 'eth1', 'subnets': [{'type': 'dhcp'}]}],
408+ 'version': 1}
409+ self.assertEqual(expected, net.generate_fallback_config())
410+
411+ def test_generate_fallback_finds_dormant_eth_with_mac(self):
412+ """generate_fallback_config finds any dormant device with a mac."""
413+ write_file(os.path.join(self.sysdir, 'eth0', 'dormant'), '1')
414+ mac = 'aa:bb:cc:aa:bb:cc'
415+ write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
416+ expected = {
417+ 'config': [{'type': 'physical', 'mac_address': mac,
418+ 'name': 'eth0', 'subnets': [{'type': 'dhcp'}]}],
419+ 'version': 1}
420+ self.assertEqual(expected, net.generate_fallback_config())
421+
422+ def test_generate_fallback_finds_eth_by_operstate(self):
423+ """generate_fallback_config finds any dormant device with a mac."""
424+ mac = 'aa:bb:cc:aa:bb:cc'
425+ write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
426+ expected = {
427+ 'config': [{'type': 'physical', 'mac_address': mac,
428+ 'name': 'eth0', 'subnets': [{'type': 'dhcp'}]}],
429+ 'version': 1}
430+ valid_operstates = ['dormant', 'down', 'lowerlayerdown', 'unknown']
431+ for state in valid_operstates:
432+ write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
433+ self.assertEqual(expected, net.generate_fallback_config())
434+ write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), 'noworky')
435+ self.assertIsNone(net.generate_fallback_config())
436+
437+ def test_generate_fallback_config_skips_veth(self):
438+ """generate_fallback_config will skip any veth interfaces."""
439+ # A connected veth which gets ignored
440+ write_file(os.path.join(self.sysdir, 'veth0', 'carrier'), '1')
441+ self.assertIsNone(net.generate_fallback_config())
442+
443+ def test_generate_fallback_config_skips_bridges(self):
444+ """generate_fallback_config will skip any bridges interfaces."""
445+ # A connected veth which gets ignored
446+ write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
447+ mac = 'aa:bb:cc:aa:bb:cc'
448+ write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
449+ ensure_file(os.path.join(self.sysdir, 'eth0', 'bridge'))
450+ self.assertIsNone(net.generate_fallback_config())
451+
452+ def test_generate_fallback_config_skips_bonds(self):
453+ """generate_fallback_config will skip any bonded interfaces."""
454+ # A connected veth which gets ignored
455+ write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
456+ mac = 'aa:bb:cc:aa:bb:cc'
457+ write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
458+ ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
459+ self.assertIsNone(net.generate_fallback_config())
460+
461+
462+class TestGetDeviceList(CiTestCase):
463+
464+ def setUp(self):
465+ super(TestGetDeviceList, self).setUp()
466+ sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
467+ self.m_sys_path = sys_mock.start()
468+ self.sysdir = self.tmp_dir() + '/'
469+ self.m_sys_path.return_value = self.sysdir
470+ self.addCleanup(sys_mock.stop)
471+
472+ def test_get_devicelist_raise_oserror(self):
473+ """get_devicelist raise any non-ENOENT OSerror."""
474+ error = OSError('Can not do it')
475+ error.errno = errno.EPERM # Set non-ENOENT
476+ self.m_sys_path.side_effect = error
477+ with self.assertRaises(OSError) as context_manager:
478+ net.get_devicelist()
479+ exception = context_manager.exception
480+ self.assertEqual('Can not do it', str(exception))
481+
482+ def test_get_devicelist_empty_without_sys_net(self):
483+ """get_devicelist returns empty list when missing SYS_CLASS_NET."""
484+ self.m_sys_path.return_value = 'idontexist'
485+ self.assertEqual([], net.get_devicelist())
486+
487+ def test_get_devicelist_empty_with_no_devices_in_sys_net(self):
488+ """get_devicelist returns empty directoty listing for SYS_CLASS_NET."""
489+ self.assertEqual([], net.get_devicelist())
490+
491+ def test_get_devicelist_lists_any_subdirectories_in_sys_net(self):
492+ """get_devicelist returns a directory listing for SYS_CLASS_NET."""
493+ write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), 'up')
494+ write_file(os.path.join(self.sysdir, 'eth1', 'operstate'), 'up')
495+ self.assertItemsEqual(['eth0', 'eth1'], net.get_devicelist())
496+
497+
498+class TestGetInterfaceMAC(CiTestCase):
499+
500+ def setUp(self):
501+ super(TestGetInterfaceMAC, self).setUp()
502+ sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
503+ self.m_sys_path = sys_mock.start()
504+ self.sysdir = self.tmp_dir() + '/'
505+ self.m_sys_path.return_value = self.sysdir
506+ self.addCleanup(sys_mock.stop)
507+
508+ def test_get_interface_mac_false_with_no_mac(self):
509+ """get_device_list returns False when no mac is reported."""
510+ ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
511+ mac_path = os.path.join(self.sysdir, 'eth0', 'address')
512+ self.assertFalse(os.path.exists(mac_path))
513+ self.assertFalse(net.get_interface_mac('eth0'))
514+
515+ def test_get_interface_mac(self):
516+ """get_interfaces returns the mac from SYS_CLASS_NET/dev/address."""
517+ mac = 'aa:bb:cc:aa:bb:cc'
518+ write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
519+ self.assertEqual(mac, net.get_interface_mac('eth1'))
520+
521+ def test_get_interface_mac_grabs_bonding_address(self):
522+ """get_interfaces returns the source device mac for bonded devices."""
523+ source_dev_mac = 'aa:bb:cc:aa:bb:cc'
524+ bonded_mac = 'dd:ee:ff:dd:ee:ff'
525+ write_file(os.path.join(self.sysdir, 'eth1', 'address'), bonded_mac)
526+ write_file(
527+ os.path.join(self.sysdir, 'eth1', 'bonding_slave', 'perm_hwaddr'),
528+ source_dev_mac)
529+ self.assertEqual(source_dev_mac, net.get_interface_mac('eth1'))
530+
531+ def test_get_interfaces_empty_list_without_sys_net(self):
532+ """get_interfaces returns an empty list when missing SYS_CLASS_NET."""
533+ self.m_sys_path.return_value = 'idontexist'
534+ self.assertEqual([], net.get_interfaces())
535+
536+ def test_get_interfaces_by_mac_skips_empty_mac(self):
537+ """Ignore 00:00:00:00:00:00 addresses from get_interfaces_by_mac."""
538+ empty_mac = '00:00:00:00:00:00'
539+ mac = 'aa:bb:cc:aa:bb:cc'
540+ write_file(os.path.join(self.sysdir, 'eth1', 'address'), empty_mac)
541+ write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0')
542+ write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0')
543+ write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac)
544+ expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]
545+ self.assertEqual(expected, net.get_interfaces())
546+
547+ def test_get_interfaces_by_mac_skips_missing_mac(self):
548+ """Ignore interfaces without an address from get_interfaces_by_mac."""
549+ write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0')
550+ address_path = os.path.join(self.sysdir, 'eth1', 'address')
551+ self.assertFalse(os.path.exists(address_path))
552+ mac = 'aa:bb:cc:aa:bb:cc'
553+ write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0')
554+ write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac)
555+ expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]
556+ self.assertEqual(expected, net.get_interfaces())
557+
558+
559+class TestInterfaceHasOwnMAC(CiTestCase):
560+
561+ def setUp(self):
562+ super(TestInterfaceHasOwnMAC, self).setUp()
563+ sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
564+ self.m_sys_path = sys_mock.start()
565+ self.sysdir = self.tmp_dir() + '/'
566+ self.m_sys_path.return_value = self.sysdir
567+ self.addCleanup(sys_mock.stop)
568+
569+ def test_interface_has_own_mac_false_when_stolen(self):
570+ """Return False from interface_has_own_mac when address is stolen."""
571+ write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '2')
572+ self.assertFalse(net.interface_has_own_mac('eth1'))
573+
574+ def test_interface_has_own_mac_true_when_not_stolen(self):
575+ """Return False from interface_has_own_mac when mac isn't stolen."""
576+ valid_assign_types = ['0', '1', '3']
577+ assign_path = os.path.join(self.sysdir, 'eth1', 'addr_assign_type')
578+ for _type in valid_assign_types:
579+ write_file(assign_path, _type)
580+ self.assertTrue(net.interface_has_own_mac('eth1'))
581+
582+ def test_interface_has_own_mac_strict_errors_on_absent_assign_type(self):
583+ """When addr_assign_type is absent, interface_has_own_mac errors."""
584+ with self.assertRaises(ValueError):
585+ net.interface_has_own_mac('eth1', strict=True)
586+
587+
588+@mock.patch('cloudinit.net.util.subp')
589+class TestEphemeralIPV4Network(CiTestCase):
590+
591+ with_logs = True
592+
593+ def setUp(self):
594+ super(TestEphemeralIPV4Network, self).setUp()
595+ sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
596+ self.m_sys_path = sys_mock.start()
597+ self.sysdir = self.tmp_dir() + '/'
598+ self.m_sys_path.return_value = self.sysdir
599+ self.addCleanup(sys_mock.stop)
600+
601+ def test_ephemeral_ipv4_network_errors_on_missing_params(self, m_subp):
602+ """No required params for EphemeralIPv4Network can be None."""
603+ required_params = {
604+ 'interface': 'eth0', 'ip': '192.168.2.2',
605+ 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'}
606+ for key in required_params.keys():
607+ params = copy.deepcopy(required_params)
608+ params[key] = None
609+ with self.assertRaises(ValueError) as context_manager:
610+ net.EphemeralIPv4Network(**params)
611+ error = context_manager.exception
612+ self.assertIn('Cannot init network on', str(error))
613+ self.assertEqual(0, m_subp.call_count)
614+
615+ def test_ephemeral_ipv4_network_errors_invalid_mask(self, m_subp):
616+ """Raise an error when prefix_or_mask is not a netmask or prefix."""
617+ params = {
618+ 'interface': 'eth0', 'ip': '192.168.2.2',
619+ 'broadcast': '192.168.2.255'}
620+ invalid_masks = ('invalid', 'invalid.', '123.123.123')
621+ for error_val in invalid_masks:
622+ params['prefix_or_mask'] = error_val
623+ with self.assertRaises(ValueError) as context_manager:
624+ with net.EphemeralIPv4Network(**params):
625+ pass
626+ error = context_manager.exception
627+ self.assertIn('Cannot setup network: netmask', str(error))
628+ self.assertEqual(0, m_subp.call_count)
629+
630+ def test_ephemeral_ipv4_network_performs_teardown(self, m_subp):
631+ """EphemeralIPv4Network performs teardown on the device if setup."""
632+ expected_setup_calls = [
633+ mock.call(
634+ ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
635+ 'broadcast', '192.168.2.255', 'dev', 'eth0'],
636+ capture=True, update_env={'LANG': 'C'}),
637+ mock.call(
638+ ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'],
639+ capture=True)]
640+ expected_teardown_calls = [
641+ mock.call(
642+ ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0',
643+ 'down'], capture=True),
644+ mock.call(
645+ ['ip', '-family', 'inet', 'addr', 'del', '192.168.2.2/24',
646+ 'dev', 'eth0'], capture=True)]
647+ params = {
648+ 'interface': 'eth0', 'ip': '192.168.2.2',
649+ 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'}
650+ with net.EphemeralIPv4Network(**params):
651+ self.assertEqual(expected_setup_calls, m_subp.call_args_list)
652+ m_subp.assert_has_calls(expected_teardown_calls)
653+
654+ def test_ephemeral_ipv4_network_noop_when_configured(self, m_subp):
655+ """EphemeralIPv4Network handles exception when address is setup.
656+
657+ It performs no cleanup as the interface was already setup.
658+ """
659+ params = {
660+ 'interface': 'eth0', 'ip': '192.168.2.2',
661+ 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'}
662+ m_subp.side_effect = ProcessExecutionError(
663+ '', 'RTNETLINK answers: File exists', 2)
664+ expected_calls = [
665+ mock.call(
666+ ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
667+ 'broadcast', '192.168.2.255', 'dev', 'eth0'],
668+ capture=True, update_env={'LANG': 'C'})]
669+ with net.EphemeralIPv4Network(**params):
670+ pass
671+ self.assertEqual(expected_calls, m_subp.call_args_list)
672+ self.assertIn(
673+ 'Skip ephemeral network setup, eth0 already has address',
674+ self.logs.getvalue())
675+
676+ def test_ephemeral_ipv4_network_with_prefix(self, m_subp):
677+ """EphemeralIPv4Network takes a valid prefix to setup the network."""
678+ params = {
679+ 'interface': 'eth0', 'ip': '192.168.2.2',
680+ 'prefix_or_mask': '24', 'broadcast': '192.168.2.255'}
681+ for prefix_val in ['24', 16]: # prefix can be int or string
682+ params['prefix_or_mask'] = prefix_val
683+ with net.EphemeralIPv4Network(**params):
684+ pass
685+ m_subp.assert_has_calls([mock.call(
686+ ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
687+ 'broadcast', '192.168.2.255', 'dev', 'eth0'],
688+ capture=True, update_env={'LANG': 'C'})])
689+ m_subp.assert_has_calls([mock.call(
690+ ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/16',
691+ 'broadcast', '192.168.2.255', 'dev', 'eth0'],
692+ capture=True, update_env={'LANG': 'C'})])
693+
694+ def test_ephemeral_ipv4_network_with_new_default_route(self, m_subp):
695+ """Add the route when router is set and no default route exists."""
696+ params = {
697+ 'interface': 'eth0', 'ip': '192.168.2.2',
698+ 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255',
699+ 'router': '192.168.2.1'}
700+ m_subp.return_value = '', '' # Empty response from ip route gw check
701+ expected_setup_calls = [
702+ mock.call(
703+ ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
704+ 'broadcast', '192.168.2.255', 'dev', 'eth0'],
705+ capture=True, update_env={'LANG': 'C'}),
706+ mock.call(
707+ ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'],
708+ capture=True),
709+ mock.call(
710+ ['ip', 'route', 'show', '0.0.0.0/0'], capture=True),
711+ mock.call(
712+ ['ip', '-4', 'route', 'add', 'default', 'via',
713+ '192.168.2.1', 'dev', 'eth0'], capture=True)]
714+ expected_teardown_calls = [mock.call(
715+ ['ip', '-4', 'route', 'del', 'default', 'dev', 'eth0'],
716+ capture=True)]
717+
718+ with net.EphemeralIPv4Network(**params):
719+ self.assertEqual(expected_setup_calls, m_subp.call_args_list)
720+ m_subp.assert_has_calls(expected_teardown_calls)
721diff --git a/setup.py b/setup.py
722index b1bde43..5c65c7f 100755
723--- a/setup.py
724+++ b/setup.py
725@@ -240,7 +240,7 @@ setuptools.setup(
726 author='Scott Moser',
727 author_email='scott.moser@canonical.com',
728 url='http://launchpad.net/cloud-init/',
729- packages=setuptools.find_packages(exclude=['tests']),
730+ packages=setuptools.find_packages(exclude=['tests.*', '*.tests', 'tests']),
731 scripts=['tools/cloud-init-per'],
732 license='Dual-licensed under GPLv3 or Apache 2.0',
733 data_files=data_files,
734diff --git a/tox.ini b/tox.ini
735index 1140f9b..ef76884 100644
736--- a/tox.ini
737+++ b/tox.ini
738@@ -21,7 +21,11 @@ setenv =
739 LC_ALL = en_US.utf-8
740
741 [testenv:pylint]
742-deps = pylint==1.7.1
743+deps =
744+ # requirements
745+ pylint==1.7.1
746+ # test-requirements because unit tests are now present in cloudinit tree
747+ -r{toxinidir}/test-requirements.txt
748 commands = {envpython} -m pylint {posargs:cloudinit}
749
750 [testenv:py3]
751@@ -29,7 +33,7 @@ basepython = python3
752 deps = -r{toxinidir}/test-requirements.txt
753 commands = {envpython} -m nose {posargs:--with-coverage \
754 --cover-erase --cover-branches --cover-inclusive \
755- --cover-package=cloudinit tests/unittests}
756+ --cover-package=cloudinit tests/unittests cloudinit}
757
758 [testenv:py27]
759 basepython = python2.7
760@@ -98,7 +102,11 @@ deps = pyflakes
761
762 [testenv:tip-pylint]
763 commands = {envpython} -m pylint {posargs:cloudinit}
764-deps = pylint
765+deps =
766+ # requirements
767+ pylint
768+ # test-requirements
769+ -r{toxinidir}/test-requirements.txt
770
771 [testenv:citest]
772 basepython = python3

Subscribers

People subscribed via source and target branches