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