Merge ~jocave/plainbox-provider-resource:net-if-mngr-testing into plainbox-provider-resource:master

Proposed by Jonathan Cave
Status: Merged
Approved by: Jonathan Cave
Approved revision: dfb438aa7d5b0a4aac4630aa3d4a9ce8e884b6fd
Merged at revision: 93c7db82c2ceea987ad633df28ead17bcfb4f08c
Proposed branch: ~jocave/plainbox-provider-resource:net-if-mngr-testing
Merge into: plainbox-provider-resource:master
Diff against target: 385 lines (+238/-45)
9 files modified
bin/net_if_management.py (+65/-45)
tests/test_net_if_management.py (+137/-0)
tests/test_net_if_management_data/CARA_T_netplan.yaml (+2/-0)
tests/test_net_if_management_data/CARA_T_nmcli.txt (+4/-0)
tests/test_net_if_management_data/CASCADE_500_netplan.yaml (+2/-0)
tests/test_net_if_management_data/CASCADE_500_nmcli.txt (+5/-0)
tests/test_net_if_management_data/RPI2_UC16_CCONF_netplan.yaml (+7/-0)
tests/test_net_if_management_data/RPI3B_UC16_CLOUDINIT_netplan.yaml (+13/-0)
tests/test_net_if_management_data/XENIAL_DESKTOP_nmcli.txt (+3/-0)
Reviewer Review Type Date Requested Status
Maciej Kisielewski Approve
Jonathan Cave (community) Needs Resubmitting
Review via email: mp+370413@code.launchpad.net

Description of the change

Culmination of the MRs to support provider testing.

In this case the aim was to make sure the net_if_management resource job works across the range of hardware and OS images we need to support.

Two commits included, first one is the changes to the existing script to make it testable and includes the changes that mean it should work on xenial desktop images (problems had been reported during SRU testing for this combination).

Second is the creation of the unit tests themselves. Includes an initial set of test scenarios chosen from reference devices and commonly problematic projects.

To post a comment you must log in.
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

I'd love to see textwrap's dedent used for those multiline strings in tests.

review: Needs Fixing
Revision history for this message
Jonathan Cave (jocave) wrote :

Rather than using dedent I thought it would be simpler for someone copying in configs to be able to just use text files.

review: Needs Resubmitting
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

