
the resizepart code was not functional. We will re-favor it later under bug 1212492. For now, we'll just favor the 'growpart' resizer. Both will be found in Ubuntu cloud images.
278 lines
8.3 KiB
Python
278 lines
8.3 KiB
Python
# vi: ts=4 expandtab
|
|
#
|
|
# Copyright (C) 2011 Canonical Ltd.
|
|
# Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# Author: Scott Moser <scott.moser@canonical.com>
|
|
# Author: Juerg Haefliger <juerg.haefliger@hp.com>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License version 3, as
|
|
# published by the Free Software Foundation.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import os
|
|
import os.path
|
|
import re
|
|
import stat
|
|
|
|
from cloudinit import log as logging
|
|
from cloudinit.settings import PER_ALWAYS
|
|
from cloudinit import util
|
|
|
|
frequency = PER_ALWAYS
|
|
|
|
DEFAULT_CONFIG = {
|
|
'mode': 'auto',
|
|
'devices': ['/'],
|
|
}
|
|
|
|
|
|
def enum(**enums):
|
|
return type('Enum', (), enums)
|
|
|
|
|
|
RESIZE = enum(SKIPPED="SKIPPED", CHANGED="CHANGED", NOCHANGE="NOCHANGE",
|
|
FAILED="FAILED")
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def resizer_factory(mode):
|
|
resize_class = None
|
|
if mode == "auto":
|
|
for (_name, resizer) in RESIZERS:
|
|
cur = resizer()
|
|
if cur.available():
|
|
resize_class = cur
|
|
break
|
|
|
|
if not resize_class:
|
|
raise ValueError("No resizers available")
|
|
|
|
else:
|
|
mmap = {}
|
|
for (k, v) in RESIZERS:
|
|
mmap[k] = v
|
|
|
|
if mode not in mmap:
|
|
raise TypeError("unknown resize mode %s" % mode)
|
|
|
|
mclass = mmap[mode]()
|
|
if mclass.available():
|
|
resize_class = mclass
|
|
|
|
if not resize_class:
|
|
raise ValueError("mode %s not available" % mode)
|
|
|
|
return resize_class
|
|
|
|
|
|
class ResizeFailedException(Exception):
|
|
pass
|
|
|
|
|
|
class ResizeParted(object):
|
|
def available(self):
|
|
myenv = os.environ.copy()
|
|
myenv['LANG'] = 'C'
|
|
|
|
try:
|
|
(out, _err) = util.subp(["parted", "--help"], env=myenv)
|
|
if re.search(r"COMMAND.*resizepart\s+", out, re.DOTALL):
|
|
return True
|
|
|
|
except util.ProcessExecutionError:
|
|
pass
|
|
return False
|
|
|
|
def resize(self, diskdev, partnum, partdev):
|
|
before = get_size(partdev)
|
|
try:
|
|
util.subp(["parted", diskdev, "resizepart", partnum])
|
|
except util.ProcessExecutionError as e:
|
|
raise ResizeFailedException(e)
|
|
|
|
return (before, get_size(partdev))
|
|
|
|
|
|
class ResizeGrowPart(object):
|
|
def available(self):
|
|
myenv = os.environ.copy()
|
|
myenv['LANG'] = 'C'
|
|
|
|
try:
|
|
(out, _err) = util.subp(["growpart", "--help"], env=myenv)
|
|
if re.search(r"--update\s+", out, re.DOTALL):
|
|
return True
|
|
|
|
except util.ProcessExecutionError:
|
|
pass
|
|
return False
|
|
|
|
def resize(self, diskdev, partnum, partdev):
|
|
before = get_size(partdev)
|
|
try:
|
|
util.subp(["growpart", '--dry-run', diskdev, partnum])
|
|
except util.ProcessExecutionError as e:
|
|
if e.exit_code != 1:
|
|
util.logexc(LOG, "Failed growpart --dry-run for (%s, %s)",
|
|
diskdev, partnum)
|
|
raise ResizeFailedException(e)
|
|
return (before, before)
|
|
|
|
try:
|
|
util.subp(["growpart", diskdev, partnum])
|
|
except util.ProcessExecutionError as e:
|
|
util.logexc(LOG, "Failed: growpart %s %s", diskdev, partnum)
|
|
raise ResizeFailedException(e)
|
|
|
|
return (before, get_size(partdev))
|
|
|
|
|
|
def get_size(filename):
|
|
fd = os.open(filename, os.O_RDONLY)
|
|
try:
|
|
return os.lseek(fd, 0, os.SEEK_END)
|
|
finally:
|
|
os.close(fd)
|
|
|
|
|
|
def device_part_info(devpath):
|
|
# convert an entry in /dev/ to parent disk and partition number
|
|
|
|
# input of /dev/vdb or /dev/disk/by-label/foo
|
|
# rpath is hopefully a real-ish path in /dev (vda, sdb..)
|
|
rpath = os.path.realpath(devpath)
|
|
|
|
bname = os.path.basename(rpath)
|
|
syspath = "/sys/class/block/%s" % bname
|
|
|
|
if not os.path.exists(syspath):
|
|
raise ValueError("%s had no syspath (%s)" % (devpath, syspath))
|
|
|
|
ptpath = os.path.join(syspath, "partition")
|
|
if not os.path.exists(ptpath):
|
|
raise TypeError("%s not a partition" % devpath)
|
|
|
|
ptnum = util.load_file(ptpath).rstrip()
|
|
|
|
# for a partition, real syspath is something like:
|
|
# /sys/devices/pci0000:00/0000:00:04.0/virtio1/block/vda/vda1
|
|
rsyspath = os.path.realpath(syspath)
|
|
disksyspath = os.path.dirname(rsyspath)
|
|
|
|
diskmajmin = util.load_file(os.path.join(disksyspath, "dev")).rstrip()
|
|
diskdevpath = os.path.realpath("/dev/block/%s" % diskmajmin)
|
|
|
|
# diskdevpath has something like 253:0
|
|
# and udev has put links in /dev/block/253:0 to the device name in /dev/
|
|
return (diskdevpath, ptnum)
|
|
|
|
|
|
def devent2dev(devent):
|
|
if devent.startswith("/dev/"):
|
|
return devent
|
|
else:
|
|
result = util.get_mount_info(devent)
|
|
if not result:
|
|
raise ValueError("Could not determine device of '%s' % dev_ent")
|
|
return result[0]
|
|
|
|
|
|
def resize_devices(resizer, devices):
|
|
# returns a tuple of tuples containing (entry-in-devices, action, message)
|
|
info = []
|
|
for devent in devices:
|
|
try:
|
|
blockdev = devent2dev(devent)
|
|
except ValueError as e:
|
|
info.append((devent, RESIZE.SKIPPED,
|
|
"unable to convert to device: %s" % e,))
|
|
continue
|
|
|
|
try:
|
|
statret = os.stat(blockdev)
|
|
except OSError as e:
|
|
info.append((devent, RESIZE.SKIPPED,
|
|
"stat of '%s' failed: %s" % (blockdev, e),))
|
|
continue
|
|
|
|
if not stat.S_ISBLK(statret.st_mode):
|
|
info.append((devent, RESIZE.SKIPPED,
|
|
"device '%s' not a block device" % blockdev,))
|
|
continue
|
|
|
|
try:
|
|
(disk, ptnum) = device_part_info(blockdev)
|
|
except (TypeError, ValueError) as e:
|
|
info.append((devent, RESIZE.SKIPPED,
|
|
"device_part_info(%s) failed: %s" % (blockdev, e),))
|
|
continue
|
|
|
|
try:
|
|
(old, new) = resizer.resize(disk, ptnum, blockdev)
|
|
if old == new:
|
|
info.append((devent, RESIZE.NOCHANGE,
|
|
"no change necessary (%s, %s)" % (disk, ptnum),))
|
|
else:
|
|
info.append((devent, RESIZE.CHANGED,
|
|
"changed (%s, %s) from %s to %s" %
|
|
(disk, ptnum, old, new),))
|
|
|
|
except ResizeFailedException as e:
|
|
info.append((devent, RESIZE.FAILED,
|
|
"failed to resize: disk=%s, ptnum=%s: %s" %
|
|
(disk, ptnum, e),))
|
|
|
|
return info
|
|
|
|
|
|
def handle(_name, cfg, _cloud, log, _args):
|
|
if 'growpart' not in cfg:
|
|
log.debug("No 'growpart' entry in cfg. Using default: %s" %
|
|
DEFAULT_CONFIG)
|
|
cfg['growpart'] = DEFAULT_CONFIG
|
|
|
|
mycfg = cfg.get('growpart')
|
|
if not isinstance(mycfg, dict):
|
|
log.warn("'growpart' in config was not a dict")
|
|
return
|
|
|
|
mode = mycfg.get('mode', "auto")
|
|
if util.is_false(mode):
|
|
log.debug("growpart disabled: mode=%s" % mode)
|
|
return
|
|
|
|
devices = util.get_cfg_option_list(cfg, "devices", ["/"])
|
|
if not len(devices):
|
|
log.debug("growpart: empty device list")
|
|
return
|
|
|
|
try:
|
|
resizer = resizer_factory(mode)
|
|
except (ValueError, TypeError) as e:
|
|
log.debug("growpart unable to find resizer for '%s': %s" % (mode, e))
|
|
if mode != "auto":
|
|
raise e
|
|
return
|
|
|
|
resized = util.log_time(logfunc=log.debug, msg="resize_devices",
|
|
func=resize_devices, args=(resizer, devices))
|
|
for (entry, action, msg) in resized:
|
|
if action == RESIZE.CHANGED:
|
|
log.info("'%s' resized: %s" % (entry, msg))
|
|
else:
|
|
log.debug("'%s' %s: %s" % (entry, action, msg))
|
|
|
|
# LP: 1212444 FIXME re-order and favor ResizeParted
|
|
#RESIZERS = (('growpart', ResizeGrowPart),)
|
|
RESIZERS = (('growpart', ResizeGrowPart), ('parted', ResizeParted))
|