Merge ~jawn-smith/ubuntu/+source/ubuntu-release-upgrader:ubuntu/jammy into ubuntu/+source/ubuntu-release-upgrader:ubuntu/jammy
- Git
- lp:~jawn-smith/ubuntu/+source/ubuntu-release-upgrader
- ubuntu/jammy
- Merge into ubuntu/jammy
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) |
Related bugs: |
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.
Description of the change
- e7a2db9... by William Wilson
-
Improve boundary checking to avoid out of bounds error
- 2fee82f... by William Wilson
-
Define failure_action string to be printed
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-
* 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)!
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.
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.
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.
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://
python3-yaml 5.3.1-1ubuntu0.1
$ curl -s http://
python3-yaml 5.3.1-1ubuntu0.1
$ curl -s http://
python3-yaml 5.3.1-1
$ curl -s http://
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.
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
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
Dave Jones (waveform) wrote : | # |
Excellent, looking good now (and no, there's nothing wrong with KeyError: continue :)
- 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
1 | diff --git a/DistUpgrade/DistUpgradeQuirks.py b/DistUpgrade/DistUpgradeQuirks.py |
2 | index 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 |
76 | diff --git a/debian/changelog b/debian/changelog |
77 | index 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). |
92 | diff --git a/tests/test_quirks.py b/tests/test_quirks.py |
93 | index 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 |
See my in-line comment.