Merge ~jawn-smith/ubuntu/+source/ubuntu-release-upgrader:ubuntu/jammy into ubuntu/+source/ubuntu-release-upgrader:ubuntu/jammy

Proposed by William Wilson
Status: Needs review
Proposed branch: ~jawn-smith/ubuntu/+source/ubuntu-release-upgrader:ubuntu/jammy
Merge into: ubuntu/+source/ubuntu-release-upgrader:ubuntu/jammy
Diff against target: 386 lines (+346/-0)
3 files modified
DistUpgrade/DistUpgradeQuirks.py (+60/-0)
debian/changelog (+8/-0)
tests/test_quirks.py (+278/-0)
Reviewer Review Type Date Requested Status
Dave Jones (community) Approve
Brian Murray (community) Needs Fixing
Review via email: mp+421557@code.launchpad.net

Commit message

If upgrading to jammy on a Raspberry Pi, check for the presence of the broken "match" section of the yaml config. This was present in the default config for Raspberry Pi images dating back to focal.

To post a comment you must log in.
e7a2db9... by William Wilson

Improve boundary checking to avoid out of bounds error

Revision history for this message
Brian Murray (brian-murray) wrote :

See my in-line comment.

review: Needs Fixing
2fee82f... by William Wilson

Define failure_action string to be printed

Revision history for this message
Dave Jones (waveform) wrote :

I'm afraid there's a few issues:

* I'd much rather parse the YAML with a yaml library given there's so many possible representations here (which cloud-init is free to use when it converts network-config to the netplan config). For example, match: {driver: "bcmgenet smsc95xx lan78xx"} is valid YAML. I know adding dependencies is generally frowned upon, but python3-yaml is seeded on the pi server and desktop images (presumably because cloud-init and ubuntu-advantage-tools both depend on it, among many other things) so adding it as a dep of u-r-u won't *actually* pull in anything new, would simplify this code, and deal with all possible variants of the configuration.

* If you remove the match/driver: clause, the set-name: clause becomes invalid (it requires a match: clause). In this case it's probably sufficient to just remove the set-name: clause along with the match: clause.

* Ideally the config_txt variable in the various tests should be renamed; it's not the config.txt on the boot partition that's being modified (unlike the prior tests concerned with removing u-boot), it's the netplan yaml.

Other than that: nice to see a good set of tests, covering all the netplan config scenarios (match present, not present, present but only in certain files)!

review: Needs Fixing
Revision history for this message
William Wilson (jawn-smith) wrote :

Ah I had thought about using python3-yaml but didn't want to add dependencies to the upgrader, which I figured could be problematic. I didn't check if it was seeded though.

Revision history for this message
Nick Rosbrook (enr0n) wrote :

If you are going to add python3-yaml as a dependency in jammy, then it should either (1) be SRU'd to focal and impish, or (2) there should be logic to handle a potential import exception.

This recently came up in bug 1965568, i.e. I learned this the hard way.

Revision history for this message
Brian Murray (brian-murray) wrote :

> If you are going to add python3-yaml as a dependency in jammy, then it should
> either (1) be SRU'd to focal and impish, or (2) there should be logic to
> handle a potential import exception.
>
> This recently came up in bug 1965568, i.e. I learned this the hard way.

Option 1 would result in there be two SRUs. One from the release being upgraded from and one for the release being upgraded to.

Revision history for this message
Dave Jones (waveform) wrote :

If it were a genuinely new dependency I would indeed worry, but I'm *fairly* sure python3-yaml's been seeded since forever (or since focal at least, which is the earliest we need to concern ourselves with as upgrades never span more than one LTS). Just checking ...

$ curl -s http://cdimage.ubuntu.com/ubuntu/releases/focal/release/ubuntu-20.04.4-preinstalled-server-arm64+raspi.manifest | grep python3-yaml
python3-yaml 5.3.1-1ubuntu0.1
$ curl -s http://cdimage.ubuntu.com/ubuntu/releases/focal/release/ubuntu-20.04.4-preinstalled-server-armhf+raspi.manifest | grep python3-yaml
python3-yaml 5.3.1-1ubuntu0.1
$ curl -s http://old-releases.ubuntu.com/releases/20.04.0/ubuntu-20.04-desktop-amd64.manifest | grep python3-yaml
python3-yaml 5.3.1-1
$ curl -s http://old-releases.ubuntu.com/releases/20.04.0/ubuntu-20.04-live-server-amd64.manifest | grep python3-yaml
python3-yaml 5.3.1-1

