~quobyte/charms/trusty/quobyte-webconsole/trunk

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/core/host.py

  • Committer: Bruno Ranieri
  • Date: 2016-07-13 14:50:01 UTC
  • Revision ID: bruno@quobyte.com-20160713145001-1h6cddu9sltlvx7w
Initial charm

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# This file is part of charm-helpers.
 
4
#
 
5
# charm-helpers is free software: you can redistribute it and/or modify
 
6
# it under the terms of the GNU Lesser General Public License version 3 as
 
7
# published by the Free Software Foundation.
 
8
#
 
9
# charm-helpers is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU Lesser General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU Lesser General Public License
 
15
# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
16
 
 
17
"""Tools for working with the host system"""
 
18
# Copyright 2012 Canonical Ltd.
 
19
#
 
20
# Authors:
 
21
#  Nick Moffitt <nick.moffitt@canonical.com>
 
22
#  Matthew Wedgwood <matthew.wedgwood@canonical.com>
 
23
 
 
24
import os
 
25
import re
 
26
import pwd
 
27
import glob
 
28
import grp
 
29
import random
 
30
import string
 
31
import subprocess
 
32
import hashlib
 
33
from contextlib import contextmanager
 
34
from collections import OrderedDict
 
35
 
 
36
import six
 
37
 
 
38
from .hookenv import log
 
39
from .fstab import Fstab
 
40
 
 
41
 
 
42
def service_start(service_name):
 
43
    """Start a system service"""
 
44
    return service('start', service_name)
 
45
 
 
46
 
 
47
def service_stop(service_name):
 
48
    """Stop a system service"""
 
49
    return service('stop', service_name)
 
50
 
 
51
 
 
52
def service_restart(service_name):
 
53
    """Restart a system service"""
 
54
    return service('restart', service_name)
 
55
 
 
56
 
 
57
def service_reload(service_name, restart_on_failure=False):
 
58
    """Reload a system service, optionally falling back to restart if
 
59
    reload fails"""
 
60
    service_result = service('reload', service_name)
 
61
    if not service_result and restart_on_failure:
 
62
        service_result = service('restart', service_name)
 
63
    return service_result
 
64
 
 
65
 
 
66
def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
 
67
    """Pause a system service.
 
68
 
 
69
    Stop it, and prevent it from starting again at boot."""
 
70
    stopped = True
 
71
    if service_running(service_name):
 
72
        stopped = service_stop(service_name)
 
73
    upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
 
74
    sysv_file = os.path.join(initd_dir, service_name)
 
75
    if os.path.exists(upstart_file):
 
76
        override_path = os.path.join(
 
77
            init_dir, '{}.override'.format(service_name))
 
78
        with open(override_path, 'w') as fh:
 
79
            fh.write("manual\n")
 
80
    elif os.path.exists(sysv_file):
 
81
        subprocess.check_call(["update-rc.d", service_name, "disable"])
 
82
    else:
 
83
        # XXX: Support SystemD too
 
84
        raise ValueError(
 
85
            "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
 
86
                service_name, upstart_file, sysv_file))
 
87
    return stopped
 
88
 
 
89
 
 
90
def service_resume(service_name, init_dir="/etc/init",
 
91
                   initd_dir="/etc/init.d"):
 
92
    """Resume a system service.
 
93
 
 
94
    Reenable starting again at boot. Start the service"""
 
95
    upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
 
96
    sysv_file = os.path.join(initd_dir, service_name)
 
97
    if os.path.exists(upstart_file):
 
98
        override_path = os.path.join(
 
99
            init_dir, '{}.override'.format(service_name))
 
100
        if os.path.exists(override_path):
 
101
            os.unlink(override_path)
 
102
    elif os.path.exists(sysv_file):
 
103
        subprocess.check_call(["update-rc.d", service_name, "enable"])
 
104
    else:
 
105
        # XXX: Support SystemD too
 
106
        raise ValueError(
 
107
            "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
 
108
                service_name, upstart_file, sysv_file))
 
109
 
 
110
    started = service_running(service_name)
 
111
    if not started:
 
112
        started = service_start(service_name)
 
113
    return started
 
