298 lines
9.4 KiB
Python
298 lines
9.4 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': ['/'],
|
|
'ignore_growroot_disabled': False,
|
|
}
|
|
|
|
|
|
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 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))
|
|
|
|
class ResizeGpart(object):
|
|
def available(self):
|
|
if not os.path.exists('/usr/local/sbin/gpart'):
|
|
return False
|
|
return True
|
|
|
|
def resize(self, diskdev, partnum, partdev):
|
|
"""
|
|
GPT disks store metadata at the beginning (primary) and at the
|
|
end (secondary) of the disk. When launching an image with a
|
|
larger disk compared to the original image, the secondary copy
|
|
is lost. Thus, the metadata will be marked CORRUPT, and need to
|
|
be recovered.
|
|
"""
|
|
try:
|
|
util.subp(["gpart", "recover", diskdev])
|
|
except util.ProcessExecutionError as e:
|
|
if e.exit_code != 0:
|
|
util.logexc(LOG, "Failed: gpart recover %s", diskdev)
|
|
raise ResizeFailedException(e)
|
|
|
|
before = get_size(partdev)
|
|
try:
|
|
util.subp(["gpart", "resize", "-i", partnum, diskdev])
|
|
except util.ProcessExecutionError as e:
|
|
util.logexc(LOG, "Failed: gpart resize -i %s %s", partnum, diskdev)
|
|
raise ResizeFailedException(e)
|
|
|
|
# Since growing the FS requires a reboot, make sure we reboot
|
|
# first when this module has finished.
|
|
open('/var/run/reboot-required', 'a').close()
|
|
|
|
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
|
|
|
|
# FreeBSD doesn't know of sysfs so just get everything we need from
|
|
# the device, like /dev/vtbd0p2.
|
|
if util.system_info()["platform"].startswith('FreeBSD'):
|
|
m = re.search('^(/dev/.+)p([0-9])$', devpath)
|
|
return (m.group(1), m.group(2))
|
|
|
|
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) and not stat.S_ISCHR(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
|
|
|
|
if util.is_false(mycfg.get('ignore_growroot_disabled', False)):
|
|
if os.path.isfile("/etc/growroot-disabled"):
|
|
log.debug("growpart disabled: /etc/growroot-disabled exists")
|
|
log.debug("use ignore_growroot_disabled to ignore")
|
|
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))
|
|
|
|
RESIZERS = (('growpart', ResizeGrowPart), ('gpart', ResizeGpart))
|