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

Proposed by Chad Smith on 2017-07-20
Status: Merged
Approved by: Scott Moser on 2017-07-31
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 on 2017-07-28
Scott Moser 2017-07-20 Approve on 2017-07-28
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.
Chad Smith (chad.smith) :

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)
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.

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)

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)

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 on 2017-07-22

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

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 on 2017-07-24

mock get_sys_class_path function in unit tests to allow for cleanup

a0bd7a7... by Chad Smith on 2017-07-24

lints

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 on 2017-07-24

check is_up instead of is_connected before configuring a device

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 on 2017-07-25

fixup leaky unittest mocks

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)
Scott Moser (smoser) wrote :

small questions in line.

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 on 2017-07-26

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

787586e... by Chad Smith on 2017-07-26

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 on 2017-07-26

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

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)

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)
Chad Smith (chad.smith) :
c25b725... by Chad Smith on 2017-07-27

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

9cdbb64... by Chad Smith on 2017-07-27

lints/flakes

8ef6575... by Chad Smith on 2017-07-27

include all unit test-requirements for tox lint targets

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.

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 on 2017-07-27

route del needs to happen before ip addr del

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.

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

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)
Scott Moser (smoser) wrote :

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

review: Approve
49a4b83... by Chad Smith on 2017-07-28

docstring fixup

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)

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