114
 
 
115
 
 
116
def service(action, service_name):
 
117
    """Control a system service"""
 
118
    cmd = ['service', service_name, action]
 
119
    return subprocess.call(cmd) == 0
 
120
 
 
121
 
 
122
def service_running(service):
 
123
    """Determine whether a system service is running"""
 
124
    try:
 
125
        output = subprocess.check_output(
 
126
            ['service', service, 'status'],
 
127
            stderr=subprocess.STDOUT).decode('UTF-8')
 
128
    except subprocess.CalledProcessError:
 
129
        return False
 
130
    else:
 
131
        if ("start/running" in output or "is running" in output):
 
132
            return True
 
133
        else:
 
134
            return False
 
135
 
 
136
 
 
137
def service_available(service_name):
 
138
    """Determine whether a system service is available"""
 
139
    try:
 
140
        subprocess.check_output(
 
141
            ['service', service_name, 'status'],
 
142
            stderr=subprocess.STDOUT).decode('UTF-8')
 
143
    except subprocess.CalledProcessError as e:
 
144
        return b'unrecognized service' not in e.output
 
145
    else:
 
146
        return True
 
147
 
 
148
 
 
149
def adduser(username, password=None, shell='/bin/bash', system_user=False,
 
150
            primary_group=None, secondary_groups=None):
 
151
    """
 
152
    Add a user to the system.
 
153
 
 
154
    Will log but otherwise succeed if the user already exists.
 
155
 
 
156
    :param str username: Username to create
 
157
    :param str password: Password for user; if ``None``, create a system user
 
158
    :param str shell: The default shell for the user
 
159
    :param bool system_user: Whether to create a login or system user
 
160
    :param str primary_group: Primary group for user; defaults to their username
 
161
    :param list secondary_groups: Optional list of additional groups
 
162
 
 
163
    :returns: The password database entry struct, as returned by `pwd.getpwnam`
 
164
    """
 
165
    try:
 
166
        user_info = pwd.getpwnam(username)
 
167
        log('user {0} already exists!'.format(username))
 
168
    except KeyError:
 
169
        log('creating user {0}'.format(username))
 
170
        cmd = ['useradd']
 
171
        if system_user or password is None:
 
172
            cmd.append('--system')
 
173
        else:
 
174
            cmd.extend([
 
175
                '--create-home',
 
176
                '--shell', shell,
 
177
                '--password', password,
 
178
            ])
 
179
        if not primary_group:
 
180
            try:
 
181
                grp.getgrnam(username)
 
182
                primary_group = username  # avoid "group exists" error
 
183
            except KeyError:
 
184
                pass
 
185
        if primary_group:
 
186
            cmd.extend(['-g', primary_group])
 
187
        if secondary_groups:
 
188
            cmd.extend(['-G', ','.join(secondary_groups)])
 
189
        cmd.append(username)
 
190
        subprocess.check_call(cmd)
 
191
        user_info = pwd.getpwnam(username)
 
192
    return user_info
 
193
 
 
194
 
 
195
def user_exists(username):
 
196
    """Check if a user exists"""
 
197
    try:
 
198
        pwd.getpwnam(username)
 
199
        user_exists = True
 
200
    except KeyError:
 
201
        user_exists = False
 
202
    return user_exists
 
203
 
 
204
 
 
205
def add_group(group_name, system_group=False):
 
206
    """Add a group to the system"""
 
207
    try:
 
208
        group_info = grp.getgrnam(group_name)
 
209
        log('group {0} already exists!'.format(group_name))
 
210
    except KeyError:
 
211
        log('creating group {0}'.format(group_name))
 
212
        cmd = ['addgroup']
 
213
        if system_group:
 
214
            cmd.append('--system')
 
215
        else:
 
216
            cmd.extend([
 
217
                '--group',
 
218
            ])
 
219
        cmd.append(group_name)
 
220
        subprocess.check_call(cmd)
 
221
        group_info = grp.getgrnam(group_name)
 
222
    return group_info
 
223
 
 
224
 
 
225
def add_user_to_group(username, group):
 
226
    """Add a user to a group"""
 
