~cloud-init-dev/cloud-init/trunk

« back to all changes in this revision

Viewing changes to cloudinit/config/cc_mounts.py

  • Committer: Scott Moser
  • Date: 2016-08-10 15:06:15 UTC
  • Revision ID: smoser@ubuntu.com-20160810150615-ma2fv107w3suy1ma
README: Mention move of revision control to git.

cloud-init development has moved its revision control to git.
It is available at 
  https://code.launchpad.net/cloud-init

Clone with 
  git clone https://git.launchpad.net/cloud-init
or
  git clone git+ssh://git.launchpad.net/cloud-init

For more information see
  https://git.launchpad.net/cloud-init/tree/HACKING.rst

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vi: ts=4 expandtab
2
 
#
3
 
#    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
 
#    This program is free software: you can redistribute it and/or modify
10
 
#    it under the terms of the GNU General Public License version 3, as
11
 
#    published by the Free Software Foundation.
12
 
#
13
 
#    This program is distributed in the hope that it will be useful,
14
 
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 
#    GNU General Public License for more details.
17
 
#
18
 
#    You should have received a copy of the GNU General Public License
19
 
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
 
 
21
 
from string import whitespace
22
 
 
23
 
import logging
24
 
import os.path
25
 
import re
26
 
 
27
 
from cloudinit import type_utils
28
 
from cloudinit import util
29
 
 
30
 
# Shortname matches 'sda', 'sda1', 'xvda', 'hda', 'sdb', xvdb, vda, vdd1, sr0
31
 
DEVICE_NAME_FILTER = r"^([x]{0,1}[shv]d[a-z][0-9]*|sr[0-9]+)$"
32
 
DEVICE_NAME_RE = re.compile(DEVICE_NAME_FILTER)
33
 
WS = re.compile("[%s]+" % (whitespace))
34
 
FSTAB_PATH = "/etc/fstab"
35
 
 
36
 
LOG = logging.getLogger(__name__)
37
 
 
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
 
    # names 'ephemeral0' or 'ephemeral1'
44
 
    # 'ebs[0-9]' appears when '--block-device-mapping sdf=snap-d4d90bbc'
45
 
    for enumname in ("ephemeral", "ebs"):
46
 
        if name.startswith(enumname) and name.find(":") == -1:
47
 
            return True
48
 
    return False
49
 
 
50
 
 
51
 
def _get_nth_partition_for_device(device_path, partition_number):
52
 
    potential_suffixes = [str(partition_number), 'p%s' % (partition_number,),
53
 
                          '-part%s' % (partition_number,)]
54
 
    for suffix in potential_suffixes:
55
 
        potential_partition_device = '%s%s' % (device_path, suffix)
56
 
        if os.path.exists(potential_partition_device):
57
 
            return potential_partition_device
58
 
    return None
59
 
 
60
 
 
61
 
def _is_block_device(device_path, partition_path=None):
62
 
    device_name = os.path.realpath(device_path).split('/')[-1]
63
 
    sys_path = os.path.join('/sys/block/', device_name)
64
 
    if partition_path is not None:
65
 
        sys_path = os.path.join(
66
 
            sys_path, os.path.realpath(partition_path).split('/')[-1])
67
 
    return os.path.exists(sys_path)
68
 
 
69
 
 
70
 
def sanitize_devname(startname, transformer, log):
71
 
    log.debug("Attempting to determine the real name of %s", startname)
72
 
 
73
 
    # workaround, allow user to specify 'ephemeral'
74
 
    # rather than more ec2 correct 'ephemeral0'
75
 
    devname = startname
76
 
    if devname == "ephemeral":
77
 
        devname = "ephemeral0"
78
 
        log.debug("Adjusted mount option from ephemeral to ephemeral0")
79
 
 
80
 
    device_path, partition_number = util.expand_dotted_devname(devname)
81
 
 
82
 
    if is_meta_device_name(device_path):
83
 
        orig = device_path
