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
1diff --git a/bin/net_if_management.py b/bin/net_if_management.py
2index c4604c3..ef6fcde 100755
3--- a/bin/net_if_management.py
4+++ b/bin/net_if_management.py
5@@ -18,6 +18,7 @@
6 # along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
7
8 from enum import Enum
9+from shutil import which
10 import subprocess as sp
11 import sys
12
13@@ -43,19 +44,24 @@ class UdevInterfaceLister(UdevResult):
14 self.names.append(p)
15
16
17+def is_nm_available():
18+ return which('nmcli') is not None
19+
20+
21+def is_netplan_available():
22+ return which('netplan') is not None
23+
24+
25 class NmInterfaceState():
26
27 def __init__(self):
28 self.devices = {}
29- cmd = 'nmcli -v'
30- rc = sp.call(cmd, shell=True, stdout=sp.DEVNULL, stderr=sp.DEVNULL)
31- if rc != 0:
32- self.available = False
33- return
34- self.available = True
35- cmd = 'nmcli -t -f DEVICE,STATE d'
36- output = sp.check_output(cmd, shell=True).decode(sys.stdout.encoding)
37- for line in output.splitlines():
38+
39+ def parse(self, data=None):
40+ if data is None:
41+ cmd = 'nmcli -t -f DEVICE,STATE d'
42+ data = sp.check_output(cmd, shell=True).decode(sys.stdout.encoding)
43+ for line in data.splitlines():
44 dev, state = line.strip().split(':')
45 self.devices[dev] = state
46
47@@ -67,60 +73,74 @@ class States(Enum):
48 nm = 'NetworkManager'
49
50
51-def main():
52- # Use udev as definitive source of network interfaces
53- all_interfaces = UdevInterfaceLister(['NETWORK', 'WIRELESS'])
54+def identify_managers(interfaces=None,
55+ has_netplan=True, netplan_yaml=None,
56+ has_nm=True, nm_device_state=None):
57+ if interfaces is None:
58+ interfaces = UdevInterfaceLister(['NETWORK', 'WIRELESS']).names
59
60- # Get the neplan config
61- netplan_conf = Netplan()
62- netplan_conf.parse()
63+ results = dict.fromkeys(interfaces, States.unspecified)
64
65- # Get the NetworkManager config
66- nm_conf = NmInterfaceState()
67+ if has_nm:
68+ nm_conf = NmInterfaceState()
69+ nm_conf.parse(nm_device_state)
70
71 # fallback state
72 global_scope_manager = States.unspecified.value
73-
74- # if netplan has a top-level renderer use that as default:
75- if netplan_conf.network.get('renderer'):
76- global_scope_manager = netplan_conf.network['renderer']
77-
78- for n in all_interfaces.names:
79- print('device: {}'.format(n))
80- print('nmcli_available: {}'.format(nm_conf.available))
81-
82+ if has_netplan:
83+ netplan_conf = Netplan()
84+ netplan_conf.parse(data=netplan_yaml)
85+ # if netplan has a top-level renderer use that as default:
86+ if netplan_conf.network.get('renderer'):
87+ global_scope_manager = netplan_conf.network['renderer']
88+
89+ for n in results:
90 category_scope_manager = States.unspecified.value
91- if n in netplan_conf.wifis:
92- category_scope_manager = netplan_conf.wifis.get(
93- 'renderer', States.unspecified.value)
94- elif n in netplan_conf.ethernets:
95- category_scope_manager = netplan_conf.ethernets.get(
96- 'renderer', States.unspecified.value)
97+ if has_netplan:
98+ if n in netplan_conf.wifis:
99+ category_scope_manager = netplan_conf.wifis.get(
100+ 'renderer', States.unspecified.value)
101+ elif n in netplan_conf.ethernets:
102+ category_scope_manager = netplan_conf.ethernets.get(
103+ 'renderer', States.unspecified.value)
104
105 # Netplan config indcates NM
106 if (global_scope_manager == States.nm.value or
107- category_scope_manager == States.nm.value):
108+ category_scope_manager == States.nm.value or
109+ not has_netplan):
110 # if NM isnt actually available this is a bad config
111- if not nm_conf.available:
112- print('managed_by: {}'.format(States.error.value))
113- print()
114+ if not has_nm:
115+ print('error: netplan defines NM or there is no netplan, '
116+ 'but NM unavailable')
117+ results[n] = States.error
118 continue
119 # NM does not know the interface
120 if nm_conf.devices.get(n) is None:
121- print('managed_by: {}'.format(States.error.value))
122- print()
123+ print('error: netplan defines NM or there is no netplan, '
124+ 'but interface unknown to NM')
125+ results[n] = States.error
126 continue
127- # NM thinks it doesnt managed the device despite netplan config
128+ # NM thinks it doesnt manage the device despite netplan config
129 if nm_conf.devices.get(n) == 'unmanaged':
130- print('managed_by: {}'.format(States.error.value))
131- print()
132+ print('error: netplan defines NM or there is no netplan, '
133+ 'but NM reports unmanaged')
134+ results[n] = States.error
135 continue
136- print('managed_by: {}'.format(States.nm.value))
137- print()
138+ results[n] = States.nm
139 continue
140
141- # No renderer specified
142- print('managed_by: {}'.format(States.networkd.value))
143+ # has netplan but no renderer specified
144+ if has_netplan:
145+ results[n] = States.networkd
146+ return results
147+
148+
149+def main():
150+ results = identify_managers(has_netplan=is_netplan_available(),
151+ has_nm=is_nm_available())
152+ for interface, state in results.items():
153+ print('device: {}'.format(interface))
154+ print('managed_by: {}'.format(state.value))
155 print()
156
157
158diff --git a/tests/test_net_if_management.py b/tests/test_net_if_management.py
159new file mode 100644
160index 0000000..8f68d86
161--- /dev/null
162+++ b/tests/test_net_if_management.py
163@@ -0,0 +1,137 @@
164+#!/usr/bin/env python3
165+# This file is part of Checkbox.
166+#
167+# Copyright 2019 Canonical Ltd.
168+# Written by:
169+# Jonathan Cave <jonathan.cave@canonical.com>
170+#
171+# Checkbox is free software: you can redistribute it and/or modify
172+# it under the terms of the GNU General Public License version 3,
173+# as published by the Free Software Foundation.
174+#
175+# Checkbox is distributed in the hope that it will be useful,
176+# but WITHOUT ANY WARRANTY; without even the implied warranty of
177+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
178+# GNU General Public License for more details.
179+#
180+# You should have received a copy of the GNU General Public License
181+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
182+
183+import os
184+import unittest
185+
186+from checkbox_support.parsers.netplan import Netplan
187+from checkbox_support.parsers.udevadm import UdevadmParser, UdevResult
188+
189+import net_if_management
190+
191+
192+class NetIfMngrTest():
193+
194+ has_netplan = True
195+ has_nm = True
196+
197+ @staticmethod
198+ def get_text(filename):
199+ full_path = os.path.join(
200+ os.path.dirname(os.path.realpath(__file__)),
201+ 'test_net_if_management_data',
202+ filename)
203+ with open(full_path, 'rt', encoding='UTF-8') as stream:
204+ return stream.read()
205+
206+ def get_results(self):
207+ if self.netplan_yaml is None:
208+ self.has_netplan = False
209+ if self.nm_device_state is None:
210+ self.has_nm = False
211+ return net_if_management.identify_managers(self.interfaces,
212+ self.has_netplan,
213+ self.netplan_yaml,
214+ self.has_nm,
215+ self.nm_device_state)
216+
217+
218+class Test_CARA_T(unittest.TestCase, NetIfMngrTest):
219+ # the interfaces we interested in (as provided by the udev parser)
220+ interfaces = ['eth0', 'eth1', 'wlan0']
221+
222+ # the combined netplan configuration
223+ netplan_yaml = NetIfMngrTest.get_text('CARA_T_netplan.yaml')
224+
225+ # capture output of `sudo nmcli -t -f DEVICE,STATE d`
226+ # or None if no NM available
227+ nm_device_state = NetIfMngrTest.get_text('CARA_T_nmcli.txt')
228+
229+ def test(self):
230+ res = self.get_results()
231+ self.assertEqual(res['eth0'].value, 'NetworkManager')
232+ self.assertEqual(res['eth1'].value, 'NetworkManager')
233+ self.assertEqual(res['wlan0'].value, 'NetworkManager')
234+
235+
236+class Test_XENIAL_DESKTOP(unittest.TestCase, NetIfMngrTest):
237+ # the interfaces we interested in (as provided by the udev parser)
238+ interfaces = ['eth0', 'wlan0']
239+
240+ # the combined netplan configuration or `None` if netplan not installed
241+ netplan_yaml = None
242+
243+ # capture output of `sudo nmcli -t -f DEVICE,STATE d`
244+ # or None if NM is not installed
245+ nm_device_state = NetIfMngrTest.get_text('XENIAL_DESKTOP_nmcli.txt')
246+
247+ def test(self):
248+ res = self.get_results()
249+ self.assertEqual(res['eth0'].value, 'NetworkManager')
250+ self.assertEqual(res['wlan0'].value, 'NetworkManager')
251+
252+
253+class Test_CASCADE_500(unittest.TestCase, NetIfMngrTest):
254+ # the interfaces we interested in (as provided by the udev parser)
255+ interfaces = ['eth0', 'wlan0']
256+
257+ # the combined netplan configuration or `None` if netplan not installed
258+ netplan_yaml = NetIfMngrTest.get_text('CASCADE_500_netplan.yaml')
259+
260+ # capture output of `sudo nmcli -t -f DEVICE,STATE d`
261+ # or None if NM is not installed
262+ nm_device_state = NetIfMngrTest.get_text('CASCADE_500_nmcli.txt')
263+
264+ def test(self):
265+ res = self.get_results()
266+ self.assertEqual(res['eth0'].value, 'NetworkManager')
267+ self.assertEqual(res['wlan0'].value, 'NetworkManager')
268+
269+
270+class Test_RPI2_UC16_CCONF(unittest.TestCase, NetIfMngrTest):
271+ # the interfaces we interested in (as provided by the udev parser)
272+ interfaces = ['eth0']
273+
274+ # the combined netplan configuration or `None` if netplan not installed
275+ netplan_yaml = NetIfMngrTest.get_text('RPI2_UC16_CCONF_netplan.yaml')
276+
277+ # capture output of `sudo nmcli -t -f DEVICE,STATE d`
278+ # or None if NM is not installed
279+ nm_device_state = None
280+
281+ def test(self):
282+ res = self.get_results()
283+ self.assertEqual(res['eth0'].value, 'networkd')
284+
285+
286+class Test_RPI3B_UC16_CLOUDINIT(unittest.TestCase, NetIfMngrTest):
287+ # the interfaces we interested in (as provided by the udev parser)
288+ interfaces = ['eth0', 'wlan0']
289+
290+ # the combined netplan configuration or `None` if netplan not installed
291+ netplan_yaml = NetIfMngrTest.get_text('RPI3B_UC16_CLOUDINIT_netplan.yaml')
292+
293+ # capture output of `sudo nmcli -t -f DEVICE,STATE d`
294+ # or None if NM is not installed
295+ nm_device_state = None
296+
297+ def test(self):
298+ res = self.get_results()
299+ self.assertEqual(res['eth0'].value, 'networkd')
300+ self.assertEqual(res['wlan0'].value, 'networkd')
301diff --git a/tests/test_net_if_management_data/CARA_T_netplan.yaml b/tests/test_net_if_management_data/CARA_T_netplan.yaml
302new file mode 100644
303index 0000000..43bbdc7
304--- /dev/null
305+++ b/tests/test_net_if_management_data/CARA_T_netplan.yaml
306@@ -0,0 +1,2 @@
307+network:
308+ renderer: NetworkManager
309\ No newline at end of file
310diff --git a/tests/test_net_if_management_data/CARA_T_nmcli.txt b/tests/test_net_if_management_data/CARA_T_nmcli.txt
311new file mode 100644
312index 0000000..b4aaaae
313--- /dev/null
314+++ b/tests/test_net_if_management_data/CARA_T_nmcli.txt
315@@ -0,0 +1,4 @@
316+eth0:connected
317+eth1:unavailable
318+wlan0:disconnected
319+lo:unmanaged
320\ No newline at end of file
321diff --git a/tests/test_net_if_management_data/CASCADE_500_netplan.yaml b/tests/test_net_if_management_data/CASCADE_500_netplan.yaml
322new file mode 100644
323index 0000000..43bbdc7
324--- /dev/null
325+++ b/tests/test_net_if_management_data/CASCADE_500_netplan.yaml
326@@ -0,0 +1,2 @@
327+network:
328+ renderer: NetworkManager
329\ No newline at end of file
330diff --git a/tests/test_net_if_management_data/CASCADE_500_nmcli.txt b/tests/test_net_if_management_data/CASCADE_500_nmcli.txt
331new file mode 100644
332index 0000000..a2ee118
333--- /dev/null
334+++ b/tests/test_net_if_management_data/CASCADE_500_nmcli.txt
335@@ -0,0 +1,5 @@
336+eth0: connected
337+wlan0: connected
338+ttyACM1: unavailable
339+lo: unmanaged
340+p2p0: unmanaged
341\ No newline at end of file
342diff --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
343new file mode 100644
344index 0000000..afa0ebc
345--- /dev/null
346+++ b/tests/test_net_if_management_data/RPI2_UC16_CCONF_netplan.yaml
347@@ -0,0 +1,7 @@
348+# This is the network config written by 'console_conf'
349+network:
350+ ethernets:
351+ eth0:
352+ addresses: []
353+ dhcp4: true
354+ version: 2
355\ No newline at end of file
356diff --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
357new file mode 100644
358index 0000000..c6deff3
359--- /dev/null
360+++ b/tests/test_net_if_management_data/RPI3B_UC16_CLOUDINIT_netplan.yaml
361@@ -0,0 +1,13 @@
362+# This file is generated from information provided by
363+# the datasource. Changes to it will not persist across an instance.
364+# To disable cloud-init's network configuration capabilities, write a file
365+# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
366+# network: {config: disabled}
367+network:
368+ version: 2
369+ ethernets:
370+ eth0:
371+ dhcp4: true
372+ match:
373+ macaddress: 00:00:00:00:00:00
374+ set-name: eth0
375\ No newline at end of file
376diff --git a/tests/test_net_if_management_data/XENIAL_DESKTOP_nmcli.txt b/tests/test_net_if_management_data/XENIAL_DESKTOP_nmcli.txt
377new file mode 100644
378index 0000000..71105db
379--- /dev/null
380+++ b/tests/test_net_if_management_data/XENIAL_DESKTOP_nmcli.txt
381@@ -0,0 +1,3 @@
382+eth0:connected
383+wlan0:disconnected
384+lo:unmanaged
385\ No newline at end of file

Subscribers

People subscribed via source and target branches