227
    cmd = ['gpasswd', '-a', username, group]
 
228
    log("Adding user {} to group {}".format(username, group))
 
229
    subprocess.check_call(cmd)
 
230
 
 
231
 
 
232
def rsync(from_path, to_path, flags='-r', options=None):
 
233
    """Replicate the contents of a path"""
 
234
    options = options or ['--delete', '--executability']
 
235
    cmd = ['/usr/bin/rsync', flags]
 
236
    cmd.extend(options)
 
237
    cmd.append(from_path)
 
238
    cmd.append(to_path)
 
239
    log(" ".join(cmd))
 
240
    return subprocess.check_output(cmd).decode('UTF-8').strip()
 
241
 
 
242
 
 
243
def symlink(source, destination):
 
244
    """Create a symbolic link"""
 
245
    log("Symlinking {} as {}".format(source, destination))
 
246
    cmd = [
 
247
        'ln',
 
248
        '-sf',
 
249
        source,
 
250
        destination,
 
251
    ]
 
252
    subprocess.check_call(cmd)
 
253
 
 
254
 
 
255
def mkdir(path, owner='root', group='root', perms=0o555, force=False):
 
256
    """Create a directory"""
 
257
    log("Making dir {} {}:{} {:o}".format(path, owner, group,
 
258
                                          perms))
 
259
    uid = pwd.getpwnam(owner).pw_uid
 
260
    gid = grp.getgrnam(group).gr_gid
 
261
    realpath = os.path.abspath(path)
 
262
    path_exists = os.path.exists(realpath)
 
263
    if path_exists and force:
 
264
        if not os.path.isdir(realpath):
 
265
            log("Removing non-directory file {} prior to mkdir()".format(path))
 
266
            os.unlink(realpath)
 
267
            os.makedirs(realpath, perms)
 
268
    elif not path_exists:
 
269
        os.makedirs(realpath, perms)
 
270
    os.chown(realpath, uid, gid)
 
271
    os.chmod(realpath, perms)
 
272
 
 
273
 
 
274
def write_file(path, content, owner='root', group='root', perms=0o444):
 
275
    """Create or overwrite a file with the contents of a byte string."""
 
276
    log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
 
277
    uid = pwd.getpwnam(owner).pw_uid
 
278
    gid = grp.getgrnam(group).gr_gid
 
279
    with open(path, 'wb') as target:
 
280
        os.fchown(target.fileno(), uid, gid)
 
281
        os.fchmod(target.fileno(), perms)
 
282
        target.write(content)
 
283
 
 
284
 
 
285
def fstab_remove(mp):
 
286
    """Remove the given mountpoint entry from /etc/fstab
 
287
    """
 
288
    return Fstab.remove_by_mountpoint(mp)
 
289
 
 
290
 
 
291
def fstab_add(dev, mp, fs, options=None):
 
292
    """Adds the given device entry to the /etc/fstab file
 
293
    """
 
294
    return Fstab.add(dev, mp, fs, options=options)
 
295
 
 
296
 
 
297
def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
 
298
    """Mount a filesystem at a particular mountpoint"""
 
299
    cmd_args = ['mount']
 
300
    if options is not None:
 
301
        cmd_args.extend(['-o', options])
 
302
    cmd_args.extend([device, mountpoint])
 
303
    try:
 
304
        subprocess.check_output(cmd_args)
 
305
    except subprocess.CalledProcessError as e:
 
306
        log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
 
307
        return False
 
308
 
 
309
    if persist:
 
310
        return fstab_add(device, mountpoint, filesystem, options=options)
 
311
    return True
 
312
 
 
313
 
 
314
def umount(mountpoint, persist=False):
 
315
    """Unmount a filesystem"""
 
316
    cmd_args = ['umount', mountpoint]
 
317
    try:
 
318
        subprocess.check_output(cmd_args)
 
319
    except subprocess.CalledProcessError as e:
 
320
        log('Error unmounting {}\n{}'.format(mountpoint, e.output))
 
321
        return False
 
322
 
 
323
    if persist:
 
324
        return fstab_remove(mountpoint)
 
325
    return True
 
326
 
 
327
 
 
328
def mounts():
 