84
 
        device_path = transformer(device_path)
85
 
        if not device_path:
86
 
            return None
87
 
        if not device_path.startswith("/"):
88
 
            device_path = "/dev/%s" % (device_path,)
89
 
        log.debug("Mapped metadata name %s to %s", orig, device_path)
90
 
    else:
91
 
        if DEVICE_NAME_RE.match(startname):
92
 
            device_path = "/dev/%s" % (device_path,)
93
 
 
94
 
    partition_path = None
95
 
    if partition_number is None:
96
 
        partition_path = _get_nth_partition_for_device(device_path, 1)
97
 
    else:
98
 
        partition_path = _get_nth_partition_for_device(device_path,
99
 
                                                       partition_number)
100
 
        if partition_path is None:
101
 
            return None
102
 
 
103
 
    if _is_block_device(device_path, partition_path):
104
 
        if partition_path is not None:
105
 
            return partition_path
106
 
        return device_path
107
 
    return None
108
 
 
109
 
 
110
 
def suggested_swapsize(memsize=None, maxsize=None, fsys=None):
111
 
    # make a suggestion on the size of swap for this system.
112
 
    if memsize is None:
113
 
        memsize = util.read_meminfo()['total']
114
 
 
115
 
    GB = 2 ** 30
116
 
    sugg_max = 8 * GB
117
 
 
118
 
    info = {'avail': 'na', 'max_in': maxsize, 'mem': memsize}
119
 
 
120
 
    if fsys is None and maxsize is None:
121
 
        # set max to 8GB default if no filesystem given
122
 
        maxsize = sugg_max
123
 
    elif fsys:
124
 
        statvfs = os.statvfs(fsys)
125
 
        avail = statvfs.f_frsize * statvfs.f_bfree
126
 
        info['avail'] = avail
127
 
 
128
 
        if maxsize is None:
129
 
            # set to 25% of filesystem space
130
 
            maxsize = min(int(avail / 4), sugg_max)
131
 
        elif maxsize > ((avail * .9)):
132
 
            # set to 90% of available disk space
133
 
            maxsize = int(avail * .9)
134
 
    elif maxsize is None:
135
 
        maxsize = sugg_max
136
 
 
137
 
    info['max'] = maxsize
138
 
 
139
 
    formulas = [
140
 
        # < 1G: swap = double memory
141
 
        (1 * GB, lambda x: x * 2),
142
 
        # < 2G: swap = 2G
143
 
        (2 * GB, lambda x: 2 * GB),
144
 
        # < 4G: swap = memory
145
 
        (4 * GB, lambda x: x),
146
 
        # < 16G: 4G
147
 
        (16 * GB, lambda x: 4 * GB),
148
 
        # < 64G: 1/2 M up to max
149
 
        (64 * GB, lambda x: x / 2),
150
 
    ]
151
 
 
152
 
    size = None
153
 
    for top, func in formulas:
154
 
        if memsize <= top:
155
 
            size = min(func(memsize), maxsize)
156
 
            # if less than 1/2 memory and not much, return 0
157
 
            if size < (memsize / 2) and size < 4 * GB:
158
 
                size = 0
159
 
                break
160
 
            break
161
 
 
162
 
    if size is not None:
163
 
        size = maxsize
164
 
 
165
 
    info['size'] = size
166
 
 
167
 
    MB = 2 ** 20
168
 
    pinfo = {}
169
 
    for k, v in info.items():
170
 
        if isinstance(v, int):
171
 
            pinfo[k] = "%s MB" % (v / MB)
172
 
        else:
173
 
            pinfo[k] = v
174
 
 
175
 
    LOG.debug("suggest %(size)s swap for %(mem)s memory with '%(avail)s'"
176
 
              " disk given max=%(max_in)s [max=%(max)s]'" % pinfo)
177
 
    return size
178
 
 
179
 
 
180
 
