Merge ~chad.smith/cloud-init:unittests-in-cloudinit-package into cloud-init:master
- Git
- lp:~chad.smith/cloud-init
- unittests-in-cloudinit-package
- Merge into master
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) |
Related bugs: |
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_
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.
- 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_
- 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_
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.
- 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_
- 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_
Chad Smith (chad.smith) : | # |
Server Team CI bot (server-team-bot) wrote : | # |
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.
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:ed950d27697
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:4b4989d8943
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:a3692fcda19
https:/
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:/
- 0f476ef... by Chad Smith
-
For realsies: it makes more sense that EphemeralIPv4Ne
twork is a context manager than DhcpCleanDiscovery handling all the setup an teardown of ips. Also allow EphemeralIPv4Ne twork to accept prefix or netmask
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:0f476ef54a9
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
- 4811785... by Chad Smith
-
mock get_sys_class_path function in unit tests to allow for cleanup
- a0bd7a7... by Chad Smith
-
lints
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:a0bd7a79f08
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
- e37c259... by Chad Smith
-
check is_up instead of is_connected before configuring a device
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:e37c259841b
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
- 93ace28... by Chad Smith
-
fixup leaky unittest mocks
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:93ace2828ef
https:/
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:/
Scott Moser (smoser) wrote : | # |
small questions in line.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:93ace2828ef
https:/
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:/
- b6369bd... by Chad Smith
-
add get_ip_info helper function to allow inspecting ip addrs before setup or teardown in EphemeralIPv4Ne
twork 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 EphemeralIPv4Ne
twork context manager
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:787586e45a5
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
FAILED: Ubuntu LTS: Build
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:cdba67c453c
https:/
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:/
Chad Smith (chad.smith) : | # |
- c25b725... by Chad Smith
-
drop get_ip_info utility function and handle ProcessExecutio
nError 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
Chad Smith (chad.smith) wrote : | # |
Per our discussion today, I simplified logic in EphemeralIPv4Ne
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.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:8ef6575c63e
https:/
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:/
- 3d85180... by Chad Smith
-
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/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:3d851806873
https:/
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:/
Scott Moser (smoser) wrote : | # |
tiny things.
fix that comment, and i approve.
thank you Chad.
- 49a4b83... by Chad Smith
-
docstring fixup
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:49a4b832712
https:/
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:/
Update scan failed
At least one of the branches involved have failed to scan. You can manually schedule a rescan if required.
Preview Diff
1 | diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py |
2 | index 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 | |
188 | diff --git a/cloudinit/net/tests/__init__.py b/cloudinit/net/tests/__init__.py |
189 | new file mode 100644 |
190 | index 0000000..e69de29 |
191 | --- /dev/null |
192 | +++ b/cloudinit/net/tests/__init__.py |
193 | diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py |
194 | new file mode 100644 |
195 | index 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) |
721 | diff --git a/setup.py b/setup.py |
722 | index 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, |
734 | diff --git a/tox.ini b/tox.ini |
735 | index 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 |
PASSED: Continuous integration, rev:a3692fcda19 267d6d63b0317cd fb40d6df8b08ed /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 64/
https:/
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: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 64/rebuild
https:/