329
    """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
 
330
    with open('/proc/mounts') as f:
 
331
        # [['/mount/point','/dev/path'],[...]]
 
332
        system_mounts = [m[1::-1] for m in [l.strip().split()
 
333
                                            for l in f.readlines()]]
 
334
    return system_mounts
 
335
 
 
336
 
 
337
def fstab_mount(mountpoint):
 
338
    """Mount filesystem using fstab"""
 
339
    cmd_args = ['mount', mountpoint]
 
340
    try:
 
341
        subprocess.check_output(cmd_args)
 
342
    except subprocess.CalledProcessError as e:
 
343
        log('Error unmounting {}\n{}'.format(mountpoint, e.output))
 
344
        return False
 
345
    return True
 
346
 
 
347
 
 
348
def file_hash(path, hash_type='md5'):
 
349
    """
 
350
    Generate a hash checksum of the contents of 'path' or None if not found.
 
351
 
 
352
    :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
 
353
                          such as md5, sha1, sha256, sha512, etc.
 
354
    """
 
355
    if os.path.exists(path):
 
356
        h = getattr(hashlib, hash_type)()
 
357
        with open(path, 'rb') as source:
 
358
            h.update(source.read())
 
359
        return h.hexdigest()
 
360
    else:
 
361
        return None
 
362
 
 
363
 
 
364
def path_hash(path):
 
365
    """
 
366
    Generate a hash checksum of all files matching 'path'. Standard wildcards
 
367
    like '*' and '?' are supported, see documentation for the 'glob' module for
 
368
    more information.
 
369
 
 
370
    :return: dict: A { filename: hash } dictionary for all matched files.
 
371
                   Empty if none found.
 
372
    """
 
373
    return {
 
374
        filename: file_hash(filename)
 
375
        for filename in glob.iglob(path)
 
376
    }
 
377
 
 
378
 
 
379
def check_hash(path, checksum, hash_type='md5'):
 
380
    """
 
381
    Validate a file using a cryptographic checksum.
 
382
 
 
383
    :param str checksum: Value of the checksum used to validate the file.
 
384
    :param str hash_type: Hash algorithm used to generate `checksum`.
 
385
        Can be any hash alrgorithm supported by :mod:`hashlib`,
 
386
        such as md5, sha1, sha256, sha512, etc.
 
387
    :raises ChecksumError: If the file fails the checksum
 
388
 
 
389
    """
 
390
    actual_checksum = file_hash(path, hash_type)
 
391
    if checksum != actual_checksum:
 
392
        raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
 
393
 
 
394
 
 
395
class ChecksumError(ValueError):
 
396
    pass
 
397
 
 
398
 
 
399
def restart_on_change(restart_map, stopstart=False):
 
400
    """Restart services based on configuration files changing
 
401
 
 
402
    This function is used a decorator, for example::
 
403
 
 
404
        @restart_on_change({
 
405
            '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
 
406
            '/etc/apache/sites-enabled/*': [ 'apache2' ]
 
407
            })
 
408
        def config_changed():
 
409
            pass  # your code here
 
410
 
 
411
    In this example, the cinder-api and cinder-volume services
 
412
    would be restarted if /etc/ceph/ceph.conf is changed by the
 
413
    ceph_client_changed function. The apache2 service would be
 
414
    restarted if any file matching the pattern got changed, created
 
415
    or removed. Standard wildcards are supported, see documentation
 
416
    for the 'glob' module for more information.
 
