Merge lp:~smoser/cloud-init/growroot into lp:~cloud-init-dev/cloud-init/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
Reviewer Review Type Date Requested Status
Joshua Harlow Pending
Review via email: mp+151817@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

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.

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 ResizeFailedException just be a subclass of ProcessExecutionError?

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("resized: %s" % resized)' be info level?

Revision history for this message
Scott Moser (smoser) wrote :

ResizeFailedException could be for some reason other than a processexecution error, so a subclass doens't seem completely correc.t

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