cc_mounts.py 13.1 KB
Newer Older
1 2 3
# vi: ts=4 expandtab
#
#    Copyright (C) 2009-2010 Canonical Ltd.
4
#    Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
5 6
#
#    Author: Scott Moser <scott.moser@canonical.com>
7
#    Author: Juerg Haefliger <juerg.haefliger@hp.com>
8 9 10 11 12 13 14 15 16 17 18 19
#
#    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/>.
20

21
from string import whitespace
22

Scott Moser's avatar
Scott Moser committed
23
import logging
24
import os.path
25
import re
26

27
from cloudinit import type_utils
28 29
from cloudinit import util

30
# Shortname matches 'sda', 'sda1', 'xvda', 'hda', 'sdb', xvdb, vda, vdd1, sr0
31 32
DEVICE_NAME_FILTER = r"^([x]{0,1}[shv]d[a-z][0-9]*|sr[0-9]+)$"
DEVICE_NAME_RE = re.compile(DEVICE_NAME_FILTER)
33
WS = re.compile("[%s]+" % (whitespace))
harlowja's avatar
harlowja committed
34
FSTAB_PATH = "/etc/fstab"
35

Scott Moser's avatar
Scott Moser committed
36 37
LOG = logging.getLogger(__name__)

38

39
def is_meta_device_name(name):
40
    # return true if this is a metadata service name
41
    if name in ["ami", "root", "swap"]:
42
        return True
43 44
    # names 'ephemeral0' or 'ephemeral1'
    # 'ebs[0-9]' appears when '--block-device-mapping sdf=snap-d4d90bbc'
45
    for enumname in ("ephemeral", "ebs"):
46 47
        if name.startswith(enumname) and name.find(":") == -1:
            return True
48 49
    return False

50

51
def _get_nth_partition_for_device(device_path, partition_number):
52 53
    potential_suffixes = [str(partition_number), 'p%s' % (partition_number,),
                          '-part%s' % (partition_number,)]
54 55 56 57 58 59 60 61
    for suffix in potential_suffixes:
        potential_partition_device = '%s%s' % (device_path, suffix)
        if os.path.exists(potential_partition_device):
            return potential_partition_device
    return None


def _is_block_device(device_path, partition_path=None):
62
    device_name = os.path.realpath(device_path).split('/')[-1]
63 64
    sys_path = os.path.join('/sys/block/', device_name)
    if partition_path is not None:
65 66
        sys_path = os.path.join(
            sys_path, os.path.realpath(partition_path).split('/')[-1])
67 68 69
    return os.path.exists(sys_path)


70
def sanitize_devname(startname, transformer, log):
71 72 73 74 75 76 77 78 79
    log.debug("Attempting to determine the real name of %s", startname)

    # workaround, allow user to specify 'ephemeral'
    # rather than more ec2 correct 'ephemeral0'
    devname = startname
    if devname == "ephemeral":
        devname = "ephemeral0"
        log.debug("Adjusted mount option from ephemeral to ephemeral0")

80
    device_path, partition_number = util.expand_dotted_devname(devname)
81

82 83 84 85
    if is_meta_device_name(device_path):
        orig = device_path
        device_path = transformer(device_path)
        if not device_path:
86
            return None
87 88 89 90 91 92 93 94 95 96
        if not device_path.startswith("/"):
            device_path = "/dev/%s" % (device_path,)
        log.debug("Mapped metadata name %s to %s", orig, device_path)
    else:
        if DEVICE_NAME_RE.match(startname):
            device_path = "/dev/%s" % (device_path,)

    partition_path = None
    if partition_number is None:
        partition_path = _get_nth_partition_for_device(device_path, 1)
97
    else:
98 99 100 101
        partition_path = _get_nth_partition_for_device(device_path,
                                                       partition_number)
        if partition_path is None:
            return None
102

103 104 105 106 107
    if _is_block_device(device_path, partition_path):
        if partition_path is not None:
            return partition_path
        return device_path
    return None
108 109


110 111 112 113 114 115 116
def suggested_swapsize(memsize=None, maxsize=None, fsys=None):
    # make a suggestion on the size of swap for this system.
    if memsize is None:
        memsize = util.read_meminfo()['total']

    GB = 2 ** 30
    sugg_max = 8 * GB
Scott Moser's avatar
Scott Moser committed
117 118

    info = {'avail': 'na', 'max_in': maxsize, 'mem': memsize}
119 120 121 122 123 124 125

    if fsys is None and maxsize is None:
        # set max to 8GB default if no filesystem given
        maxsize = sugg_max
    elif fsys:
        statvfs = os.statvfs(fsys)
        avail = statvfs.f_frsize * statvfs.f_bfree
Scott Moser's avatar
Scott Moser committed
126
        info['avail'] = avail
127 128 129 130 131 132 133

        if maxsize is None:
            # set to 25% of filesystem space
            maxsize = min(int(avail / 4), sugg_max)
        elif maxsize > ((avail * .9)):
            # set to 90% of available disk space
            maxsize = int(avail * .9)
