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

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

Commit message

cloudinit.net: add initialize_network_device function and tests

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

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

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

Description of the change

cloudinit.net: add initialize_network_device function and unittest.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

mock get_sys_class_path function in unit tests to allow for cleanup

a0bd7a7... by Chad Smith

lints

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

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

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

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

check is_up instead of is_connected before configuring a device

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

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

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

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

fixup leaky unittest mocks

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

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

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

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

small questions in line.

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

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

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

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

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

787586e... by Chad Smith

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

cdba67c... by Chad Smith

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

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

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

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

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

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

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

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

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

9cdbb64... by Chad Smith

lints/flakes

8ef6575... by Chad Smith

include all unit test-requirements for tox lint targets

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

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

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

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

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

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

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

route del needs to happen before ip addr del

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

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

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

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

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

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

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

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

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

review: Approve
49a4b83... by Chad Smith

docstring fixup

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

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

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

review: Approve (continuous-integration)

Preview Diff

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

Subscribers

People subscribed via source and target branches