417
    """
 
418
    def wrap(f):
 
419
        def wrapped_f(*args, **kwargs):
 
420
            checksums = {path: path_hash(path) for path in restart_map}
 
421
            f(*args, **kwargs)
 
422
            restarts = []
 
423
            for path in restart_map:
 
424
                if path_hash(path) != checksums[path]:
 
425
                    restarts += restart_map[path]
 
426
            services_list = list(OrderedDict.fromkeys(restarts))
 
427
            if not stopstart:
 
428
                for service_name in services_list:
 
429
                    service('restart', service_name)
 
430
            else:
 
431
                for action in ['stop', 'start']:
 
432
                    for service_name in services_list:
 
433
                        service(action, service_name)
 
434
        return wrapped_f
 
435
    return wrap
 
436
 
 
437
 
 
438
def lsb_release():
 
439
    """Return /etc/lsb-release in a dict"""
 
440
    d = {}
 
441
    with open('/etc/lsb-release', 'r') as lsb:
 
442
        for l in lsb:
 
443
            k, v = l.split('=')
 
444
            d[k.strip()] = v.strip()
 
445
    return d
 
446
 
 
447
 
 
448
def pwgen(length=None):
 
449
    """Generate a random pasword."""
 
450
    if length is None:
 
451
        # A random length is ok to use a weak PRNG
 
452
        length = random.choice(range(35, 45))
 
453
    alphanumeric_chars = [
 
454
        l for l in (string.ascii_letters + string.digits)
 
455
        if l not in 'l0QD1vAEIOUaeiou']
 
456
    # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
 
457
    # actual password
 
458
    random_generator = random.SystemRandom()
 
459
    random_chars = [
 
460
        random_generator.choice(alphanumeric_chars) for _ in range(length)]
 
461
    return(''.join(random_chars))
 
462
 
 
463
 
 
464
def is_phy_iface(interface):
 
465
    """Returns True if interface is not virtual, otherwise False."""
 
466
    if interface:
 
467
        sys_net = '/sys/class/net'
 
468
        if os.path.isdir(sys_net):
 
469
            for iface in glob.glob(os.path.join(sys_net, '*')):
 
470
                if '/virtual/' in os.path.realpath(iface):
 
471
                    continue
 
472
 
 
473
                if interface == os.path.basename(iface):
 
474
                    return True
 
475
 
 
476
    return False
 
477
 
 
478
 
 
479
def get_bond_master(interface):
 
480
    """Returns bond master if interface is bond slave otherwise None.
 
481
 
 
482
    NOTE: the provided interface is expected to be physical
 