Scott Moser's avatar
Scott Moser committed
134 135 136 137
    elif maxsize is None:
        maxsize = sugg_max

    info['max'] = maxsize
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157

    formulas = [
        # < 1G: swap = double memory
        (1 * GB, lambda x: x * 2),
        # < 2G: swap = 2G
        (2 * GB, lambda x: 2 * GB),
        # < 4G: swap = memory
        (4 * GB, lambda x: x),
        # < 16G: 4G
        (16 * GB, lambda x: 4 * GB),
        # < 64G: 1/2 M up to max
        (64 * GB, lambda x: x / 2),
    ]

    size = None
    for top, func in formulas:
        if memsize <= top:
            size = min(func(memsize), maxsize)
            # if less than 1/2 memory and not much, return 0
            if size < (memsize / 2) and size < 4 * GB:
158 159 160 161 162 163
                size = 0
                break
            break

    if size is not None:
        size = maxsize
Scott Moser's avatar
Scott Moser committed
164 165 166 167 168 169 170 171 172 173 174 175 176

    info['size'] = size

    MB = 2 ** 20
    pinfo = {}
    for k, v in info.items():
        if isinstance(v, int):
            pinfo[k] = "%s MB" % (v / MB)
        else:
            pinfo[k] = v

    LOG.debug("suggest %(size)s swap for %(mem)s memory with '%(avail)s'"
              " disk given max=%(max_in)s [max=%(max)s]'" % pinfo)
177
    return size
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206


def setup_swapfile(fname, size=None, maxsize=None):
    """
    fname: full path string of filename to setup
    size: the size to create. set to "auto" for recommended
    maxsize: the maximum size
    """
    tdir = os.path.dirname(fname)
    if str(size).lower() == "auto":
        try:
            memsize = util.read_meminfo()['total']
        except IOError as e:
            LOG.debug("Not creating swap. failed to read meminfo")
            return

        util.ensure_dir(tdir)
        size = suggested_swapsize(fsys=tdir, maxsize=maxsize,
                                  memsize=memsize)

    if not size:
        LOG.debug("Not creating swap: suggested size was 0")
        return

    mbsize = str(int(size / (2 ** 20)))
    msg = "creating swap file '%s' of %sMB" % (fname, mbsize)
    try:
        util.ensure_dir(tdir)
        util.log_time(LOG.debug, msg, func=util.subp,
207 208 209 210 211 212
                      args=[['sh', '-c',
                            ('rm -f "$1" && umask 0066 && '
                             '{ fallocate -l "${2}M" "$1" || '
                             ' dd if=/dev/zero "of=$1" bs=1M "count=$2"; } && '
                             'mkswap "$1" || { r=$?; rm -f "$1"; exit $r; }'),
                             'setup_swap', fname, mbsize]])
213 214 215 216

    except Exception as e:
        raise IOError("Failed %s: %s" % (msg, e))

217
    return fname
218 219


220 221 222 223
def handle_swapcfg(swapcfg):
    """handle the swap config, calling setup_swap if necessary.
       return None or (filename, size)
    """
224 225 226 227
    if not isinstance(swapcfg, dict):
        LOG.warn("input for swap config was not a dict.")
        return None

228 229
    fname = swapcfg.get('filename', '/swap.img')
    size = swapcfg.get('size', 0)
230
    maxsize = swapcfg.get('maxsize', None)
231 232 233 234 235

    if not (size and fname):
        LOG.debug("no need to setup swap")
        return

236 237 238 239
    if os.path.exists(fname):
        if not os.path.exists("/proc/swaps"):
            LOG.debug("swap file %s existed. no /proc/swaps. Being safe.",
                      fname)
240
            return fname
241 242 243
        try:
            for line in util.load_file("/proc/swaps").splitlines():
                if line.startswith(fname + " "):
Scott Moser's avatar
Scott Moser committed
244
                    LOG.debug("swap file %s already in use.", fname)
245
                    return fname
Scott Moser's avatar
Scott Moser committed
246
            LOG.debug("swap file %s existed, but not in /proc/swaps", fname)
247
        except Exception:
248
            LOG.warn("swap file %s existed. Error reading /proc/swaps", fname)
249
            return fname
250

251 252 253 254 255 256 257 258 259 260 261 262 263
    try:
        if isinstance(size, str) and size != "auto":
            size = util.human2bytes(size)
        if isinstance(maxsize, str):
            maxsize = util.human2bytes(maxsize)
        return setup_swapfile(fname=fname, size=size, maxsize=maxsize)

    except Exception as e:
        LOG.warn("failed to setup swap: %s", e)

    return None


264
def handle(_name, cfg, cloud, log, _args):
265
    # fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno
266 267 268 269 270
    def_mnt_opts = "defaults,nobootwait"
    if cloud.distro.uses_systemd():
        def_mnt_opts = "defaults,nofail"

    defvals = [None, None, "auto", def_mnt_opts, "0", "2"]
271 272 273
    defvals = cfg.get("mount_default_fields", defvals)

    # these are our default set of mounts
274 275
    defmnts = [["ephemeral0", "/mnt", "auto", defvals[3], "0", "2"],
               ["swap", "none", "swap", "sw", "0", "0"]]