Yup, python3-yaml was seeded at least as far back as focal on PC server, PC desktop, and pi server images. So, technically yes, an SRU *should* be done but I wouldn't worry greatly in this particular case as python3-yaml should be universally installed.

Revision history for this message
Nick Rosbrook (enr0n) wrote :

> Yup, python3-yaml was seeded at least as far back as focal on PC server, PC
> desktop, and pi server images. So, technically yes, an SRU *should* be done
> but I wouldn't worry greatly in this particular case as python3-yaml should be
> universally installed.

Right, then option (2) is probably more appropriate in this case. William's original patch already has a helpful `failure_action` string, so this should be displayed to the user if for some reason `import yaml` raises an exception.

4f2b990... by William Wilson

Use python yaml library instead of parsing text line by line

61b3a82... by William Wilson

change os.rename to a file write to avoid missing configs

Revision history for this message
Dave Jones (waveform) wrote :

Some inline comments; one thing needs changing (orig_yaml being a ref to netplan_yaml) but otherwise looking much better!

c44644f... by William Wilson

Ensure backups are correct and have comments preserved

Revision history for this message
Dave Jones (waveform) wrote :

Excellent, looking good now (and no, there's nothing wrong with KeyError: continue :)

review: Approve
1e210e1... by William Wilson

Remove extra whitespace

Unmerged commits

1e210e1... by William Wilson

Remove extra whitespace

c44644f... by William Wilson

Ensure backups are correct and have comments preserved

61b3a82... by William Wilson

change os.rename to a file write to avoid missing configs

4f2b990... by William Wilson

Use python yaml library instead of parsing text line by line

2fee82f... by William Wilson

Define failure_action string to be printed

e7a2db9... by William Wilson

Improve boundary checking to avoid out of bounds error

4880202... by William Wilson

Remove invalid match section from default Raspberry Pi netplan config after upgrades to jammy

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/DistUpgrade/DistUpgradeQuirks.py b/DistUpgrade/DistUpgradeQuirks.py
2index ccbb117..c6be753 100644
3--- a/DistUpgrade/DistUpgradeQuirks.py
4+++ b/DistUpgrade/DistUpgradeQuirks.py
5@@ -151,6 +151,7 @@ class DistUpgradeQuirks(object):
6 if 'linux-firmware-raspi2' in cache:
7 if cache['linux-firmware-raspi2'].is_installed:
8 self._remove_uboot_on_rpi()
9+ self._remove_netplan_match_rpi()
10
11 # individual quirks handler when the dpkg run is finished ---------
12 def PostCleanup(self):
13@@ -1377,3 +1378,62 @@ class DistUpgradeQuirks(object):
14 except IOError as exc:
15 logging.error("unable to write new boot config to %s: %s; %s",
16 boot_config_filename, exc, failure_action)
17+
18+ def _remove_netplan_match_rpi(self, netplan_dir='/etc/netplan'):
19+ failure_action = (
20+ "You may need to manually edit your netplan config "
21+ "and remove the line \"driver: bcmgenet smsc95xx lan78xx\". "
22+ "See LP: #1970761 for further details")
23+
24+ try:
25+ import yaml
26+ except ImportError:
27+ logging.error("failed to import yaml library. %s", failure_action)
28+ return
29+
30+ for netplan_config_file in glob.glob("%s/*.yaml" % netplan_dir):
31+ try:
32+ netplan_config_filename = os.path.join(netplan_dir,
33+ netplan_config_file)
34+ with open(netplan_config_filename, 'r', encoding='utf-8') as f:
35+ orig_content = f.read()
36+ netplan_yaml = yaml.load(orig_content,
37+ Loader=yaml.SafeLoader)
38+
39+ except FileNotFoundError:
40+ logging.error("failed to open netplan configuration in %s; %s",
41+ netplan_config_filename, failure_action)
42+ return
43+
44+ file_modified = False
45+ try:
46+ for eth in netplan_yaml["network"]["ethernets"].values():
47+ if eth["match"]["driver"] == "bcmgenet smsc95xx lan78xx":
48+ file_modified = True
49+ del eth["match"]
50+ if "set-name" in eth:
51+ del eth["set-name"]
52+ except KeyError:
53+ continue
54+
55+ if not file_modified:
56+ continue
57+
58+ try:
59+ netplan_backup_filename = (netplan_config_filename +
60+ '.distUpgrade')
61+ with open(netplan_backup_filename, 'w', encoding='utf-8') as f:
62+ f.write(orig_content)
63+ except IOError as exc:
64+ logging.error("unable to write netplan config backup to "
65+ "%s: %s; %s",
66+ netplan_backup_filename, exc, failure_action)
67+ return
68+ try:
69+ with open(netplan_config_filename, 'w', encoding='utf-8') as f:
70+ yaml.dump(netplan_yaml, f, default_flow_style=False)
71+ except IOError as exc:
72+ logging.error("unable to write new netplan config to "
73+ "%s: %s; %s",
74+ netplan_config_filename, exc, failure_action)
75+ return
76diff --git a/debian/changelog b/debian/changelog
77index ac95dd1..e073f5d 100644
78--- a/debian/changelog
79+++ b/debian/changelog
80@@ -1,3 +1,11 @@
81+ubuntu-release-upgrader (1:22.04.11) jammy; urgency=medium
82+
83+ * DistUpgrade/DistUpgradeQuirks.py: Remove section of netplan
84+ config that is installed by default on Raspberry Pi and
85+ causes netplan failures after upgrade to jammy (LP: #1970761)
86+
87+ -- William 'jawn-smith' Wilson <jawn-smith@ubuntu.com> Tue, 03 May 2022 12:54:45 -0500
88+
89 ubuntu-release-upgrader (1:22.04.10) jammy; urgency=medium
90
91 * DistUpgrade: Set NEEDRESTART_SUSPEND during upgrades (LP: #1968919).
92diff --git a/tests/test_quirks.py b/tests/test_quirks.py
93index 09b0160..eedf8f1 100644
94--- a/tests/test_quirks.py
95+++ b/tests/test_quirks.py
96@@ -10,6 +10,7 @@ import unittest
97 import shutil
98 import tempfile
99 import json
100+import yaml
101
102 from DistUpgrade.DistUpgradeQuirks import DistUpgradeQuirks
103
104@@ -721,6 +722,283 @@ initramfs initrd.img followkernel
105 with open(os.path.join(boot_dir, 'config.txt')) as f:
106 self.assertTrue(f.read() == expected_config_txt)
107
108+ # This test case tests the scenario where the invalid
109+ # match section is present and needs to be removed
110+ def test_remove_netplan_match_single_removal(self):
111+ with tempfile.TemporaryDirectory() as netplan_dir:
112+ config_yaml = """\
113+network:
114+ ethernets:
115+ # this is a test comment to ensure it is preserved in the backup
116+ eth0:
117+ dhcp4: true
118+ # this section will be deleted
119+ match:
120+ driver: bcmgenet smsc95xx lan78xx
121+ name: en*
122+ optional: true
123+ set-name: eth0
124+ wifis:
125+ wlan0:
126+ dhcp4: true
127+ access-points:
128+ "TestWifi":
129+ password: "TestPassword"
130+ version: 2
131+"""
132+ expected_config_yaml = """\
133+network:
134+ ethernets:
135+ eth0:
136+ dhcp4: true
137+ optional: true
138+ wifis:
139+ wlan0:
140+ dhcp4: true
141+ access-points:
142+ "TestWifi":
143+ password: "TestPassword"
144+ version: 2
145+"""
146+ config_filename = '99-test-netplan.yaml'
147+ with open(os.path.join(netplan_dir, config_filename), 'w') as f:
148+ f.write(config_yaml)
149+
150+ mock_controller = mock.Mock()
151+ q = DistUpgradeQuirks(mock_controller, mock.Mock())
152+ q._remove_netplan_match_rpi(netplan_dir)
153+
154+ # make sure the backup exists and is the same as the original
155+ backup_config_filename = config_filename + '.distUpgrade'
156+ self.assertTrue(os.path.exists(os.path.join(
157+ netplan_dir, backup_config_filename)))
158+ with open(os.path.join(netplan_dir,
159+ backup_config_filename)) as f:
160+ self.assertTrue(f.read() == config_yaml)
161+
162+ # make sure the new contents are what we expect
163+ with open(os.path.join(netplan_dir, config_filename)) as f:
164+ new_config = yaml.load(f, yaml.SafeLoader)
165+ expected_config = yaml.load(expected_config_yaml,
166+ yaml.SafeLoader)
167+ self.assertTrue(new_config == expected_config)
168+
169+ # This test case tests the scenario where the match
170+ # section is present but is valid syntax
171+ def test_remove_netplan_match_valid_syntax(self):
172+ with tempfile.TemporaryDirectory() as netplan_dir:
173+ config_yaml = """\
174+network:
175+ ethernets:
176+ eth0:
177+ dhcp4: true
178+ match:
179+ # this is a test comment to ensure it is preserved in the backup
180+ driver: ["bcmgenet", "smsc95xx", "lan78xx"]
181+ optional: true
182+ set-name: eth0
183+ wifis:
184+ wlan0:
185+ dhcp4: true
186+ access-points:
187+ "TestWifi":
188+ password: "TestPassword"
189+ version: 2
190+"""
191+
192+ mock_controller = mock.Mock()
193+ q = DistUpgradeQuirks(mock_controller, mock.Mock())
194+ q._remove_netplan_match_rpi(netplan_dir)
195+
196+ self.assertFalse(os.path.exists(os.path.join(
197+ netplan_dir, '50-cloud-init.yaml.distUpgrade')))
198+
199+ # This test case tests the scenario where the match
200+ # section not present at all
201+ def test_remove_netplan_match_no_match(self):
202+ with tempfile.TemporaryDirectory() as netplan_dir:
203+ config_yaml = """\
204+network:
205+ ethernets:
206+ eth0:
207+ dhcp4: true
208+ optional: true
209+ wifis:
210+ wlan0:
211+ dhcp4: true
212+ access-points:
213+ "TestWifi":
214+ password: "TestPassword"
215+ version: 2
216+"""
217+
218+ mock_controller = mock.Mock()
219+ q = DistUpgradeQuirks(mock_controller, mock.Mock())
220+ q._remove_netplan_match_rpi(netplan_dir)
221+
222+ self.assertFalse(os.path.exists(os.path.join(
223+ netplan_dir, '50-cloud-init.yaml.distUpgrade')))
224+
225+ # This test case tests the scenario where the invalid
226+ # match section is present in one file, but multiple
227+ # config files exist
228+ def test_remove_netplan_match_one_of_multiple(self):
229+ with tempfile.TemporaryDirectory() as netplan_dir:
230+ ethernet_config_yaml = """\
231+############
232+# also
233+# test
234+# multi
235+# line
236+# comments
237+# are
238+# preserved
239+############
240+network:
241+ ethernets:
242+ eth0:
243+ dhcp4: true
244+ match:
245+ driver: bcmgenet smsc95xx lan78xx
246+ optional: true
247+ set-name: eth0
248+ version: 2
249+"""
250+ wifi_config_yaml = """\
251+network:
252+ wifis:
253+ wlan0:
254+ dhcp4: true
255+ access-points:
256+ "TestWifi":
257+ password: "TestPassword"
258+ version: 2
259+"""
260+ expected_ethernet_config_yaml = """\
261+network:
262+ ethernets:
263+ eth0:
264+ dhcp4: true
265+ optional: true
266+ version: 2
267+"""
268+ e_config_filename = '01-test-ethernet.yaml'
269+ with open(os.path.join(netplan_dir, e_config_filename), 'w') as f:
270+ f.write(ethernet_config_yaml)
271+
272+ w_config_filename = '02-test-wifi.yaml'
273+ with open(os.path.join(netplan_dir, w_config_filename), 'w') as f:
274+ f.write(wifi_config_yaml)
275+
276+ mock_controller = mock.Mock()
277+ q = DistUpgradeQuirks(mock_controller, mock.Mock())
278+ q._remove_netplan_match_rpi(netplan_dir)
279+
280+ # make sure the backup exists and is the same as the original
281+ backup_config_filename = e_config_filename + '.distUpgrade'
282+ self.assertTrue(os.path.exists(os.path.join(
283+ netplan_dir, backup_config_filename)))
284+ with open(os.path.join(netplan_dir,
285+ backup_config_filename)) as f:
286+ self.assertTrue(f.read() == ethernet_config_yaml)
287+
288+ # make sure the new contents are what we expect
289+ with open(os.path.join(netplan_dir, e_config_filename)) as f:
290+ new_config = yaml.load(f, yaml.SafeLoader)
291+ expected_config = yaml.load(expected_ethernet_config_yaml,
292+ yaml.SafeLoader)
293+ self.assertTrue(new_config == expected_config)
294+
295+ # make sure a backup wasn't created for the config
296+ # that doesn't need modification
297+ self.assertFalse(os.path.exists(os.path.join(
298+ netplan_dir, w_config_filename + '.distUpgrade')))
299+
300+ # This test case tests the scenario where the invalid
301+ # match section is present in multiple files
302+ def test_remove_netplan_match_multiple(self):
303+ with tempfile.TemporaryDirectory() as netplan_dir:
304+ eth0_config_yaml = """\
305+# comments can go at the top
306+network:
307+ ethernets:
308+ eth0:
309+ dhcp4: true
310+ match:
311+ driver: bcmgenet smsc95xx lan78xx
312+ optional: true
313+ set-name: eth0
314+ version: 2
315+"""
316+ eth1_config_yaml = """\
317+network:
318+ ethernets:
319+ eth1:
320+ dhcp4: true
321+ match:
322+ driver: bcmgenet smsc95xx lan78xx
323+ version: 2
324+# comments can go at the bottom too
325+"""
326+ expected_eth0_config_yaml = """\
327+network:
328+ ethernets:
329+ eth0:
330+ dhcp4: true
331+ optional: true
332+ version: 2
333+"""
334+ expected_eth1_config_yaml = """\
335+network:
336+ ethernets:
337+ eth1:
338+ dhcp4: true
339+ version: 2
340+"""
341+ eth0_config_filename = '01-test-eth0.yaml'
342+ with open(os.path.join(netplan_dir,
343+ eth0_config_filename), 'w') as f:
344+ f.write(eth0_config_yaml)
345+
346+ eth1_config_filename = '02-test-eth1.yaml'
347+ with open(os.path.join(netplan_dir,
348+ eth1_config_filename), 'w') as f:
349+ f.write(eth1_config_yaml)
350+
351+ mock_controller = mock.Mock()
352+ q = DistUpgradeQuirks(mock_controller, mock.Mock())
353+ q._remove_netplan_match_rpi(netplan_dir)
354+
355+ # make sure the backup exists and is the same as the original
356+ backup_eth0_filename = eth0_config_filename + '.distUpgrade'
357+ self.assertTrue(os.path.exists(os.path.join(
358+ netplan_dir, backup_eth0_filename)))
359+ with open(os.path.join(netplan_dir,
360+ backup_eth0_filename)) as f:
361+ self.assertTrue(f.read() == eth0_config_yaml)
362+
363+ # make sure the modified contents are what we expect
364+ with open(os.path.join(netplan_dir, eth0_config_filename)) as f:
365+ new_config = yaml.load(f, yaml.SafeLoader)
366+ expected_config = yaml.load(expected_eth0_config_yaml,
367+ yaml.SafeLoader)
368+ self.assertTrue(new_config == expected_config)
369+
370+ # make sure the backup exists and is the same as the original
371+ backup_eth1_filename = eth1_config_filename + '.distUpgrade'
372+ self.assertTrue(os.path.exists(os.path.join(
373+ netplan_dir, backup_eth1_filename)))
374+ with open(os.path.join(netplan_dir,
375+ backup_eth1_filename)) as f:
376+ self.assertTrue(f.read() == eth1_config_yaml)
377+
378+ # make sure the modified contents are what we expect
379+ with open(os.path.join(netplan_dir, eth1_config_filename)) as f:
380+ new_config = yaml.load(f, yaml.SafeLoader)
381+ expected_config = yaml.load(expected_eth1_config_yaml,
382+ yaml.SafeLoader)
383+ self.assertTrue(new_config == expected_config)
384+
385
386 class TestSnapQuirks(unittest.TestCase):
387

Subscribers

People subscribed via source and target branches