def setup_swapfile(fname, size=None, maxsize=None):
181
 
    """
182
 
    fname: full path string of filename to setup
183
 
    size: the size to create. set to "auto" for recommended
184
 
    maxsize: the maximum size
185
 
    """
186
 
    tdir = os.path.dirname(fname)
187
 
    if str(size).lower() == "auto":
188
 
        try:
189
 
            memsize = util.read_meminfo()['total']
190
 
        except IOError as e:
191
 
            LOG.debug("Not creating swap. failed to read meminfo")
192
 
            return
193
 
 
194
 
        util.ensure_dir(tdir)
195
 
        size = suggested_swapsize(fsys=tdir, maxsize=maxsize,
196
 
                                  memsize=memsize)
197
 
 
198
 
    if not size:
199
 
        LOG.debug("Not creating swap: suggested size was 0")
200
 
        return
201
 
 
202
 
    mbsize = str(int(size / (2 ** 20)))
203
 
    msg = "creating swap file '%s' of %sMB" % (fname, mbsize)
204
 
    try:
205
 
        util.ensure_dir(tdir)
206
 
        util.log_time(LOG.debug, msg, func=util.subp,
207
 
                      args=[['sh', '-c',
208
 
                            ('rm -f "$1" && umask 0066 && '
209
 
                             '{ fallocate -l "${2}M" "$1" || '
210
 
                             ' dd if=/dev/zero "of=$1" bs=1M "count=$2"; } && '
211
 
                             'mkswap "$1" || { r=$?; rm -f "$1"; exit $r; }'),
212
 
                             'setup_swap', fname, mbsize]])
213
 
 
214
 
    except Exception as e:
215
 
        raise IOError("Failed %s: %s" % (msg, e))
216
 
 
217
 
    return fname
218
 
 
219
 
 
220
 
def handle_swapcfg(swapcfg):
221
 
    """handle the swap config, calling setup_swap if necessary.
222
 
       return None or (filename, size)
223
 
    """
224
 
    if not isinstance(swapcfg, dict):
225
 
        LOG.warn("input for swap config was not a dict.")
226
 
        return None
227
 
 
228
 
    fname = swapcfg.get('filename', '/swap.img')
229
 
    size = swapcfg.get('size', 0)
230
 
    maxsize = swapcfg.get('maxsize', None)
231
 
 
232
 
    if not (size and fname):
233
 
        LOG.debug("no need to setup swap")
234
 
        return
235
 
 
236
 
    if os.path.exists(fname):
237
 
        if not os.path.exists("/proc/swaps"):
238
 
            LOG.debug("swap file %s existed. no /proc/swaps. Being safe.",
239
 
                      fname)
240
 
            return fname
241
 
        try:
242
 
            for line in util.load_file("/proc/swaps").splitlines():
243
 
                if line.startswith(fname + " "):
244
 
                    LOG.debug("swap file %s already in use.", fname)
245
 
                    return fname
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
 
    try:
252
 
        if isinstance(size, str) and size != "auto":
253
 
            size = util.human2bytes(size)
254
 
        if isinstance(maxsize, str):
255
 
            maxsize = util.human2bytes(maxsize)
256
 
        return setup_swapfile(fname=fname, size=size, maxsize=maxsize)
257
 
 
258
 
    except Exception as e:
259
 
        LOG.warn("failed to setup swap: %s", e)
260
 
 
261
 
    return None
262
 
 
263
 
 
264
 
def handle(_name, cfg, cloud, log, _args):
265
 
    # fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno
266
 
    def_mnt_opts = "defaults,nobootwait"
267
 
    if cloud.distro.uses_systemd():
268
 
        def_mnt_opts = "defaults,nofail"
269
 
 
270
 
    defvals = [None, None, "auto", def_mnt_opts, "0", "2"]
271
 
    defvals = cfg.get("mount_default_fields", defvals)
272
 
 
273
 
    # these are our default set of mounts