276

277 278
    cfgmnt = []
    if "mounts" in cfg:
279 280 281 282
        cfgmnt = cfg["mounts"]

    for i in range(len(cfgmnt)):
        # skip something that wasn't a list
283
        if not isinstance(cfgmnt[i], list):
284
            log.warn("Mount option %s not a list, got a %s instead",
285
                     (i + 1), type_utils.obj_name(cfgmnt[i]))
286
            continue
287

288 289 290 291
        start = str(cfgmnt[i][0])
        sanitized = sanitize_devname(start, cloud.device_name_to_device, log)
        if sanitized is None:
            log.debug("Ignorming nonexistant named mount %s", start)
292
            continue
293

294 295 296
        if sanitized != start:
            log.debug("changed %s => %s" % (start, sanitized))
        cfgmnt[i][0] = sanitized
297

298
        # in case the user did not quote a field (likely fs-freq, fs_passno)
299
        # but do not convert None to 'None' (LP: #898365)
300
        for j in range(len(cfgmnt[i])):
301
            if cfgmnt[i][j] is None:
302 303
                continue
            else:
304
                cfgmnt[i][j] = str(cfgmnt[i][j])
305

306
    for i in range(len(cfgmnt)):
307
        # fill in values with defaults from defvals above
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
        for j in range(len(defvals)):
            if len(cfgmnt[i]) <= j:
                cfgmnt[i].append(defvals[j])
            elif cfgmnt[i][j] is None:
                cfgmnt[i][j] = defvals[j]

        # if the second entry in the list is 'None' this
        # clears all previous entries of that same 'fs_spec'
        # (fs_spec is the first field in /etc/fstab, ie, that device)
        if cfgmnt[i][1] is None:
            for j in range(i):
                if cfgmnt[j][0] == cfgmnt[i][0]:
                    cfgmnt[j][1] = None

    # for each of the "default" mounts, add them only if no other
    # entry has the same device name
    for defmnt in defmnts:
325 326 327
        start = defmnt[0]
        sanitized = sanitize_devname(start, cloud.device_name_to_device, log)
        if sanitized is None:
Scott Moser's avatar
Scott Moser committed
328
            log.debug("Ignoring nonexistant default named mount %s", start)
329
            continue
330
        if sanitized != start:
Scott Moser's avatar
Scott Moser committed
331
            log.debug("changed default device %s => %s" % (start, sanitized))
332 333
        defmnt[0] = sanitized

334 335 336 337 338
        cfgmnt_has = False
        for cfgm in cfgmnt:
            if cfgm[0] == defmnt[0]:
                cfgmnt_has = True
                break
339

340
        if cfgmnt_has:
341
            log.debug(("Not including %s, already"
Scott Moser's avatar
Scott Moser committed
342
                       " previously included"), start)
343
            continue
344 345 346 347
        cfgmnt.append(defmnt)

    # now, each entry in the cfgmnt list has all fstab values
    # if the second field is None (not the string, the value) we skip it
348 349 350 351 352 353
    actlist = []
    for x in cfgmnt:
        if x[1] is None:
            log.debug("Skipping non-existent device named %s", x[0])
        else:
            actlist.append(x)
354

Scott Moser's avatar
Scott Moser committed
355
    swapret = handle_swapcfg(cfg.get('swap', {}))
356
    if swapret:
357
        actlist.append([swapret, "none", "swap", "sw", "0", "0"])
358

359
    if len(actlist) == 0:
360
        log.debug("No modifications to fstab needed.")
361
        return
362

363
    comment = "comment=cloudconfig"
364
    cc_lines = []
365
    needswap = False
366
    dirs = []
367 368
    for line in actlist:
        # write 'comment' in the fs_mntops, entry,  claiming this
369
        line[3] = "%s,%s" % (line[3], comment)
370 371 372 373
        if line[2] == "swap":
            needswap = True
        if line[1].startswith("/"):
            dirs.append(line[1])
374 375
        cc_lines.append('\t'.join(line))

376
    fstab_lines = []
harlowja's avatar
harlowja committed
377
    for line in util.load_file(FSTAB_PATH).splitlines():
378
        try:
379
            toks = WS.split(line)
380 381
            if toks[3].find(comment) != -1:
                continue
382
        except Exception:
383 384 385 386
            pass
        fstab_lines.append(line)

    fstab_lines.extend(cc_lines)
387
    contents = "%s\n" % ('\n'.join(fstab_lines))
harlowja's avatar
harlowja committed
388
    util.write_file(FSTAB_PATH, contents)
389 390

    if needswap:
391 392
        try:
            util.subp(("swapon", "-a"))
393
        except Exception:
394
            util.logexc(log, "Activating swap via 'swapon -a' failed")
395 396

    for d in dirs:
397
        try:
harlowja's avatar
harlowja committed
398
            util.ensure_dir(d)
399
        except Exception:
400
            util.logexc(log, "Failed to make '%s' config-mount", d)
401

402
    try:
403
        util.subp(("mount", "-a"))
404
    except Exception:
405
        util.logexc(log, "Activating mounts via 'mount -a' failed")