summaryrefslogtreecommitdiff
path: root/cloudinit/transforms/resizefs.py
blob: 1690094a5b8d696ff834f65901e62287b66006c2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# vi: ts=4 expandtab
#
#    Copyright (C) 2011 Canonical Ltd.
#    Copyright (C) 2012 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 stat
import time

from cloudinit import util
from cloudinit.settings import PER_ALWAYS

frequency = PER_ALWAYS

resize_fs_prefixes_cmds = [
    ('ext', 'resize2fs'),
    ('xfs', 'xfs_growfs'),
]


def nodeify_path(devpth, where, log):
    try:
        st_dev = os.stat(where).st_dev
        dev = os.makedev(os.major(st_dev), os.minor(st_dev))
        os.mknod(devpth, 0400 | stat.S_IFBLK, dev)
        return st_dev
    except:
        if util.is_container():
            log.debug("Inside container, ignoring mknod failure in resizefs")
            return
        log.warn("Failed to make device node to resize %s at %s",
                 where, devpth)
        raise


def get_fs_type(st_dev, path, log):
    try:
        dev_entries = util.find_devs_with(tag='TYPE', oformat='value',
                                         no_cache=True, path=path)
        if not dev_entries:
            return None
        return dev_entries[0].strip()
    except util.ProcessExecutionError:
        util.logexc(log, ("Failed to get filesystem type"
                          " of maj=%s, min=%s for path %s"),
                    os.major(st_dev), os.minor(st_dev), path)
        raise


def handle(name, cfg, cloud, log, args):
    if len(args) != 0:
        resize_root = args[0]
    else:
        resize_root = util.get_cfg_option_str(cfg, "resize_rootfs", True)

    if not util.translate_bool(resize_root):
        log.debug("Skipping transform named %s, resizing disabled", name)
        return

    # TODO is the directory ok to be used??
    resize_root_d = util.get_cfg_option_str(cfg, "resize_rootfs_tmp", "/run")
    resize_root_d = cloud.paths.join(False, resize_root_d)
    util.ensure_dir(resize_root_d)

    # TODO: allow what is to be resized to be configurable??
    resize_what = cloud.paths.join(False, "/")
    with util.SilentTemporaryFile(prefix="cloudinit.resizefs.",
                                  dir=resize_root_d, delete=True) as tfh:
        devpth = tfh.name

        # Delete the file so that mknod will work
        # but don't change the file handle to know that its
        # removed so that when a later call that recreates
        # occurs this temporary file will still benefit from
        # auto deletion
        tfh.unlink_now()
    
        st_dev = nodeify_path(devpth, resize_what, log)
        fs_type = get_fs_type(st_dev, devpth, log)
        if not fs_type:
            log.warn("Could not determine filesystem type of %s", resize_what)
            return
    
        resizer = None
        fstype_lc = fs_type.lower()
        for (pfix, root_cmd) in resize_fs_prefixes_cmds:
            if fstype_lc.startswith(pfix):
                resizer = root_cmd
                break

        if not resizer:
            log.warn("Not resizing unknown filesystem type %s for %s",
                     fs_type, resize_what)
            return

        log.debug("Resizing %s (%s) using %s", resize_what, fs_type, resizer)
        resize_cmd = [resizer, devpth]

        if resize_root == "noblock":
            # Fork to a child that will run 
            # the resize command
            util.fork_cb(do_resize, resize_cmd, log)
            # Don't delete the file now in the parent
            tfh.delete = False
        else:
            do_resize(resize_cmd, log)

    action = 'Resized'
    if resize_root == "noblock":
        action = 'Resizing (via forking)'
    log.debug("%s root filesystem (type=%s, maj=%i, min=%i, val=%s)",
              action, fs_type, os.major(st_dev), os.minor(st_dev), resize_root)


def do_resize(resize_cmd, log):
    start = time.time()
    try:
        util.subp(resize_cmd)
    except util.ProcessExecutionError:
        util.logexc(log, "Failed to resize filesystem (cmd=%s)", resize_cmd)
        raise
    tot_time = int(time.time() - start)
    log.debug("Resizing took %s seconds", tot_time)
    # TODO: Should we add a fsck check after this to make
    # sure we didn't corrupt anything?