Merge lp:~smoser/cloud-init/growroot into lp:~cloud-init-dev/cloud-init/trunk
- growroot
- Merge into trunk
Proposed by
Scott Moser
Status: | Merged |
---|---|
Merged at revision: | 790 |
Proposed branch: | lp:~smoser/cloud-init/growroot |
Merge into: | lp:~cloud-init-dev/cloud-init/trunk |
Diff against target: |
769 lines (+634/-84) 6 files modified
cloudinit/config/cc_growpart.py (+272/-0) cloudinit/config/cc_resizefs.py (+1/-84) cloudinit/util.py (+83/-0) config/cloud.cfg (+1/-0) doc/examples/cloud-config-growpart.txt (+24/-0) tests/unittests/test_handler/test_handler_growpart.py (+253/-0) |
To merge this branch: | bzr merge lp:~smoser/cloud-init/growroot |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Joshua Harlow | Pending | ||
Review via email: mp+151817@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote : | # |
lp:~smoser/cloud-init/growroot
updated
- 794. By Scott Moser
-
change default mode to 'auto'
- 795. By Scott Moser
-
add doc
Revision history for this message
Joshua Harlow (harlowja) wrote : | # |
Just a nit. Can the function resizer_factory go after the declaration of 'RESIZERS'?
Revision history for this message
Joshua Harlow (harlowja) wrote : | # |
Another nit, can ResizeFailedExc
Does that make sense?
Revision history for this message
Joshua Harlow (harlowja) wrote : | # |
Seems fine with me otherwise.
Might be nice to have 'log.debug(
Revision history for this message
Scott Moser (smoser) wrote : | # |
ResizeFailedExc
lp:~smoser/cloud-init/growroot
updated
- 796. By Scott Moser
-
pep8, pylint, make resize_devices return more useful
resize_devices now contains what action occurred for each entry.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'cloudinit/config/cc_growpart.py' |
2 | --- cloudinit/config/cc_growpart.py 1970-01-01 00:00:00 +0000 |
3 | +++ cloudinit/config/cc_growpart.py 2013-03-05 21:28:23 +0000 |
4 | @@ -0,0 +1,272 @@ |
5 | +# vi: ts=4 expandtab |
6 | +# |
7 | +# Copyright (C) 2011 Canonical Ltd. |
8 | +# |
9 | +# Author: Scott Moser <scott.moser@canonical.com> |
10 | +# |
11 | +# This program is free software: you can redistribute it and/or modify |
12 | +# it under the terms of the GNU General Public License version 3, as |
13 | +# published by the Free Software Foundation. |
14 | +# |
15 | +# This program is distributed in the hope that it will be useful, |
16 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
17 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
18 | +# GNU General Public License for more details. |
19 | +# |
20 | +# You should have received a copy of the GNU General Public License |
21 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
22 | + |
23 | +import os |
24 | +import os.path |
25 | +import re |
26 | +import stat |
27 | + |
28 | +from cloudinit import log as logging |
29 | +from cloudinit.settings import PER_ALWAYS |
30 | +from cloudinit import util |
31 | + |
32 | +frequency = PER_ALWAYS |
33 | + |
34 | +DEFAULT_CONFIG = { |
35 | + 'mode': 'auto', |
36 | + 'devices': ['/'], |
37 | +} |
38 | + |
39 | + |
40 | +def enum(**enums): |
41 | + return type('Enum', (), enums) |
42 | + |
43 | + |
44 | +RESIZE = enum(SKIPPED="SKIPPED", CHANGED="CHANGED", NOCHANGE="NOCHANGE", |
45 | + FAILED="FAILED") |
46 | + |
47 | +LOG = logging.getLogger(__name__) |
48 | + |
49 | + |
50 | +def resizer_factory(mode): |
51 | + resize_class = None |
52 | + if mode == "auto": |
53 | + for (_name, resizer) in RESIZERS: |
54 | + cur = resizer() |
55 | + if cur.available(): |
56 | + resize_class = cur |
57 | + break |
58 | + |
59 | + if not resize_class: |
60 | + raise ValueError("No resizers available") |
61 | + |
62 | + else: |
63 | + mmap = {} |
64 | + for (k, v) in RESIZERS: |
65 | + mmap[k] = v |
66 | + |
67 | + if mode not in mmap: |
68 | + raise TypeError("unknown resize mode %s" % mode) |
69 | + |
70 | + mclass = mmap[mode]() |
71 | + if mclass.available(): |
72 | + resize_class = mclass |
73 | + |
74 | + if not resize_class: |
75 | + raise ValueError("mode %s not available" % mode) |
76 | + |
77 | + return resize_class |
78 | + |
79 | + |
80 | +class ResizeFailedException(Exception): |
81 | + pass |
82 | + |
83 | + |
84 | +class ResizeParted(object): |
85 | + def available(self): |
86 | + myenv = os.environ.copy() |
87 | + myenv['LANG'] = 'C' |
88 | + |
89 | + try: |
90 | + (out, _err) = util.subp(["parted", "--help"], env=myenv) |
91 | + if re.search(r"COMMAND.*resizepart\s+", out, re.DOTALL): |
92 | + return True |
93 | + |
94 | + except util.ProcessExecutionError: |
95 | + pass |
96 | + return False |
97 | + |
98 | + def resize(self, diskdev, partnum, partdev): |
99 | + before = get_size(partdev) |
100 | + try: |
101 | + util.subp(["parted", "resizepart", diskdev, partnum]) |
102 | + except util.ProcessExecutionError as e: |
103 | + raise ResizeFailedException(e) |
104 | + |
105 | + return (before, get_size(partdev)) |
106 | + |
107 | + |
108 | +class ResizeGrowPart(object): |
109 | + def available(self): |
110 | + myenv = os.environ.copy() |
111 | + myenv['LANG'] = 'C' |
112 | + |
113 | + try: |
114 | + (out, _err) = util.subp(["growpart", "--help"], env=myenv) |
115 | + if re.search(r"--update\s+", out, re.DOTALL): |
116 | + return True |
117 | + |
118 | + except util.ProcessExecutionError: |
119 | + pass |
120 | + return False |
121 | + |
122 | + def resize(self, diskdev, partnum, partdev): |
123 | + before = get_size(partdev) |
124 | + try: |
125 | + util.subp(["growpart", '--dry-run', diskdev, partnum]) |
126 | + except util.ProcessExecutionError as e: |
127 | + if e.exit_code != 1: |
128 | + util.logexc(LOG, ("Failed growpart --dry-run for (%s, %s)" % |
129 | + (diskdev, partnum))) |
130 | + raise ResizeFailedException(e) |
131 | + return (before, before) |
132 | + |
133 | + try: |
134 | + util.subp(["growpart", diskdev, partnum]) |
135 | + except util.ProcessExecutionError as e: |
136 | + util.logexc(LOG, "Failed: growpart %s %s" % (diskdev, partnum)) |
137 | + raise ResizeFailedException(e) |
138 | + |
139 | + return (before, get_size(partdev)) |
140 | + |
141 | + |
142 | +def get_size(filename): |
143 | + fd = os.open(filename, os.O_RDONLY) |
144 | + try: |
145 | + return os.lseek(fd, 0, os.SEEK_END) |
146 | + finally: |
147 | + os.close(fd) |
148 | + |
149 | + |
150 | +def device_part_info(devpath): |
151 | + # convert an entry in /dev/ to parent disk and partition number |
152 | + |
153 | + # input of /dev/vdb or /dev/disk/by-label/foo |
154 | + # rpath is hopefully a real-ish path in /dev (vda, sdb..) |
155 | + rpath = os.path.realpath(devpath) |
156 | + |
157 | + bname = os.path.basename(rpath) |
158 | + syspath = "/sys/class/block/%s" % bname |
159 | + |
160 | + if not os.path.exists(syspath): |
161 | + raise ValueError("%s had no syspath (%s)" % (devpath, syspath)) |
162 | + |
163 | + ptpath = os.path.join(syspath, "partition") |
164 | + if not os.path.exists(ptpath): |
165 | + raise TypeError("%s not a partition" % devpath) |
166 | + |
167 | + ptnum = util.load_file(ptpath).rstrip() |
168 | + |
169 | + # for a partition, real syspath is something like: |
170 | + # /sys/devices/pci0000:00/0000:00:04.0/virtio1/block/vda/vda1 |
171 | + rsyspath = os.path.realpath(syspath) |
172 | + disksyspath = os.path.dirname(rsyspath) |
173 | + |
174 | + diskmajmin = util.load_file(os.path.join(disksyspath, "dev")).rstrip() |
175 | + diskdevpath = os.path.realpath("/dev/block/%s" % diskmajmin) |
176 | + |
177 | + # diskdevpath has something like 253:0 |
178 | + # and udev has put links in /dev/block/253:0 to the device name in /dev/ |
179 | + return (diskdevpath, ptnum) |
180 | + |
181 | + |
182 | +def devent2dev(devent): |
183 | + if devent.startswith("/dev/"): |
184 | + return devent |
185 | + else: |
186 | + result = util.get_mount_info(devent) |
187 | + if not result: |
188 | + raise ValueError("Could not determine device of '%s' % dev_ent") |
189 | + return result[0] |
190 | + |
191 | + |
192 | +def resize_devices(resizer, devices): |
193 | + # returns a tuple of tuples containing (entry-in-devices, action, message) |
194 | + info = [] |
195 | + for devent in devices: |
196 | + try: |
197 | + blockdev = devent2dev(devent) |
198 | + except ValueError as e: |
199 | + info.append((devent, RESIZE.SKIPPED, |
200 | + "unable to convert to device: %s" % e,)) |
201 | + continue |
202 | + |
203 | + try: |
204 | + statret = os.stat(blockdev) |
205 | + except OSError as e: |
206 | + info.append((devent, RESIZE.SKIPPED, |
207 | + "stat of '%s' failed: %s" % (blockdev, e),)) |
208 | + continue |
209 | + |
210 | + if not stat.S_ISBLK(statret.st_mode): |
211 | + info.append((devent, RESIZE.SKIPPED, |
212 | + "device '%s' not a block device" % blockdev,)) |
213 | + continue |
214 | + |
215 | + try: |
216 | + (disk, ptnum) = device_part_info(blockdev) |
217 | + except (TypeError, ValueError) as e: |
218 | + info.append((devent, RESIZE.SKIPPED, |
219 | + "device_part_info(%s) failed: %s" % (blockdev, e),)) |
220 | + continue |
221 | + |
222 | + try: |
223 | + (old, new) = resizer.resize(disk, ptnum, blockdev) |
224 | + if old == new: |
225 | + info.append((devent, RESIZE.NOCHANGE, |
226 | + "no change necessary (%s, %s)" % (disk, ptnum),)) |
227 | + else: |
228 | + info.append((devent, RESIZE.CHANGED, |
229 | + "changed (%s, %s) from %s to %s" % |
230 | + (disk, ptnum, old, new),)) |
231 | + |
232 | + except ResizeFailedException as e: |
233 | + info.append((devent, RESIZE.FAILED, |
234 | + "failed to resize: disk=%s, ptnum=%s: %s" % |
235 | + (disk, ptnum, e),)) |
236 | + |
237 | + return info |
238 | + |
239 | + |
240 | +def handle(_name, cfg, _cloud, log, _args): |
241 | + if 'growpart' not in cfg: |
242 | + log.debug("No 'growpart' entry in cfg. Using default: %s" % |
243 | + DEFAULT_CONFIG) |
244 | + cfg['growpart'] = DEFAULT_CONFIG |
245 | + |
246 | + mycfg = cfg.get('growpart') |
247 | + if not isinstance(mycfg, dict): |
248 | + log.warn("'growpart' in config was not a dict") |
249 | + return |
250 | + |
251 | + mode = mycfg.get('mode', "auto") |
252 | + if util.is_false(mode): |
253 | + log.debug("growpart disabled: mode=%s" % mode) |
254 | + return |
255 | + |
256 | + devices = util.get_cfg_option_list(cfg, "devices", ["/"]) |
257 | + if not len(devices): |
258 | + log.debug("growpart: empty device list") |
259 | + return |
260 | + |
261 | + try: |
262 | + resizer = resizer_factory(mode) |
263 | + except (ValueError, TypeError) as e: |
264 | + log.debug("growpart unable to find resizer for '%s': %s" % (mode, e)) |
265 | + if mode != "auto": |
266 | + raise e |
267 | + return |
268 | + |
269 | + resized = resize_devices(resizer, devices) |
270 | + for (entry, action, msg) in resized: |
271 | + if action == RESIZE.CHANGED: |
272 | + log.info("'%s' resized: %s" % (entry, msg)) |
273 | + else: |
274 | + log.debug("'%s' %s: %s" % (entry, action, msg)) |
275 | + |
276 | +RESIZERS = (('parted', ResizeParted), ('growpart', ResizeGrowPart)) |
277 | |
278 | === modified file 'cloudinit/config/cc_resizefs.py' |
279 | --- cloudinit/config/cc_resizefs.py 2013-03-01 05:28:35 +0000 |
280 | +++ cloudinit/config/cc_resizefs.py 2013-03-05 21:28:23 +0000 |
281 | @@ -51,89 +51,6 @@ |
282 | NOBLOCK = "noblock" |
283 | |
284 | |
285 | -def get_mount_info(path, log): |
286 | - # Use /proc/$$/mountinfo to find the device where path is mounted. |
287 | - # This is done because with a btrfs filesystem using os.stat(path) |
288 | - # does not return the ID of the device. |
289 | - # |
290 | - # Here, / has a device of 18 (decimal). |
291 | - # |
292 | - # $ stat / |
293 | - # File: '/' |
294 | - # Size: 234 Blocks: 0 IO Block: 4096 directory |
295 | - # Device: 12h/18d Inode: 256 Links: 1 |
296 | - # Access: (0755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root) |
297 | - # Access: 2013-01-13 07:31:04.358011255 +0000 |
298 | - # Modify: 2013-01-13 18:48:25.930011255 +0000 |
299 | - # Change: 2013-01-13 18:48:25.930011255 +0000 |
300 | - # Birth: - |
301 | - # |
302 | - # Find where / is mounted: |
303 | - # |
304 | - # $ mount | grep ' / ' |
305 | - # /dev/vda1 on / type btrfs (rw,subvol=@,compress=lzo) |
306 | - # |
307 | - # And the device ID for /dev/vda1 is not 18: |
308 | - # |
309 | - # $ ls -l /dev/vda1 |
310 | - # brw-rw---- 1 root disk 253, 1 Jan 13 08:29 /dev/vda1 |
311 | - # |
312 | - # So use /proc/$$/mountinfo to find the device underlying the |
313 | - # input path. |
314 | - path_elements = [e for e in path.split('/') if e] |
315 | - devpth = None |
316 | - fs_type = None |
317 | - match_mount_point = None |
318 | - match_mount_point_elements = None |
319 | - mountinfo_path = '/proc/%s/mountinfo' % os.getpid() |
320 | - for line in util.load_file(mountinfo_path).splitlines(): |
321 | - parts = line.split() |
322 | - |
323 | - mount_point = parts[4] |
324 | - mount_point_elements = [e for e in mount_point.split('/') if e] |
325 | - |
326 | - # Ignore mounts deeper than the path in question. |
327 | - if len(mount_point_elements) > len(path_elements): |
328 | - continue |
329 | - |
330 | - # Ignore mounts where the common path is not the same. |
331 | - l = min(len(mount_point_elements), len(path_elements)) |
332 | - if mount_point_elements[0:l] != path_elements[0:l]: |
333 | - continue |
334 | - |
335 | - # Ignore mount points higher than an already seen mount |
336 | - # point. |
337 | - if (match_mount_point_elements is not None and |
338 | - len(match_mount_point_elements) > len(mount_point_elements)): |
339 | - continue |
340 | - |
341 | - # Find the '-' which terminates a list of optional columns to |
342 | - # find the filesystem type and the path to the device. See |
343 | - # man 5 proc for the format of this file. |
344 | - try: |
345 | - i = parts.index('-') |
346 | - except ValueError: |
347 | - log.debug("Did not find column named '-' in %s", |
348 | - mountinfo_path) |
349 | - return None |
350 | - |
351 | - # Get the path to the device. |
352 | - try: |
353 | - fs_type = parts[i + 1] |
354 | - devpth = parts[i + 2] |
355 | - except IndexError: |
356 | - log.debug("Too few columns in %s after '-' column", mountinfo_path) |
357 | - return None |
358 | - |
359 | - match_mount_point = mount_point |
360 | - match_mount_point_elements = mount_point_elements |
361 | - |
362 | - if devpth and fs_type and match_mount_point: |
363 | - return (devpth, fs_type, match_mount_point) |
364 | - else: |
365 | - return None |
366 | - |
367 | - |
368 | def handle(name, cfg, _cloud, log, args): |
369 | if len(args) != 0: |
370 | resize_root = args[0] |
371 | @@ -150,7 +67,7 @@ |
372 | |
373 | # TODO(harlowja): allow what is to be resized to be configurable?? |
374 | resize_what = "/" |
375 | - result = get_mount_info(resize_what, log) |
376 | + result = util.get_mount_info(resize_what, log) |
377 | if not result: |
378 | log.warn("Could not determine filesystem type of %s", resize_what) |
379 | return |
380 | |
381 | === modified file 'cloudinit/util.py' |
382 | --- cloudinit/util.py 2013-01-31 00:21:37 +0000 |
383 | +++ cloudinit/util.py 2013-03-05 21:28:23 +0000 |
384 | @@ -1586,3 +1586,86 @@ |
385 | raise RuntimeError("Invalid package type.") |
386 | |
387 | return pkglist |
388 | + |
389 | + |
390 | +def get_mount_info(path, log=LOG): |
391 | + # Use /proc/$$/mountinfo to find the device where path is mounted. |
392 | + # This is done because with a btrfs filesystem using os.stat(path) |
393 | + # does not return the ID of the device. |
394 | + # |
395 | + # Here, / has a device of 18 (decimal). |
396 | + # |
397 | + # $ stat / |
398 | + # File: '/' |
399 | + # Size: 234 Blocks: 0 IO Block: 4096 directory |
400 | + # Device: 12h/18d Inode: 256 Links: 1 |
401 | + # Access: (0755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root) |
402 | + # Access: 2013-01-13 07:31:04.358011255 +0000 |
403 | + # Modify: 2013-01-13 18:48:25.930011255 +0000 |
404 | + # Change: 2013-01-13 18:48:25.930011255 +0000 |
405 | + # Birth: - |
406 | + # |
407 | + # Find where / is mounted: |
408 | + # |
409 | + # $ mount | grep ' / ' |
410 | + # /dev/vda1 on / type btrfs (rw,subvol=@,compress=lzo) |
411 | + # |
412 | + # And the device ID for /dev/vda1 is not 18: |
413 | + # |
414 | + # $ ls -l /dev/vda1 |
415 | + # brw-rw---- 1 root disk 253, 1 Jan 13 08:29 /dev/vda1 |
416 | + # |
417 | + # So use /proc/$$/mountinfo to find the device underlying the |
418 | + # input path. |
419 | + path_elements = [e for e in path.split('/') if e] |
420 | + devpth = None |
421 | + fs_type = None |
422 | + match_mount_point = None |
423 | + match_mount_point_elements = None |
424 | + mountinfo_path = '/proc/%s/mountinfo' % os.getpid() |
425 | + for line in load_file(mountinfo_path).splitlines(): |
426 | + parts = line.split() |
427 | + |
428 | + mount_point = parts[4] |
429 | + mount_point_elements = [e for e in mount_point.split('/') if e] |
430 | + |
431 | + # Ignore mounts deeper than the path in question. |
432 | + if len(mount_point_elements) > len(path_elements): |
433 | + continue |
434 | + |
435 | + # Ignore mounts where the common path is not the same. |
436 | + l = min(len(mount_point_elements), len(path_elements)) |
437 | + if mount_point_elements[0:l] != path_elements[0:l]: |
438 | + continue |
439 | + |
440 | + # Ignore mount points higher than an already seen mount |
441 | + # point. |
442 | + if (match_mount_point_elements is not None and |
443 | + len(match_mount_point_elements) > len(mount_point_elements)): |
444 | + continue |
445 | + |
446 | + # Find the '-' which terminates a list of optional columns to |
447 | + # find the filesystem type and the path to the device. See |
448 | + # man 5 proc for the format of this file. |
449 | + try: |
450 | + i = parts.index('-') |
451 | + except ValueError: |
452 | + log.debug("Did not find column named '-' in %s", |
453 | + mountinfo_path) |
454 | + return None |
455 | + |
456 | + # Get the path to the device. |
457 | + try: |
458 | + fs_type = parts[i + 1] |
459 | + devpth = parts[i + 2] |
460 | + except IndexError: |
461 | + log.debug("Too few columns in %s after '-' column", mountinfo_path) |
462 | + return None |
463 | + |
464 | + match_mount_point = mount_point |
465 | + match_mount_point_elements = mount_point_elements |
466 | + |
467 | + if devpth and fs_type and match_mount_point: |
468 | + return (devpth, fs_type, match_mount_point) |
469 | + else: |
470 | + return None |
471 | |
472 | === modified file 'config/cloud.cfg' |
473 | --- config/cloud.cfg 2012-11-19 14:25:09 +0000 |
474 | +++ config/cloud.cfg 2013-03-05 21:28:23 +0000 |
475 | @@ -26,6 +26,7 @@ |
476 | - migrator |
477 | - bootcmd |
478 | - write-files |
479 | + - growpart |
480 | - resizefs |
481 | - set_hostname |
482 | - update_hostname |
483 | |
484 | === added file 'doc/examples/cloud-config-growpart.txt' |
485 | --- doc/examples/cloud-config-growpart.txt 1970-01-01 00:00:00 +0000 |
486 | +++ doc/examples/cloud-config-growpart.txt 2013-03-05 21:28:23 +0000 |
487 | @@ -0,0 +1,24 @@ |
488 | +#cloud-config |
489 | +# |
490 | +# growpart entry is a dict, if it is not present at all |
491 | +# in config, then the default is used ({'mode': 'auto', 'devices': ['/']}) |
492 | +# |
493 | +# mode: |
494 | +# values: |
495 | +# * auto: use any option possible (growpart or parted) |
496 | +# if none are available, do not warn, but debug. |
497 | +# * growpart: use growpart to grow partitions |
498 | +# if growpart is not available, this is an error. |
499 | +# * parted: use parted (parted resizepart) to resize partitions |
500 | +# if parted is not available, this is an error. |
501 | +# * off, false |
502 | +# |
503 | +# devices: |
504 | +# a list of things to resize. |
505 | +# items can be filesystem paths or devices (in /dev) |
506 | +# examples: |
507 | +# devices: [/, /dev/vdb1] |
508 | +# |
509 | +growpart: |
510 | + mode: auto |
511 | + devices: ['/'] |
512 | |
513 | === added file 'tests/unittests/test_handler/test_handler_growpart.py' |
514 | --- tests/unittests/test_handler/test_handler_growpart.py 1970-01-01 00:00:00 +0000 |
515 | +++ tests/unittests/test_handler/test_handler_growpart.py 2013-03-05 21:28:23 +0000 |
516 | @@ -0,0 +1,253 @@ |
517 | +from mocker import MockerTestCase |
518 | + |
519 | +from cloudinit import cloud |
520 | +from cloudinit import helpers |
521 | +from cloudinit import util |
522 | + |
523 | +from cloudinit.config import cc_growpart |
524 | + |
525 | +import errno |
526 | +import logging |
527 | +import os |
528 | +import mocker |
529 | +import re |
530 | +import stat |
531 | + |
532 | +# growpart: |
533 | +# mode: auto # off, on, auto, 'growpart', 'parted' |
534 | +# devices: ['root'] |
535 | + |
536 | +HELP_PARTED_NO_RESIZE = """ |
537 | +Usage: parted [OPTION]... [DEVICE [COMMAND [PARAMETERS]...]...] |
538 | +Apply COMMANDs with PARAMETERS to DEVICE. If no COMMAND(s) are given, run in |
539 | +interactive mode. |
540 | + |
541 | +OPTIONs: |
542 | +<SNIP> |
543 | + |
544 | +COMMANDs: |
545 | +<SNIP> |
546 | + quit exit program |
547 | + rescue START END rescue a lost partition near START |
548 | + and END |
549 | + resize NUMBER START END resize partition NUMBER and its file |
550 | + system |
551 | + rm NUMBER delete partition NUMBER |
552 | +<SNIP> |
553 | +Report bugs to bug-parted@gnu.org |
554 | +""" |
555 | + |
556 | +HELP_PARTED_RESIZE = """ |
557 | +Usage: parted [OPTION]... [DEVICE [COMMAND [PARAMETERS]...]...] |
558 | +Apply COMMANDs with PARAMETERS to DEVICE. If no COMMAND(s) are given, run in |
559 | +interactive mode. |
560 | + |
561 | +OPTIONs: |
562 | +<SNIP> |
563 | + |
564 | +COMMANDs: |
565 | +<SNIP> |
566 | + quit exit program |
567 | + rescue START END rescue a lost partition near START |
568 | + and END |
569 | + resize NUMBER START END resize partition NUMBER and its file |
570 | + system |
571 | + resizepart NUMBER END resize partition NUMBER |
572 | + rm NUMBER delete partition NUMBER |
573 | +<SNIP> |
574 | +Report bugs to bug-parted@gnu.org |
575 | +""" |
576 | + |
577 | +HELP_GROWPART_RESIZE = """ |
578 | +growpart disk partition |
579 | + rewrite partition table so that partition takes up all the space it can |
580 | + options: |
581 | + -h | --help print Usage and exit |
582 | +<SNIP> |
583 | + -u | --update R update the the kernel partition table info after growing |
584 | + this requires kernel support and 'partx --update' |
585 | + R is one of: |
586 | + - 'auto' : [default] update partition if possible |
587 | +<SNIP> |
588 | + Example: |
589 | + - growpart /dev/sda 1 |
590 | + Resize partition 1 on /dev/sda |
591 | +""" |
592 | + |
593 | +HELP_GROWPART_NO_RESIZE = """ |
594 | +growpart disk partition |
595 | + rewrite partition table so that partition takes up all the space it can |
596 | + options: |
597 | + -h | --help print Usage and exit |
598 | +<SNIP> |
599 | + Example: |
600 | + - growpart /dev/sda 1 |
601 | + Resize partition 1 on /dev/sda |
602 | +""" |
603 | + |
604 | +class TestDisabled(MockerTestCase): |
605 | + def setUp(self): |
606 | + super(TestDisabled, self).setUp() |
607 | + self.name = "growpart" |
608 | + self.cloud_init = None |
609 | + self.log = logging.getLogger("TestDisabled") |
610 | + self.args = [] |
611 | + |
612 | + self.handle = cc_growpart.handle |
613 | + |
614 | + def test_mode_off(self): |
615 | + #Test that nothing is done if mode is off. |
616 | + |
617 | + # this really only verifies that resizer_factory isn't called |
618 | + config = {'growpart': {'mode': 'off'}} |
619 | + self.mocker.replace(cc_growpart.resizer_factory, |
620 | + passthrough=False) |
621 | + self.mocker.replay() |
622 | + |
623 | + self.handle(self.name, config, self.cloud_init, self.log, self.args) |
624 | + |
625 | +class TestConfig(MockerTestCase): |
626 | + def setUp(self): |
627 | + super(TestConfig, self).setUp() |
628 | + self.name = "growpart" |
629 | + self.paths = None |
630 | + self.cloud = cloud.Cloud(None, self.paths, None, None, None) |
631 | + self.log = logging.getLogger("TestConfig") |
632 | + self.args = [] |
633 | + os.environ = {} |
634 | + |
635 | + self.cloud_init = None |
636 | + self.handle = cc_growpart.handle |
637 | + |
638 | + # Order must be correct |
639 | + self.mocker.order() |
640 | + |
641 | + def test_no_resizers_auto_is_fine(self): |
642 | + subp = self.mocker.replace(util.subp, passthrough=False) |
643 | + subp(['parted', '--help'], env={'LANG': 'C'}) |
644 | + self.mocker.result((HELP_PARTED_NO_RESIZE,"")) |
645 | + subp(['growpart', '--help'], env={'LANG': 'C'}) |
646 | + self.mocker.result((HELP_GROWPART_NO_RESIZE,"")) |
647 | + self.mocker.replay() |
648 | + |
649 | + config = {'growpart': {'mode': 'auto'}} |
650 | + self.handle(self.name, config, self.cloud_init, self.log, self.args) |
651 | + |
652 | + def test_no_resizers_mode_growpart_is_exception(self): |
653 | + subp = self.mocker.replace(util.subp, passthrough=False) |
654 | + subp(['growpart', '--help'], env={'LANG': 'C'}) |
655 | + self.mocker.result((HELP_GROWPART_NO_RESIZE,"")) |
656 | + self.mocker.replay() |
657 | + |
658 | + config = {'growpart': {'mode': "growpart"}} |
659 | + self.assertRaises(ValueError, self.handle, self.name, config, |
660 | + self.cloud_init, self.log, self.args) |
661 | + |
662 | + def test_mode_auto_prefers_parted(self): |
663 | + subp = self.mocker.replace(util.subp, passthrough=False) |
664 | + subp(['parted', '--help'], env={'LANG': 'C'}) |
665 | + self.mocker.result((HELP_PARTED_RESIZE,"")) |
666 | + self.mocker.replay() |
667 | + |
668 | + ret = cc_growpart.resizer_factory(mode="auto") |
669 | + self.assertTrue(isinstance(ret, cc_growpart.ResizeParted)) |
670 | + |
671 | + def test_handle_with_no_growpart_entry(self): |
672 | + #if no 'growpart' entry in config, then mode=auto should be used |
673 | + |
674 | + myresizer = object() |
675 | + |
676 | + factory = self.mocker.replace(cc_growpart.resizer_factory, |
677 | + passthrough=False) |
678 | + rsdevs = self.mocker.replace(cc_growpart.resize_devices, |
679 | + passthrough=False) |
680 | + factory("auto") |
681 | + self.mocker.result(myresizer) |
682 | + rsdevs(myresizer, ["/"]) |
683 | + self.mocker.result((("/", cc_growpart.RESIZE.CHANGED, "my-message",),)) |
684 | + self.mocker.replay() |
685 | + |
686 | + try: |
687 | + orig_resizers = cc_growpart.RESIZERS |
688 | + cc_growpart.RESIZERS = (('mysizer', object),) |
689 | + self.handle(self.name, {}, self.cloud_init, self.log, self.args) |
690 | + finally: |
691 | + cc_growpart.RESIZERS = orig_resizers |
692 | + |
693 | + |
694 | +class TestResize(MockerTestCase): |
695 | + def setUp(self): |
696 | + super(TestResize, self).setUp() |
697 | + self.name = "growpart" |
698 | + self.log = logging.getLogger("TestResize") |
699 | + |
700 | + # Order must be correct |
701 | + self.mocker.order() |
702 | + |
703 | + def test_simple_devices(self): |
704 | + #test simple device list |
705 | + # this patches out devent2dev, os.stat, and device_part_info |
706 | + # so in the end, doesn't test a lot |
707 | + devs = ["/dev/XXda1", "/dev/YYda2"] |
708 | + devstat_ret = Bunch(st_mode=25008, st_ino=6078, st_dev=5L, |
709 | + st_nlink=1, st_uid=0, st_gid=6, st_size=0, |
710 | + st_atime=0, st_mtime=0, st_ctime=0) |
711 | + enoent = ["/dev/NOENT"] |
712 | + real_stat = os.stat |
713 | + resize_calls = [] |
714 | + |
715 | + class myresizer(): |
716 | + def resize(self, diskdev, partnum, partdev): |
717 | + resize_calls.append((diskdev, partnum, partdev)) |
718 | + if partdev == "/dev/YYda2": |
719 | + return (1024, 2048) |
720 | + return (1024, 1024) # old size, new size |
721 | + |
722 | + def mystat(path): |
723 | + if path in devs: |
724 | + return devstat_ret |
725 | + if path in enoent: |
726 | + e = OSError("%s: does not exist" % path) |
727 | + e.errno = errno.ENOENT |
728 | + raise e |
729 | + return real_stat(path) |
730 | + |
731 | + try: |
732 | + opinfo = cc_growpart.device_part_info |
733 | + cc_growpart.device_part_info = simple_device_part_info |
734 | + os.stat = mystat |
735 | + |
736 | + resized = cc_growpart.resize_devices(myresizer(), devs + enoent) |
737 | + |
738 | + def find(name, res): |
739 | + for f in res: |
740 | + if f[0] == name: |
741 | + return f |
742 | + return None |
743 | + |
744 | + self.assertEqual(cc_growpart.RESIZE.NOCHANGE, |
745 | + find("/dev/XXda1", resized)[1]) |
746 | + self.assertEqual(cc_growpart.RESIZE.CHANGED, |
747 | + find("/dev/YYda2", resized)[1]) |
748 | + self.assertEqual(cc_growpart.RESIZE.SKIPPED, |
749 | + find(enoent[0], resized)[1]) |
750 | + #self.assertEqual(resize_calls, |
751 | + #[("/dev/XXda", "1", "/dev/XXda1"), |
752 | + #("/dev/YYda", "2", "/dev/YYda2")]) |
753 | + finally: |
754 | + cc_growpart.device_part_info = opinfo |
755 | + os.stat = real_stat |
756 | + |
757 | + |
758 | +def simple_device_part_info(devpath): |
759 | + # simple stupid return (/dev/vda, 1) for /dev/vda |
760 | + ret = re.search("([^0-9]*)([0-9]*)$", devpath) |
761 | + x = (ret.group(1), ret.group(2)) |
762 | + return x |
763 | + |
764 | +class Bunch: |
765 | + def __init__(self, **kwds): |
766 | + self.__dict__.update(kwds) |
767 | + |
768 | + |
769 | +# vi: ts=4 expandtab |
I'm not terribly happy with the tests here, they're very tied to the implementation.
but it generally functions at this point. The idea being:
* if you specify 'mode=growpart' or 'mode=parted' rather than 'mode=auto', and it cannot do that, then it is failure.
* if you specify 'auto' and nothing is available to resize, then it just debug output.