Merge ~chad.smith/cloud-init:fix-device-path-from-cmdline-regression into cloud-init:master
- Git
- lp:~chad.smith/cloud-init
- fix-device-path-from-cmdline-regression
- Merge into master
Status: | Merged |
---|---|
Approved by: | Scott Moser |
Approved revision: | 00fdcd665a11980b69b5c66c7e369e1df2630d1d |
Merged at revision: | 17a15f9e0ae78e4fc4e24fab0caebdf78f06ef66 |
Proposed branch: | ~chad.smith/cloud-init:fix-device-path-from-cmdline-regression |
Merge into: | cloud-init:master |
Diff against target: |
302 lines (+70/-64) 2 files modified
cloudinit/config/cc_resizefs.py (+13/-30) tests/unittests/test_handler/test_handler_resizefs.py (+57/-34) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Needs Fixing | |
Scott Moser | Approve | ||
Review via email: mp+332634@code.launchpad.net |
Commit message
resizefs: Fix regression when system booted with root=PARTUUID=
A recent cleanup of the resizefs module broke resizing when
a system was booted with root=PARTUUID=
does not exist. This path is exposed with the Ubuntu 16.04 but
not with Ubuntu 17.10. A recreate exists under bug 1684869.
LP: #1725067
Description of the change
Scott Moser (smoser) wrote : | # |
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:129beef23ad
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
SUCCESS: MAAS Compatability Testing
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
I tried the suggestion of using FileSystemMocki
http://
Where the idea became problematic was there is no simple way to make /dev/disk/
so at this point, it does seem like we should still patch os.stat , but doing so is
not related to your MP without further work.
- 6d58f22... by Chad Smith
-
drop rootdev_
from_cmdline duplicate definition from cc_resizefs as we use util.rootdev_ from_cmdline. Adjust unit tests to test util.rootdev_ from_cmdline instead of cc_resizefs. rootdev_ from_cmdline. Replace jsonschema in requirements.txt - 00fdcd6... by Chad Smith
-
add comment about using FilesystemMocking instead of os.stat mocks
Chad Smith (chad.smith) wrote : | # |
@smoser review comments addressed
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:445ee93379a
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
FAILED: MAAS Compatability Testing
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) : | # |
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:445ee93379a
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
FAILED: MAAS Compatability Testing
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:00fdcd665a1
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
SUCCESS: MAAS Compatability Testing
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:445ee93379a
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
FAILED: Ubuntu LTS: Integration
Click here to trigger a rebuild:
https:/
Preview Diff
1 | diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py |
2 | index f774baa..0d282e6 100644 |
3 | --- a/cloudinit/config/cc_resizefs.py |
4 | +++ b/cloudinit/config/cc_resizefs.py |
5 | @@ -145,25 +145,6 @@ RESIZE_FS_PRECHECK_CMDS = { |
6 | } |
7 | |
8 | |
9 | -def rootdev_from_cmdline(cmdline): |
10 | - found = None |
11 | - for tok in cmdline.split(): |
12 | - if tok.startswith("root="): |
13 | - found = tok[5:] |
14 | - break |
15 | - if found is None: |
16 | - return None |
17 | - |
18 | - if found.startswith("/dev/"): |
19 | - return found |
20 | - if found.startswith("LABEL="): |
21 | - return "/dev/disk/by-label/" + found[len("LABEL="):] |
22 | - if found.startswith("UUID="): |
23 | - return "/dev/disk/by-uuid/" + found[len("UUID="):] |
24 | - |
25 | - return "/dev/" + found |
26 | - |
27 | - |
28 | def can_skip_resize(fs_type, resize_what, devpth): |
29 | fstype_lc = fs_type.lower() |
30 | for i, func in RESIZE_FS_PRECHECK_CMDS.items(): |
31 | @@ -172,14 +153,15 @@ def can_skip_resize(fs_type, resize_what, devpth): |
32 | return False |
33 | |
34 | |
35 | -def is_device_path_writable_block(devpath, info, log): |
36 | - """Return True if devpath is a writable block device. |
37 | +def maybe_get_writable_device_path(devpath, info, log): |
38 | + """Return updated devpath if the devpath is a writable block device. |
39 | |
40 | - @param devpath: Path to the root device we want to resize. |
41 | + @param devpath: Requested path to the root device we want to resize. |
42 | @param info: String representing information about the requested device. |
43 | @param log: Logger to which logs will be added upon error. |
44 | |
45 | - @returns Boolean True if block device is writable |
46 | + @returns devpath or updated devpath per kernel commandline if the device |
47 | + path is a writable block device, returns None otherwise. |
48 | """ |
49 | container = util.is_container() |
50 | |
51 | @@ -189,12 +171,12 @@ def is_device_path_writable_block(devpath, info, log): |
52 | devpath = util.rootdev_from_cmdline(util.get_cmdline()) |
53 | if devpath is None: |
54 | log.warn("Unable to find device '/dev/root'") |
55 | - return False |
56 | + return None |
57 | log.debug("Converted /dev/root to '%s' per kernel cmdline", devpath) |
58 | |
59 | if devpath == 'overlayroot': |
60 | log.debug("Not attempting to resize devpath '%s': %s", devpath, info) |
61 | - return False |
62 | + return None |
63 | |
64 | try: |
65 | statret = os.stat(devpath) |
66 | @@ -207,7 +189,7 @@ def is_device_path_writable_block(devpath, info, log): |
67 | devpath, info) |
68 | else: |
69 | raise exc |
70 | - return False |
71 | + return None |
72 | |
73 | if not stat.S_ISBLK(statret.st_mode) and not stat.S_ISCHR(statret.st_mode): |
74 | if container: |
75 | @@ -216,8 +198,8 @@ def is_device_path_writable_block(devpath, info, log): |
76 | else: |
77 | log.warn("device '%s' not a block device. cannot resize: %s" % |
78 | (devpath, info)) |
79 | - return False |
80 | - return True |
81 | + return None |
82 | + return devpath # The writable block devpath |
83 | |
84 | |
85 | def handle(name, cfg, _cloud, log, args): |
86 | @@ -242,8 +224,9 @@ def handle(name, cfg, _cloud, log, args): |
87 | info = "dev=%s mnt_point=%s path=%s" % (devpth, mount_point, resize_what) |
88 | log.debug("resize_info: %s" % info) |
89 | |
90 | - if not is_device_path_writable_block(devpth, info, log): |
91 | - return |
92 | + devpth = maybe_get_writable_device_path(devpth, info, log) |
93 | + if not devpth: |
94 | + return # devpath was not a writable block device |
95 | |
96 | resizer = None |
97 | if can_skip_resize(fs_type, resize_what, devpth): |
98 | diff --git a/tests/unittests/test_handler/test_handler_resizefs.py b/tests/unittests/test_handler/test_handler_resizefs.py |
99 | index 3e5d436..29d5574 100644 |
100 | --- a/tests/unittests/test_handler/test_handler_resizefs.py |
101 | +++ b/tests/unittests/test_handler/test_handler_resizefs.py |
102 | @@ -1,9 +1,9 @@ |
103 | # This file is part of cloud-init. See LICENSE file for license information. |
104 | |
105 | from cloudinit.config.cc_resizefs import ( |
106 | - can_skip_resize, handle, is_device_path_writable_block, |
107 | - rootdev_from_cmdline) |
108 | + can_skip_resize, handle, maybe_get_writable_device_path) |
109 | |
110 | +from collections import namedtuple |
111 | import logging |
112 | import textwrap |
113 | |
114 | @@ -138,47 +138,48 @@ class TestRootDevFromCmdline(CiTestCase): |
115 | invalid_cases = [ |
116 | 'BOOT_IMAGE=/adsf asdfa werasef root adf', 'BOOT_IMAGE=/adsf', ''] |
117 | for case in invalid_cases: |
118 | - self.assertIsNone(rootdev_from_cmdline(case)) |
119 | + self.assertIsNone(util.rootdev_from_cmdline(case)) |
120 | |
121 | def test_rootdev_from_cmdline_with_root_startswith_dev(self): |
122 | """Return the cmdline root when the path starts with /dev.""" |
123 | self.assertEqual( |
124 | - '/dev/this', rootdev_from_cmdline('asdf root=/dev/this')) |
125 | + '/dev/this', util.rootdev_from_cmdline('asdf root=/dev/this')) |
126 | |
127 | def test_rootdev_from_cmdline_with_root_without_dev_prefix(self): |
128 | """Add /dev prefix to cmdline root when the path lacks the prefix.""" |
129 | - self.assertEqual('/dev/this', rootdev_from_cmdline('asdf root=this')) |
130 | + self.assertEqual( |
131 | + '/dev/this', util.rootdev_from_cmdline('asdf root=this')) |
132 | |
133 | def test_rootdev_from_cmdline_with_root_with_label(self): |
134 | """When cmdline root contains a LABEL, our root is disk/by-label.""" |
135 | self.assertEqual( |
136 | '/dev/disk/by-label/unique', |
137 | - rootdev_from_cmdline('asdf root=LABEL=unique')) |
138 | + util.rootdev_from_cmdline('asdf root=LABEL=unique')) |
139 | |
140 | def test_rootdev_from_cmdline_with_root_with_uuid(self): |
141 | """When cmdline root contains a UUID, our root is disk/by-uuid.""" |
142 | self.assertEqual( |
143 | '/dev/disk/by-uuid/adsfdsaf-adsf', |
144 | - rootdev_from_cmdline('asdf root=UUID=adsfdsaf-adsf')) |
145 | + util.rootdev_from_cmdline('asdf root=UUID=adsfdsaf-adsf')) |
146 | |
147 | |
148 | -class TestIsDevicePathWritableBlock(CiTestCase): |
149 | +class TestMaybeGetDevicePathAsWritableBlock(CiTestCase): |
150 | |
151 | with_logs = True |
152 | |
153 | - def test_is_device_path_writable_block_false_on_overlayroot(self): |
154 | + def test_maybe_get_writable_device_path_none_on_overlayroot(self): |
155 | """When devpath is overlayroot (on MAAS), is_dev_writable is False.""" |
156 | info = 'does not matter' |
157 | - is_writable = wrap_and_call( |
158 | + devpath = wrap_and_call( |
159 | 'cloudinit.config.cc_resizefs.util', |
160 | {'is_container': {'return_value': False}}, |
161 | - is_device_path_writable_block, 'overlayroot', info, LOG) |
162 | - self.assertFalse(is_writable) |
163 | + maybe_get_writable_device_path, 'overlayroot', info, LOG) |
164 | + self.assertIsNone(devpath) |
165 | self.assertIn( |
166 | "Not attempting to resize devpath 'overlayroot'", |
167 | self.logs.getvalue()) |
168 | |
169 | - def test_is_device_path_writable_block_warns_missing_cmdline_root(self): |
170 | + def test_maybe_get_writable_device_path_warns_missing_cmdline_root(self): |
171 | """When root does not exist isn't in the cmdline, log warning.""" |
172 | info = 'does not matter' |
173 | |
174 | @@ -190,43 +191,43 @@ class TestIsDevicePathWritableBlock(CiTestCase): |
175 | exists_mock_path = 'cloudinit.config.cc_resizefs.os.path.exists' |
176 | with mock.patch(exists_mock_path) as m_exists: |
177 | m_exists.return_value = False |
178 | - is_writable = wrap_and_call( |
179 | + devpath = wrap_and_call( |
180 | 'cloudinit.config.cc_resizefs.util', |
181 | {'is_container': {'return_value': False}, |
182 | 'get_mount_info': {'side_effect': fake_mount_info}, |
183 | 'get_cmdline': {'return_value': 'BOOT_IMAGE=/vmlinuz.efi'}}, |
184 | - is_device_path_writable_block, '/dev/root', info, LOG) |
185 | - self.assertFalse(is_writable) |
186 | + maybe_get_writable_device_path, '/dev/root', info, LOG) |
187 | + self.assertIsNone(devpath) |
188 | logs = self.logs.getvalue() |
189 | self.assertIn("WARNING: Unable to find device '/dev/root'", logs) |
190 | |
191 | - def test_is_device_path_writable_block_does_not_exist(self): |
192 | + def test_maybe_get_writable_device_path_does_not_exist(self): |
193 | """When devpath does not exist, a warning is logged.""" |
194 | info = 'dev=/I/dont/exist mnt_point=/ path=/dev/none' |
195 | - is_writable = wrap_and_call( |
196 | + devpath = wrap_and_call( |
197 | 'cloudinit.config.cc_resizefs.util', |
198 | {'is_container': {'return_value': False}}, |
199 | - is_device_path_writable_block, '/I/dont/exist', info, LOG) |
200 | - self.assertFalse(is_writable) |
201 | + maybe_get_writable_device_path, '/I/dont/exist', info, LOG) |
202 | + self.assertIsNone(devpath) |
203 | self.assertIn( |
204 | "WARNING: Device '/I/dont/exist' did not exist." |
205 | ' cannot resize: %s' % info, |
206 | self.logs.getvalue()) |
207 | |
208 | - def test_is_device_path_writable_block_does_not_exist_in_container(self): |
209 | + def test_maybe_get_writable_device_path_does_not_exist_in_container(self): |
210 | """When devpath does not exist in a container, log a debug message.""" |
211 | info = 'dev=/I/dont/exist mnt_point=/ path=/dev/none' |
212 | - is_writable = wrap_and_call( |
213 | + devpath = wrap_and_call( |
214 | 'cloudinit.config.cc_resizefs.util', |
215 | {'is_container': {'return_value': True}}, |
216 | - is_device_path_writable_block, '/I/dont/exist', info, LOG) |
217 | - self.assertFalse(is_writable) |
218 | + maybe_get_writable_device_path, '/I/dont/exist', info, LOG) |
219 | + self.assertIsNone(devpath) |
220 | self.assertIn( |
221 | "DEBUG: Device '/I/dont/exist' did not exist in container." |
222 | ' cannot resize: %s' % info, |
223 | self.logs.getvalue()) |
224 | |
225 | - def test_is_device_path_writable_block_raises_oserror(self): |
226 | + def test_maybe_get_writable_device_path_raises_oserror(self): |
227 | """When unexpected OSError is raises by os.stat it is reraised.""" |
228 | info = 'dev=/I/dont/exist mnt_point=/ path=/dev/none' |
229 | with self.assertRaises(OSError) as context_manager: |
230 | @@ -234,41 +235,63 @@ class TestIsDevicePathWritableBlock(CiTestCase): |
231 | 'cloudinit.config.cc_resizefs', |
232 | {'util.is_container': {'return_value': True}, |
233 | 'os.stat': {'side_effect': OSError('Something unexpected')}}, |
234 | - is_device_path_writable_block, '/I/dont/exist', info, LOG) |
235 | + maybe_get_writable_device_path, '/I/dont/exist', info, LOG) |
236 | self.assertEqual( |
237 | 'Something unexpected', str(context_manager.exception)) |
238 | |
239 | - def test_is_device_path_writable_block_non_block(self): |
240 | + def test_maybe_get_writable_device_path_non_block(self): |
241 | """When device is not a block device, emit warning return False.""" |
242 | fake_devpath = self.tmp_path('dev/readwrite') |
243 | util.write_file(fake_devpath, '', mode=0o600) # read-write |
244 | info = 'dev=/dev/root mnt_point=/ path={0}'.format(fake_devpath) |
245 | |
246 | - is_writable = wrap_and_call( |
247 | + devpath = wrap_and_call( |
248 | 'cloudinit.config.cc_resizefs.util', |
249 | {'is_container': {'return_value': False}}, |
250 | - is_device_path_writable_block, fake_devpath, info, LOG) |
251 | - self.assertFalse(is_writable) |
252 | + maybe_get_writable_device_path, fake_devpath, info, LOG) |
253 | + self.assertIsNone(devpath) |
254 | self.assertIn( |
255 | "WARNING: device '{0}' not a block device. cannot resize".format( |
256 | fake_devpath), |
257 | self.logs.getvalue()) |
258 | |
259 | - def test_is_device_path_writable_block_non_block_on_container(self): |
260 | + def test_maybe_get_writable_device_path_non_block_on_container(self): |
261 | """When device is non-block device in container, emit debug log.""" |
262 | fake_devpath = self.tmp_path('dev/readwrite') |
263 | util.write_file(fake_devpath, '', mode=0o600) # read-write |
264 | info = 'dev=/dev/root mnt_point=/ path={0}'.format(fake_devpath) |
265 | |
266 | - is_writable = wrap_and_call( |
267 | + devpath = wrap_and_call( |
268 | 'cloudinit.config.cc_resizefs.util', |
269 | {'is_container': {'return_value': True}}, |
270 | - is_device_path_writable_block, fake_devpath, info, LOG) |
271 | - self.assertFalse(is_writable) |
272 | + maybe_get_writable_device_path, fake_devpath, info, LOG) |
273 | + self.assertIsNone(devpath) |
274 | self.assertIn( |
275 | "DEBUG: device '{0}' not a block device in container." |
276 | ' cannot resize'.format(fake_devpath), |
277 | self.logs.getvalue()) |
278 | |
279 | + def test_maybe_get_writable_device_path_returns_cmdline_root(self): |
280 | + """When root device is UUID in kernel commandline, update devpath.""" |
281 | + # XXX Long-term we want to use FilesystemMocking test to avoid |
282 | + # touching os.stat. |
283 | + FakeStat = namedtuple( |
284 | + 'FakeStat', ['st_mode', 'st_size', 'st_mtime']) # minimal def. |
285 | + info = 'dev=/dev/root mnt_point=/ path=/does/not/matter' |
286 | + devpath = wrap_and_call( |
287 | + 'cloudinit.config.cc_resizefs', |
288 | + {'util.get_cmdline': {'return_value': 'asdf root=UUID=my-uuid'}, |
289 | + 'util.is_container': False, |
290 | + 'os.path.exists': False, # /dev/root doesn't exist |
291 | + 'os.stat': { |
292 | + 'return_value': FakeStat(25008, 0, 1)} # char block device |
293 | + }, |
294 | + maybe_get_writable_device_path, '/dev/root', info, LOG) |
295 | + self.assertEqual('/dev/disk/by-uuid/my-uuid', devpath) |
296 | + self.assertIn( |
297 | + "DEBUG: Converted /dev/root to '/dev/disk/by-uuid/my-uuid'" |
298 | + " per kernel cmdline", |
299 | + self.logs.getvalue()) |
300 | + |
301 | |
302 | # vi: ts=4 expandtab |
Please go ahead and delete the 'rootdev_ from_cmdline' function in cc_rezisefs.py
it is not used anywhere and is just confusing. the one in util is preferred.