LGTM, +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/bin/net_if_management.py b/bin/net_if_management.py
index c4604c3..ef6fcde 100755
--- a/bin/net_if_management.py
+++ b/bin/net_if_management.py
@@ -18,6 +18,7 @@
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
1919
20from enum import Enum20from enum import Enum
21from shutil import which
21import subprocess as sp22import subprocess as sp
22import sys23import sys
2324
@@ -43,19 +44,24 @@ class UdevInterfaceLister(UdevResult):
43 self.names.append(p)44 self.names.append(p)
4445
4546
47def is_nm_available():
48 return which('nmcli') is not None
49
50
51def is_netplan_available():
52 return which('netplan') is not None
53
54
46class NmInterfaceState():55class NmInterfaceState():
4756
48 def __init__(self):57 def __init__(self):
49 self.devices = {}58 self.devices = {}
50 cmd = 'nmcli -v'59
51 rc = sp.call(cmd, shell=True, stdout=sp.DEVNULL, stderr=sp.DEVNULL)60 def parse(self, data=None):
52 if rc != 0:61 if data is None:
53 self.available = False62 cmd = 'nmcli -t -f DEVICE,STATE d'
54 return63 data = sp.check_output(cmd, shell=True).decode(sys.stdout.encoding)
55 self.available = True64 for line in data.splitlines():
56 cmd = 'nmcli -t -f DEVICE,STATE d'
57 output = sp.check_output(cmd, shell=True).decode(sys.stdout.encoding)
58 for line in output.splitlines():
59 dev, state = line.strip().split(':')65 dev, state = line.strip().split(':')
60 self.devices[dev] = state66 self.devices[dev] = state
6167
@@ -67,60 +73,74 @@ class States(Enum):
67 nm = 'NetworkManager'73 nm = 'NetworkManager'
6874
6975
70def main():76def identify_managers(interfaces=None,
71 # Use udev as definitive source of network interfaces77 has_netplan=True, netplan_yaml=None,
72 all_interfaces = UdevInterfaceLister(['NETWORK', 'WIRELESS'])78 has_nm=True, nm_device_state=None):
79 if interfaces is None:
80 interfaces = UdevInterfaceLister(['NETWORK', 'WIRELESS']).names
7381
74 # Get the neplan config82 results = dict.fromkeys(interfaces, States.unspecified)
75 netplan_conf = Netplan()
76 netplan_conf.parse()
7783
78 # Get the NetworkManager config84 if has_nm:
79 nm_conf = NmInterfaceState()85 nm_conf = NmInterfaceState()
86 nm_conf.parse(nm_device_state)
8087
81 # fallback state88 # fallback state
82 global_scope_manager = States.unspecified.value89 global_scope_manager = States.unspecified.value
8390 if has_netplan:
84 # if netplan has a top-level renderer use that as default:91 netplan_conf = Netplan()
85 if netplan_conf.network.get('renderer'):92 netplan_conf.parse(data=netplan_yaml)
86 global_scope_manager = netplan_conf.network['renderer']93 # if netplan has a top-level renderer use that as default:
8794 if netplan_conf.network.get('renderer'):
88 for n in all_interfaces.names:95 global_scope_manager = netplan_conf.network['renderer']
89 print('device: {}'.format(n))96
90 print('nmcli_available: {}'.format(nm_conf.available))97 for n in results:
91
92 category_scope_manager = States.unspecified.value98 category_scope_manager = States.unspecified.value
93 if n in netplan_conf.wifis:99 if has_netplan:
94 category_scope_manager = netplan_conf.wifis.get(100 if n in netplan_conf.wifis:
95 'renderer', States.unspecified.value)101 category_scope_manager = netplan_conf.wifis.get(
96 elif n in netplan_conf.ethernets:102 'renderer', States.unspecified.value)
97 category_scope_manager = netplan_conf.ethernets.get(103 elif n in netplan_conf.ethernets:
98 'renderer', States.unspecified.value)104 category_scope_manager = netplan_conf.ethernets.get(
105 'renderer', States.unspecified.value)
99106
100 # Netplan config indcates NM107 # Netplan config indcates NM
101 if (global_scope_manager == States.nm.value or108 if (global_scope_manager == States.nm.value or
102 category_scope_manager == States.nm.value):109 category_scope_manager == States.nm.value or
110 not has_netplan):
103 # if NM isnt actually available this is a bad config111 # if NM isnt actually available this is a bad config
104 if not nm_conf.available:112 if not has_nm:
105 print('managed_by: {}'.format(States.error.value))113 print('error: netplan defines NM or there is no netplan, '
106 print()114 'but NM unavailable')
115 results[n] = States.error
107 continue116 continue
108 # NM does not know the interface117 # NM does not know the interface
109 if nm_conf.devices.get(n) is None:118 if nm_conf.devices.get(n) is None:
110 print('managed_by: {}'.format(States.error.value))119 print('error: netplan defines NM or there is no netplan, '
111 print()120 'but interface unknown to NM')
121 results[n] = States.error
112 continue122 continue
113 # NM thinks it doesnt managed the device despite netplan config123 # NM thinks it doesnt manage the device despite netplan config
114 if nm_conf.devices.get(n) == 'unmanaged':124 if nm_conf.devices.get(n) == 'unmanaged':
115 print('managed_by: {}'.format(States.error.value))125 print('error: netplan defines NM or there is no netplan, '
116 print()126 'but NM reports unmanaged')
127 results[n] = States.error
117 continue128 continue
118 print('managed_by: {}'.format(States.nm.value))129 results[n] = States.nm
119 print()
120 continue130 continue
121131
122 # No renderer specified132 # has netplan but no renderer specified
123 print('managed_by: {}'.format(States.networkd.value))133 if has_netplan:
134 results[n] = States.networkd
135 return results
136
137
138def main():
139 results = identify_managers(has_netplan=is_netplan_available(),
140 has_nm=is_nm_available())
141 for interface, state in results.items():
142 print('device: {}'.format(interface))
143 print('managed_by: {}'.format(state.value))
124 print()144 print()
125145
126146
diff --git a/tests/test_net_if_management.py b/tests/test_net_if_management.py
127new file mode 100644147new file mode 100644
index 0000000..8f68d86
--- /dev/null
+++ b/tests/test_net_if_management.py
@@ -0,0 +1,137 @@
1#!/usr/bin/env python3
2# This file is part of Checkbox.
3#
4# Copyright 2019 Canonical Ltd.
5# Written by:
6# Jonathan Cave <jonathan.cave@canonical.com>
7#
8# Checkbox is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 3,
10# as published by the Free Software Foundation.
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20import os
21import unittest
22
23from checkbox_support.parsers.netplan import Netplan
24from checkbox_support.parsers.udevadm import UdevadmParser, UdevResult
25
26import net_if_management
27
28
29class NetIfMngrTest():
30
31 has_netplan = True
32 has_nm = True
33
34 @staticmethod
35 def get_text(filename):
36 full_path = os.path.join(
37 os.path.dirname(os.path.realpath(__file__)),
38 'test_net_if_management_data',
39 filename)
40 with open(full_path, 'rt', encoding='UTF-8') as stream:
41 return stream.read()
42
43 def get_results(self):
44 if self.netplan_yaml is None:
45 self.has_netplan = False
46 if self.nm_device_state is None:
47 self.has_nm = False
48 return net_if_management.identify_managers(self.interfaces,
49 self.has_netplan,
50 self.netplan_yaml,
51 self.has_nm,
52 self.nm_device_state)
53
54
55class Test_CARA_T(unittest.TestCase, NetIfMngrTest):
56 # the interfaces we interested in (as provided by the udev parser)
57 interfaces = ['eth0', 'eth1', 'wlan0']
58
59 # the combined netplan configuration
60 netplan_yaml = NetIfMngrTest.get_text('CARA_T_netplan.yaml')
61
62 # capture output of `sudo nmcli -t -f DEVICE,STATE d`
63 # or None if no NM available
64 nm_device_state = NetIfMngrTest.get_text('CARA_T_nmcli.txt')
65
66 def test(self):
67 res = self.get_results()
68 self.assertEqual(res['eth0'].value, 'NetworkManager')
69 self.assertEqual(res['eth1'].value, 'NetworkManager')
70 self.assertEqual(res['wlan0'].value, 'NetworkManager')
71
72
73class Test_XENIAL_DESKTOP(unittest.TestCase, NetIfMngrTest):
74 # the interfaces we interested in (as provided by the udev parser)
75 interfaces = ['eth0', 'wlan0']
76
77 # the combined netplan configuration or `None` if netplan not installed
78 netplan_yaml = None
79
80 # capture output of `sudo nmcli -t -f DEVICE,STATE d`
81 # or None if NM is not installed
82 nm_device_state = NetIfMngrTest.get_text('XENIAL_DESKTOP_nmcli.txt')
83
84 def test(self):
85 res = self.get_results()
86 self.assertEqual(res['eth0'].value, 'NetworkManager')
87 self.assertEqual(res['wlan0'].value, 'NetworkManager')
88
89
90class Test_CASCADE_500(unittest.TestCase, NetIfMngrTest):
91 # the interfaces we interested in (as provided by the udev parser)
92 interfaces = ['eth0', 'wlan0']
93
94 # the combined netplan configuration or `None` if netplan not installed
95 netplan_yaml = NetIfMngrTest.get_text('CASCADE_500_netplan.yaml')
96
97 # capture output of `sudo nmcli -t -f DEVICE,STATE d`
98 # or None if NM is not installed
99 nm_device_state = NetIfMngrTest.get_text('CASCADE_500_nmcli.txt')
100
101 def test(self):
102 res = self.get_results()
103 self.assertEqual(res['eth0'].value, 'NetworkManager')
104 self.assertEqual(res['wlan0'].value, 'NetworkManager')
105
106
107class Test_RPI2_UC16_CCONF(unittest.TestCase, NetIfMngrTest):
108 # the interfaces we interested in (as provided by the udev parser)
109 interfaces = ['eth0']
110
111 # the combined netplan configuration or `None` if netplan not installed
112 netplan_yaml = NetIfMngrTest.get_text('RPI2_UC16_CCONF_netplan.yaml')
113
114 # capture output of `sudo nmcli -t -f DEVICE,STATE d`
115 # or None if NM is not installed
116 nm_device_state = None
117
118 def test(self):
119 res = self.get_results()
120 self.assertEqual(res['eth0'].value, 'networkd')
121
122
123class Test_RPI3B_UC16_CLOUDINIT(unittest.TestCase, NetIfMngrTest):
124 # the interfaces we interested in (as provided by the udev parser)
125 interfaces = ['eth0', 'wlan0']
126
127 # the combined netplan configuration or `None` if netplan not installed
128 netplan_yaml = NetIfMngrTest.get_text('RPI3B_UC16_CLOUDINIT_netplan.yaml')
129
130 # capture output of `sudo nmcli -t -f DEVICE,STATE d`
131 # or None if NM is not installed
132 nm_device_state = None
133
134 def test(self):
135 res = self.get_results()
136 self.assertEqual(res['eth0'].value, 'networkd')
137 self.assertEqual(res['wlan0'].value, 'networkd')
diff --git a/tests/test_net_if_management_data/CARA_T_netplan.yaml b/tests/test_net_if_management_data/CARA_T_netplan.yaml
0new file mode 100644138new file mode 100644
index 0000000..43bbdc7
--- /dev/null
+++ b/tests/test_net_if_management_data/CARA_T_netplan.yaml
@@ -0,0 +1,2 @@
1network:
2 renderer: NetworkManager
0\ No newline at end of file3\ No newline at end of file
diff --git a/tests/test_net_if_management_data/CARA_T_nmcli.txt b/tests/test_net_if_management_data/CARA_T_nmcli.txt
1new file mode 1006444new file mode 100644
index 0000000..b4aaaae
--- /dev/null
+++ b/tests/test_net_if_management_data/CARA_T_nmcli.txt
@@ -0,0 +1,4 @@
1eth0:connected
2eth1:unavailable
3wlan0:disconnected
4lo:unmanaged
0\ No newline at end of file5\ No newline at end of file
diff --git a/tests/test_net_if_management_data/CASCADE_500_netplan.yaml b/tests/test_net_if_management_data/CASCADE_500_netplan.yaml
1new file mode 1006446new file mode 100644
index 0000000..43bbdc7
--- /dev/null
+++ b/tests/test_net_if_management_data/CASCADE_500_netplan.yaml
@@ -0,0 +1,2 @@
1network:
2 renderer: NetworkManager
0\ No newline at end of file3\ No newline at end of file
diff --git a/tests/test_net_if_management_data/CASCADE_500_nmcli.txt b/tests/test_net_if_management_data/CASCADE_500_nmcli.txt
1new file mode 1006444new file mode 100644
index 0000000..a2ee118
--- /dev/null
+++ b/tests/test_net_if_management_data/CASCADE_500_nmcli.txt
@@ -0,0 +1,5 @@
1eth0: connected
2wlan0: connected
3ttyACM1: unavailable
4lo: unmanaged
5p2p0: unmanaged
0\ No newline at end of file6\ No newline at end of file
diff --git a/tests/test_net_if_management_data/RPI2_UC16_CCONF_netplan.yaml b/tests/test_net_if_management_data/RPI2_UC16_CCONF_netplan.yaml
1new file mode 1006447new file mode 100644
index 0000000..afa0ebc
--- /dev/null
+++ b/tests/test_net_if_management_data/RPI2_UC16_CCONF_netplan.yaml
@@ -0,0 +1,7 @@
1# This is the network config written by 'console_conf'
2network:
3 ethernets:
4 eth0:
5 addresses: []
6 dhcp4: true
7 version: 2
0\ No newline at end of file8\ No newline at end of file
diff --git a/tests/test_net_if_management_data/RPI3B_UC16_CLOUDINIT_netplan.yaml b/tests/test_net_if_management_data/RPI3B_UC16_CLOUDINIT_netplan.yaml
1new file mode 1006449new file mode 100644
index 0000000..c6deff3
--- /dev/null
+++ b/tests/test_net_if_management_data/RPI3B_UC16_CLOUDINIT_netplan.yaml
@@ -0,0 +1,13 @@
1# This file is generated from information provided by
2# the datasource. Changes to it will not persist across an instance.
3# To disable cloud-init's network configuration capabilities, write a file
4# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
5# network: {config: disabled}
6network:
7 version: 2
8 ethernets:
9 eth0:
10 dhcp4: true
11 match:
12 macaddress: 00:00:00:00:00:00
13 set-name: eth0
0\ No newline at end of file14\ No newline at end of file
diff --git a/tests/test_net_if_management_data/XENIAL_DESKTOP_nmcli.txt b/tests/test_net_if_management_data/XENIAL_DESKTOP_nmcli.txt
1new file mode 10064415new file mode 100644
index 0000000..71105db
--- /dev/null
+++ b/tests/test_net_if_management_data/XENIAL_DESKTOP_nmcli.txt
@@ -0,0 +1,3 @@
1eth0:connected
2wlan0:disconnected
3lo:unmanaged
0\ No newline at end of file4\ No newline at end of file

Subscribers

People subscribed via source and target branches