Merge ~ltrager/maas:lp1888021 into maas:master

Proposed by Lee Trager
Status: Merged
Approved by: Adam Collard
Approved revision: 5ee39f8396cbb7f1ac26e2818cbbb7c403a131de
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:lp1888021
Merge into: maas:master
Diff against target: 303 lines (+129/-18)
3 files modified
src/machine-resources/src/machine-resources/Gopkg.lock (+3/-3)
src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py (+55/-5)
src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py (+71/-10)
Reviewer Review Type Date Requested Status
Alberto Donato (community) Approve
MAAS Lander Approve
Review via email: mp+388725@code.launchpad.net

Commit message

LP: #1888021 - Read storage MODEL and MODEL_ENC from udev

Storage devices may have two model names. One is encoded to allow spaces
and other special characters, the other is not. lsblk in Xenial and Bionic
give the encoded model name while in Focal it gives the non-encode model
name. LXD gives the encoded model name in LXD 4.3+. Read and store both
from udev to ensure matching works.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b lp1888021 lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 5ee39f8396cbb7f1ac26e2818cbbb7c403a131de

review: Approve
Revision history for this message
Alberto Donato (ack) wrote :

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/machine-resources/src/machine-resources/Gopkg.lock b/src/machine-resources/src/machine-resources/Gopkg.lock
2index 52abaeb..fbd28e4 100644
3--- a/src/machine-resources/src/machine-resources/Gopkg.lock
4+++ b/src/machine-resources/src/machine-resources/Gopkg.lock
5@@ -22,8 +22,8 @@
6 [[projects]]
7 branch = "master"
8 name = "github.com/lxc/lxd"
9- packages = ["lxd/resources","shared","shared/api","shared/cancel","shared/ioprogress","shared/logger","shared/osarch","shared/units","shared/usbid","shared/version"]
10- revision = "44487eda6756305828ea8aaf7555225f61d1d292"
11+ packages = ["lxd/include","lxd/resources","shared","shared/api","shared/cancel","shared/ioprogress","shared/logger","shared/osarch","shared/units","shared/usbid","shared/validate","shared/version"]
12+ revision = "4c7cd25c6842f49f3a29468aa745ee97651d42bd"
13
14 [[projects]]
15 name = "github.com/mitchellh/go-homedir"
16@@ -41,7 +41,7 @@
17 branch = "master"
18 name = "golang.org/x/sys"
19 packages = ["internal/unsafeheader","unix","windows"]
20- revision = "ddb9806d33aed8dbaac1cd6f1cba58952e87f933"
21+ revision = "0cf7623e9dbd9bc79267b27a7e6c5757ef550423"
22
23 [[projects]]
24 branch = "v2"
25diff --git a/src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py b/src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py
26index 68819e4..ea9def3 100755
27--- a/src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py
28+++ b/src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py
29@@ -5,7 +5,7 @@
30 #
31 # Author: Lee Trager <lee.trager@canonical.com>
32 #
33-# Copyright (C) 2017-2019 Canonical
34+# Copyright (C) 2017-2020 Canonical
35 #
36 # This program is free software: you can redistribute it and/or modify
37 # it under the terms of the GNU Affero General Public License as
38@@ -582,6 +582,54 @@ class CustomNetworking:
39 self._apply_ephemeral_netplan()
40
41
42+def udev_decode(string):
43+ """Decode udev encoded values
44+
45+ Inverse of
46+ https://github.com/systemd/systemd/blob/master/src/basic/device-nodes.c#L22
47+ Go version in LXD
48+ https://github.com/lxc/lxd/blob/master/lxd/resources/utils.go#L106
49+ """
50+ ret = ""
51+ i = 0
52+ while i < len(string):
53+ if string[i] == "\\" and i + 4 < len(string) and string[i + 1] == "x":
54+ ret += chr(int(string[i + 2 : i + 4], 16))
55+ i += 4
56+ else:
57+ ret += string[i]
58+ i += 1
59+ return ret.strip()
60+
61+
62+def get_storage_model_from_udev(block_dev):
63+ """Get the storage model name from udev
64+
65+ Storage devices may have two model names. One is encoded to allow
66+ spaces and other special characters, the other is not. lsblk in Xenial
67+ and Bionic give the encoded model name while in Focal it gives the
68+ non-encoded model name. LXD gives the encoded model name in LXD 4.3+. Read
69+ and store both from udev to ensure matching works.
70+ """
71+ udev_path = "/run/udev/data/b%s" % block_dev["MAJ:MIN"]
72+ if not os.path.exists(udev_path):
73+ # udev should always be available, if udev_path isn't found something
74+ # changed. lsblk will still give a model name, try using that.
75+ sys.stderr.write(
76+ "WARNING: Unable to read block device data from "
77+ "udev(%s)" % udev_path
78+ )
79+ block_dev["MODEL_ENC"] = block_dev["MODEL"]
80+ return block_dev
81+ with open(udev_path, "r") as f:
82+ for line in f.readlines():
83+ if line.startswith("E:ID_MODEL_ENC"):
84+ block_dev["MODEL_ENC"] = udev_decode(line.split("=", 1)[1])
85+ elif line.startswith("E:ID_MODEL"):
86+ block_dev["MODEL"] = line.split("=", 1)[1].strip()
87+ return block_dev
88+
89+
90 # Cache the block devices so we only have to query once.
91 _block_devices = None
92 _block_devices_lock = Lock()
93@@ -604,7 +652,7 @@ def get_block_devices():
94 "-d",
95 "-P",
96 "-o",
97- "NAME,MODEL,SERIAL",
98+ "NAME,MODEL,SERIAL,MAJ:MIN",
99 ]
100 block_list = check_output(cmd, timeout=60).decode("utf-8")
101 except TimeoutExpired:
102@@ -626,7 +674,9 @@ def get_block_devices():
103 for token in tokens:
104 k, v = token.split("=", 1)
105 current_block[k] = v.strip()
106- block_devices.append(current_block)
107+ block_devices.append(
108+ get_storage_model_from_udev(current_block)
109+ )
110 # LP:1732539 - Don't fill cache until all results are proceeded.
111 _block_devices = block_devices
112
113@@ -739,8 +789,8 @@ def parse_parameters(script, scripts_dir):
114 for blockdev in get_block_devices():
115 if (
116 model == blockdev["MODEL"]
117- and serial == blockdev["SERIAL"]
118- ):
119+ or model == blockdev.get("MODEL_ENC")
120+ ) and serial == blockdev["SERIAL"]:
121 value["path"] = value["input"] = (
122 "/dev/%s" % blockdev["NAME"]
123 )
124diff --git a/src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py b/src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py
125index e322ca3..5a6940b 100644
126--- a/src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py
127+++ b/src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py
128@@ -1,4 +1,4 @@
129-# Copyright 2017-2019 Canonical Ltd. This software is licensed under the
130+# Copyright 2017-2020 Canonical Ltd. This software is licensed under the
131 # GNU Affero General Public License version 3 (see the file LICENSE).
132
133 """Tests for maas_run_remote_scripts.py."""
134@@ -15,8 +15,9 @@ import random
135 import stat
136 from subprocess import CalledProcessError, DEVNULL, PIPE, TimeoutExpired
137 import tarfile
138+from textwrap import dedent
139 import time
140-from unittest.mock import ANY, call, MagicMock
141+from unittest.mock import ANY, call, MagicMock, mock_open
142 from zipfile import ZipFile
143
144 import yaml
145@@ -39,6 +40,7 @@ from snippets.maas_run_remote_scripts import (
146 download_and_extract_tar,
147 get_block_devices,
148 get_interfaces,
149+ get_storage_model_from_udev,
150 install_dependencies,
151 output_and_send,
152 output_and_send_scripts,
153@@ -47,6 +49,7 @@ from snippets.maas_run_remote_scripts import (
154 run_script,
155 run_scripts,
156 run_scripts_from_metadata,
157+ udev_decode,
158 )
159
160 # Unused ScriptResult id, used to make sure number is always unique.
161@@ -1581,19 +1584,25 @@ class TestParseParameters(MAASTestCase):
162 maas_run_remote_scripts._block_devices = None
163
164 def test_get_block_devices(self):
165+ mock_get_storage_model_from_udev = self.patch(
166+ maas_run_remote_scripts, "get_storage_model_from_udev"
167+ )
168+ mock_get_storage_model_from_udev.side_effect = lambda bd: bd
169 expected_blockdevs = [
170 {
171 "NAME": factory.make_name("NAME"),
172 "MODEL": factory.make_name("MODEL"),
173 "SERIAL": factory.make_name("SERIAL"),
174+ "MAJ:MIN": factory.make_name("MAJ:MIN"),
175 }
176 for _ in range(3)
177 ]
178 mock_check_output = self.patch(maas_run_remote_scripts, "check_output")
179 mock_check_output.return_value = "".join(
180 [
181- 'NAME="{NAME}" MODEL="{MODEL}" SERIAL="{SERIAL}"\n'.format(
182- **blockdev
183+ 'NAME="{NAME}" MODEL="{MODEL}" SERIAL="{SERIAL}" '
184+ 'MAJ:MIN="{MAJ_MIN}"\n'.format(
185+ **blockdev, MAJ_MIN=blockdev["MAJ:MIN"]
186 )
187 for blockdev in expected_blockdevs
188 ]
189@@ -1601,6 +1610,7 @@ class TestParseParameters(MAASTestCase):
190 maas_run_remote_scripts._block_devices = None
191
192 self.assertItemsEqual(expected_blockdevs, get_block_devices())
193+ self.assertTrue(mock_get_storage_model_from_udev.called)
194
195 def test_get_block_devices_cached(self):
196 block_devices = factory.make_name("block_devices")
197@@ -1633,6 +1643,49 @@ class TestParseParameters(MAASTestCase):
198
199 self.assertRaises(KeyError, get_block_devices)
200
201+ def test_udev_decode(self):
202+ self.assertEquals(
203+ "QEMU HARDDISK", udev_decode("QEMU\x20HARDDISK\x20\x20\x20")
204+ )
205+
206+ def test_get_storage_model_from_udev(self):
207+ self.patch(
208+ maas_run_remote_scripts.os.path, "exists"
209+ ).return_value = True
210+ self.patch(
211+ maas_run_remote_scripts,
212+ "open",
213+ mock_open(
214+ read_data=dedent(
215+ """\
216+ S:disk/by-id/scsi-SATA_QEMU_HARDDISK_QM00001
217+ E:ID_MODEL=QEMU_HARDDISK
218+ E:ID_MODEL_ENC=QEMU\x20HARDDISK\x20\x20\x20
219+ """
220+ )
221+ ),
222+ )
223+ self.assertDictEqual(
224+ {
225+ "MAJ:MIN": "8:0",
226+ "MODEL": "QEMU_HARDDISK",
227+ "MODEL_ENC": "QEMU HARDDISK",
228+ },
229+ get_storage_model_from_udev({"MAJ:MIN": "8:0"}),
230+ )
231+
232+ def test_get_storage_model_from_udev_warns(self):
233+ mock_stderr_write = self.patch(
234+ maas_run_remote_scripts.sys.stderr, "write"
235+ )
236+ model = factory.make_name("model")
237+ block_dev = {"MAJ:MIN": factory.make_name("MAJ:MIN"), "MODEL": model}
238+ self.assertDictEqual(
239+ {"MODEL_ENC": model, **block_dev},
240+ get_storage_model_from_udev(block_dev),
241+ )
242+ self.assertThat(mock_stderr_write, MockCalledOnce())
243+
244 def test_get_interfaces(self):
245 maas_run_remote_scripts._interfaces = None
246 netplan_dir = self.useFixture(TempDirectory()).path
247@@ -1728,6 +1781,10 @@ class TestParseParameters(MAASTestCase):
248 self.assertThat(mock_listdir, MockNotCalled())
249
250 def test_parse_parameters(self):
251+ mock_get_storage_model_from_udev = self.patch(
252+ maas_run_remote_scripts, "get_storage_model_from_udev"
253+ )
254+ mock_get_storage_model_from_udev.side_effect = lambda bd: bd
255 scripts_dir = factory.make_name("scripts_dir")
256 script = {
257 "path": os.path.join("path_to", factory.make_name("script_name")),
258@@ -1778,9 +1835,8 @@ class TestParseParameters(MAASTestCase):
259 mock_check_output = self.patch(maas_run_remote_scripts, "check_output")
260 mock_check_output.return_value = "".join(
261 [
262- 'NAME="{name}" MODEL="{model}" SERIAL="{serial}"\n'.format(
263- **param["value"]
264- )
265+ 'NAME="{name}" MODEL="{model}" SERIAL="{serial}" '
266+ 'MAJ_MIN="8:0"\n'.format(**param["value"])
267 for param_name, param in script["parameters"].items()
268 if "storage" in param_name
269 ]
270@@ -1808,8 +1864,13 @@ class TestParseParameters(MAASTestCase):
271 ],
272 parse_parameters(script, scripts_dir),
273 )
274+ self.assertTrue(mock_get_storage_model_from_udev.called)
275
276 def test_parse_parameters_argument_format(self):
277+ mock_get_storage_model_from_udev = self.patch(
278+ maas_run_remote_scripts, "get_storage_model_from_udev"
279+ )
280+ mock_get_storage_model_from_udev.side_effect = lambda bd: bd
281 scripts_dir = factory.make_name("scripts_dir")
282 script = {
283 "path": os.path.join("path_to", factory.make_name("script_name")),
284@@ -1853,9 +1914,8 @@ class TestParseParameters(MAASTestCase):
285 mock_check_output = self.patch(maas_run_remote_scripts, "check_output")
286 mock_check_output.return_value = "".join(
287 [
288- 'NAME="{name}" MODEL="{model}" SERIAL="{serial}"\n'.format(
289- **param["value"]
290- )
291+ 'NAME="{name}" MODEL="{model}" SERIAL="{serial}" '
292+ 'MAJ:MIN="8:0"\n'.format(**param["value"])
293 for param_name, param in script["parameters"].items()
294 if "storage" in param_name
295 ]
296@@ -1889,6 +1949,7 @@ class TestParseParameters(MAASTestCase):
297 ],
298 parse_parameters(script, scripts_dir),
299 )
300+ self.assertTrue(mock_get_storage_model_from_udev.called)
301
302 def test_parse_parameters_storage_value_all_raises_keyerror(self):
303 scripts_dir = factory.make_name("scripts_dir")

Subscribers

People subscribed via source and target branches