483
    """
 
484
    if interface:
 
485
        iface_path = '/sys/class/net/%s' % (interface)
 
486
        if os.path.exists(iface_path):
 
487
            if '/virtual/' in os.path.realpath(iface_path):
 
488
                return None
 
489
 
 
490
            master = os.path.join(iface_path, 'master')
 
491
            if os.path.exists(master):
 
492
                master = os.path.realpath(master)
 
493
                # make sure it is a bond master
 
494
                if os.path.exists(os.path.join(master, 'bonding')):
 
495
                    return os.path.basename(master)
 
496
 
 
497
    return None
 
498
 
 
499
 
 
500
def list_nics(nic_type=None):
 
501
    '''Return a list of nics of given type(s)'''
 
502
    if isinstance(nic_type, six.string_types):
 
503
        int_types = [nic_type]
 
504
    else:
 
505
        int_types = nic_type
 
506
 
 
507
    interfaces = []
 
508
    if nic_type:
 
509
        for int_type in int_types:
 
510
            cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
 
511
            ip_output = subprocess.check_output(cmd).decode('UTF-8')
 
512
            ip_output = ip_output.split('\n')
 
513
            ip_output = (line for line in ip_output if line)
 
514
            for line in ip_output:
 
515
                if line.split()[1].startswith(int_type):
 
516
                    matched = re.search('.*: (' + int_type +
 
517
                                        r'[0-9]+\.[0-9]+)@.*', line)
 
518
                    if matched:
 
519
                        iface = matched.groups()[0]
 
520
                    else:
 
521
                        iface = line.split()[1].replace(":", "")
 
522
 
 
523
                    if iface not in interfaces:
 
524
                        interfaces.append(iface)
 
525
    else:
 
526
        cmd = ['ip', 'a']
 
527
        ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
 
528
        ip_output = (line.strip() for line in ip_output if line)
 
529
 
 
530
        key = re.compile('^[0-9]+:\s+(.+):')
 
531
        for line in ip_output:
 
532
            matched = re.search(key, line)
 
533
            if matched:
 
534
                iface = matched.group(1)
 
535
                iface = iface.partition("@")[0]
 
536
                if iface not in interfaces:
 
537
                    interfaces.append(iface)
 
538
 
 
539
    return interfaces
 
540
 
 
541
 
 
542
def set_nic_mtu(nic, mtu):
 
543
    '''Set MTU on a network interface'''
 
544
    cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
 
545
    subprocess.check_call(cmd)
 
546
 
 
547
 
 
548
def get_nic_mtu(nic):
 
549
    cmd = ['ip', 'addr', 'show', nic]
 
550
    ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
 
551
    mtu = ""
 
552
    for line in ip_output:
 
553
        words = line.split()
 
554
        if 'mtu' in words:
 
555
            mtu = words[words.index("mtu") + 1]
 
556
    return mtu
 
557
 
 
558
 
 
559
def get_nic_hwaddr(nic):
 
560
    cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
 
561
    ip_output = subprocess.check_output(cmd).decode('UTF-8')
 
562
    hwaddr = ""
 
563
    words = ip_output.split()
 
564
    if 'link/ether' in words:
 
565
        hwaddr = words[words.index('link/ether') + 1]
 
566
    return hwaddr
 
567
 
 
568
 
 
569
def cmp_pkgrevno(package, revno, pkgcache=None):
 
570
    '''Compare supplied revno with the revno of the installed package
 
571
 
 
572
    *  1 => Installed revno is greater than supplied arg
 
573
    *  0 => Installed revno is the same as supplied arg
 
574
    * -1 => Installed revno is less than supplied arg
 
575
 
 
576
    This function imports apt_cache function from charmhelpers.fetch if
 
577
    the pkgcache argument is None. Be sure to add charmhelpers.fetch if
 
578
    you call this function, or pass an apt_pkg.Cache() instance.
 
579
    '''
 
580
    import apt_pkg
 
581
    if not pkgcache:
 
582
        from charmhelpers.fetch import apt_cache
 
583
        pkgcache = apt_cache()
 
584
    pkg = pkgcache[package]
 
585
    return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
 
586
 
 
587
 
 
588
@contextmanager
 
589
def chdir(d):
 
590
    cur = os.getcwd()
 
591
    try:
 
592
        yield os.chdir(d)
 
593
    finally:
 
594
        os.chdir(cur)
 
595
 
 
596
 
 
597
def chownr(path, owner, group, follow_links=True, chowntopdir=False):
 
598
    """
 
599
    Recursively change user and group ownership of files and directories
 
600
    in given path. Doesn't chown path itself by default, only its children.
 
601
 
 
602
    :param bool follow_links: Also Chown links if True
 
603
    :param bool chowntopdir: Also chown path itself if True
 
604
    """
 
605
    uid = pwd.getpwnam(owner).pw_uid
 
606
    gid = grp.getgrnam(group).gr_gid
 
607
    if follow_links:
 
608
        chown = os.chown
 
609
    else:
 
610
        chown = os.lchown
 
611
 
 
612
    if chowntopdir:
 
613
        broken_symlink = os.path.lexists(path) and not os.path.exists(path)
 
614
        if not broken_symlink:
 
615
            chown(path, uid, gid)
 
616
    for root, dirs, files in os.walk(path):
 
617
        for name in dirs + files:
 
618
            full = os.path.join(root, name)
 
619
            broken_symlink = os.path.lexists(full) and not os.path.exists(full)
 
620
            if not broken_symlink:
 
621
                chown(full, uid, gid)
 
622
 
 
623
 
 
624
def lchownr(path, owner, group):
 
625
    chownr(path, owner, group, follow_links=False)
 
626
 
 
627
 
 
628
def get_total_ram():
 
629
    '''The total amount of system RAM in bytes.
 
630
 
 
631
    This is what is reported by the OS, and may be overcommitted when
 
632
    there are multiple containers hosted on the same machine.
 
633
    '''
 
634
    with open('/proc/meminfo', 'r') as f:
 
635
        for line in f.readlines():
 
636
            if line:
 
637
                key, value, unit = line.split()
 
638
                if key == 'MemTotal:':
 
639
                    assert unit == 'kB', 'Unknown unit'
 
640
                    return int(value) * 1024  # Classic, not KiB.
 
641
        raise NotImplementedError()