274
 
    defmnts = [["ephemeral0", "/mnt", "auto", defvals[3], "0", "2"],
275
 
               ["swap", "none", "swap", "sw", "0", "0"]]
276
 
 
277
 
    cfgmnt = []
278
 
    if "mounts" in cfg:
279
 
        cfgmnt = cfg["mounts"]
280
 
 
281
 
    for i in range(len(cfgmnt)):
282
 
        # 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
 
        start = str(cfgmnt[i][0])
289
 
        sanitized = sanitize_devname(start, cloud.device_name_to_device, log)
290
 
        if sanitized is None:
291
 
            log.debug("Ignorming nonexistant named mount %s", start)
292
 
            continue
293
 
 
294
 
        if sanitized != start:
295
 
            log.debug("changed %s => %s" % (start, sanitized))
296
 
        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
 
                continue
303
 
            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
 
        for j in range(len(defvals)):
309
 
            if len(cfgmnt[i]) <= j:
310
 
                cfgmnt[i].append(defvals[j])
311
 
            elif cfgmnt[i][j] is None:
312
 
                cfgmnt[i][j] = defvals[j]
313
 
 
314
 
        # if the second entry in the list is 'None' this
315
 
        # clears all previous entries of that same 'fs_spec'
316
 
        # (fs_spec is the first field in /etc/fstab, ie, that device)
317
 
        if cfgmnt[i][1] is None:
318
 
            for j in range(i):
319
 
                if cfgmnt[j][0] == cfgmnt[i][0]:
320
 
                    cfgmnt[j][1] = None
321
 
 
322
 
    # for each of the "default" mounts, add them only if no other
323
 
    # entry has the same device name
324
 
    for defmnt in defmnts:
325
 
        start = defmnt[0]
326
 
        sanitized = sanitize_devname(start, cloud.device_name_to_device, log)
327
 
        if sanitized is None:
328
 
            log.debug("Ignoring nonexistant default named mount %s", start)
329
 
            continue
330
 
        if sanitized != start:
331
 
            log.debug("changed default device %s => %s" % (start, sanitized))
332
 
        defmnt[0] = sanitized
333
 
 
334
 
        cfgmnt_has = False
335
 
        for cfgm in cfgmnt:
336
 
            if cfgm[0] == defmnt[0]:
337
 
                cfgmnt_has = True
338
 
                break
339
 
 
340
 
        if cfgmnt_has:
341
 
            log.debug(("Not including %s, already"
342
 
                       " previously included"), start)
343
 
            continue
344
 
        cfgmnt.append(defmnt)
345
 
 
346
 
    # now, each entry in the cfgmnt list has all fstab values
347
 
    # if the second field is None (not the string, the value) we skip it
348
 
    actlist = []
349
 
    for x in cfgmnt:
350
 
        if x[1] is None:
351
 
            log.debug("Skipping non-existent device named %s", x[0])
352
 
        else:
353
 
            actlist.append(x)
354
 
 
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
 
    for line in actlist:
368
 
        # write 'comment' in the fs_mntops, entry,  claiming this
369
 
        line[3] = "%s,%s" % (line[3], comment)
370
 
        if line[2] == "swap":
371
 
            needswap = True
372
 
        if line[1].startswith("/"):
373
 
            dirs.append(line[1])
374
 
        cc_lines.append('\t'.join(line))
375
 
 
376
 
    fstab_lines = []
377
 
    for line in util.load_file(FSTAB_PATH).splitlines():
378
 
        try:
379
 
            toks = WS.split(line)
380
 
            if toks[3].find(comment) != -1:
381
 
                continue
382
 
        except Exception:
383
 
            pass
384
 
        fstab_lines.append(line)
385
 
 
386
 
    fstab_lines.extend(cc_lines)
387
 
    contents = "%s\n" % ('\n'.join(fstab_lines))
388
 
    util.write_file(FSTAB_PATH, contents)
389
 
 
390
 
    if needswap:
391
 
        try:
392
 
            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:
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")