~openstack-charmers-next/charms/precise/glance-simplestreams-sync/trunk

« back to all changes in this revision

Viewing changes to charmhelpers/core/host.py

[freyes,r=billy-olsen]

Refactor config-changed hook to ensure that cron jobs are installed
properly.

Closes-Bug: #1434356

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 = service_stop(service_name)
 
71
    upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
 
72
    sysv_file = os.path.join(initd_dir, service_name)
 
73
    if os.path.exists(upstart_file):
 
74
        override_path = os.path.join(
 
75
            init_dir, '{}.override'.format(service_name))
 
76
        with open(override_path, 'w') as fh:
 
77
            fh.write("manual\n")
 
78
    elif os.path.exists(sysv_file):
 
79
        subprocess.check_call(["update-rc.d", service_name, "disable"])
 
80
    else:
 
81
        # XXX: Support SystemD too
 
82
        raise ValueError(
 
83
            "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
 
84
                service_name, upstart_file, sysv_file))
 
85
    return stopped
 
86
 
 
87
 
 
88
def service_resume(service_name, init_dir="/etc/init",
 
89
                   initd_dir="/etc/init.d"):
 
90
    """Resume a system service.
 
91
 
 
92
    Reenable starting again at boot. Start the service"""
 
93
    upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
 
94
    sysv_file = os.path.join(initd_dir, service_name)
 
95
    if os.path.exists(upstart_file):
 
96
        override_path = os.path.join(
 
97
            init_dir, '{}.override'.format(service_name))
 
98
        if os.path.exists(override_path):
 
99
            os.unlink(override_path)
 
100
    elif os.path.exists(sysv_file):
 
101
        subprocess.check_call(["update-rc.d", service_name, "enable"])
 
102
    else:
 
103
        # XXX: Support SystemD too
 
104
        raise ValueError(
 
105
            "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
 
106
                service_name, upstart_file, sysv_file))
 
107
 
 
108
    started = service_start(service_name)
 
109
    return started
 
110
 
 
111
 
 
112
def service(action, service_name):
 
113
    """Control a system service"""
 
114
    cmd = ['service', service_name, action]
 
115
    return subprocess.call(cmd) == 0
 
116
 
 
117
 
 
118
def service_running(service):
 
119
    """Determine whether a system service is running"""
 
120
    try:
 
121
        output = subprocess.check_output(
 
122
            ['service', service, 'status'],
 
123
            stderr=subprocess.STDOUT).decode('UTF-8')
 
124
    except subprocess.CalledProcessError:
 
125
        return False
 
126
    else:
 
127
        if ("start/running" in output or "is running" in output):
 
128
            return True
 
129
        else:
 
130
            return False
 
131
 
 
132
 
 
133
def service_available(service_name):
 
134
    """Determine whether a system service is available"""
 
135
    try:
 
136
        subprocess.check_output(
 
137
            ['service', service_name, 'status'],
 
138
            stderr=subprocess.STDOUT).decode('UTF-8')
 
139
    except subprocess.CalledProcessError as e:
 
140
        return b'unrecognized service' not in e.output
 
141
    else:
 
142
        return True
 
143
 
 
144
 
 
145
def adduser(username, password=None, shell='/bin/bash', system_user=False):
 
146
    """Add a user to the system"""
 
147
    try:
 
148
        user_info = pwd.getpwnam(username)
 
149
        log('user {0} already exists!'.format(username))
 
150
    except KeyError:
 
151
        log('creating user {0}'.format(username))
 
152
        cmd = ['useradd']
 
153
        if system_user or password is None:
 
154
            cmd.append('--system')
 
155
        else:
 
156
            cmd.extend([
 
157
                '--create-home',
 
158
                '--shell', shell,
 
159
                '--password', password,
 
160
            ])
 
161
        cmd.append(username)
 
162
        subprocess.check_call(cmd)
 
163
        user_info = pwd.getpwnam(username)
 
164
    return user_info
 
165
 
 
166
 
 
167
def user_exists(username):
 
168
    """Check if a user exists"""
 
169
    try:
 
170
        pwd.getpwnam(username)
 
171
        user_exists = True
 
172
    except KeyError:
 
173
        user_exists = False
 
174
    return user_exists
 
175
 
 
176
 
 
177
def add_group(group_name, system_group=False):
 
178
    """Add a group to the system"""
 
179
    try:
 
180
        group_info = grp.getgrnam(group_name)
 
181
        log('group {0} already exists!'.format(group_name))
 
182
    except KeyError:
 
183
        log('creating group {0}'.format(group_name))
 
184
        cmd = ['addgroup']
 
185
        if system_group:
 
186
            cmd.append('--system')
 
187
        else:
 
188
            cmd.extend([
 
189
                '--group',
 
190
            ])
 
191
        cmd.append(group_name)
 
192
        subprocess.check_call(cmd)
 
193
        group_info = grp.getgrnam(group_name)
 
194
    return group_info
 
195
 
 
196
 
 
197
def add_user_to_group(username, group):
 
198
    """Add a user to a group"""
 
199
    cmd = ['gpasswd', '-a', username, group]
 
200
    log("Adding user {} to group {}".format(username, group))
 
201
    subprocess.check_call(cmd)
 
202
 
 
203
 
 
204
def rsync(from_path, to_path, flags='-r', options=None):
 
205
    """Replicate the contents of a path"""
 
206
    options = options or ['--delete', '--executability']
 
207
    cmd = ['/usr/bin/rsync', flags]
 
208
    cmd.extend(options)
 
209
    cmd.append(from_path)
 
210
    cmd.append(to_path)
 
211
    log(" ".join(cmd))
 
212
    return subprocess.check_output(cmd).decode('UTF-8').strip()
 
213
 
 
214
 
 
215
def symlink(source, destination):
 
216
    """Create a symbolic link"""
 
217
    log("Symlinking {} as {}".format(source, destination))
 
218
    cmd = [
 
219
        'ln',
 
220
        '-sf',
 
221
        source,
 
222
        destination,
 
223
    ]
 
224
    subprocess.check_call(cmd)
 
225
 
 
226
 
 
227
def mkdir(path, owner='root', group='root', perms=0o555, force=False):
 
228
    """Create a directory"""
 
229
    log("Making dir {} {}:{} {:o}".format(path, owner, group,
 
230
                                          perms))
 
231
    uid = pwd.getpwnam(owner).pw_uid
 
232
    gid = grp.getgrnam(group).gr_gid
 
233
    realpath = os.path.abspath(path)
 
234
    path_exists = os.path.exists(realpath)
 
235
    if path_exists and force:
 
236
        if not os.path.isdir(realpath):
 
237
            log("Removing non-directory file {} prior to mkdir()".format(path))
 
238
            os.unlink(realpath)
 
239
            os.makedirs(realpath, perms)
 
240
    elif not path_exists:
 
241
        os.makedirs(realpath, perms)
 
242
    os.chown(realpath, uid, gid)
 
243
    os.chmod(realpath, perms)
 
244
 
 
245
 
 
246
def write_file(path, content, owner='root', group='root', perms=0o444):
 
247
    """Create or overwrite a file with the contents of a byte string."""
 
248
    log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
 
249
    uid = pwd.getpwnam(owner).pw_uid
 
250
    gid = grp.getgrnam(group).gr_gid
 
251
    with open(path, 'wb') as target:
 
252
        os.fchown(target.fileno(), uid, gid)
 
253
        os.fchmod(target.fileno(), perms)
 
254
        target.write(content)
 
255
 
 
256
 
 
257
def fstab_remove(mp):
 
258
    """Remove the given mountpoint entry from /etc/fstab
 
259
    """
 
260
    return Fstab.remove_by_mountpoint(mp)
 
261
 
 
262
 
 
263
def fstab_add(dev, mp, fs, options=None):
 
264
    """Adds the given device entry to the /etc/fstab file
 
265
    """
 
266
    return Fstab.add(dev, mp, fs, options=options)
 
267
 
 
268
 
 
269
def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
 
270
    """Mount a filesystem at a particular mountpoint"""
 
271
    cmd_args = ['mount']
 
272
    if options is not None:
 
273
        cmd_args.extend(['-o', options])
 
274
    cmd_args.extend([device, mountpoint])
 
275
    try:
 
276
        subprocess.check_output(cmd_args)
 
277
    except subprocess.CalledProcessError as e:
 
278
        log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
 
279
        return False
 
280
 
 
281
    if persist:
 
282
        return fstab_add(device, mountpoint, filesystem, options=options)
 
283
    return True
 
284
 
 
285
 
 
286
def umount(mountpoint, persist=False):
 
287
    """Unmount a filesystem"""
 
288
    cmd_args = ['umount', mountpoint]
 
289
    try:
 
290
        subprocess.check_output(cmd_args)
 
291
    except subprocess.CalledProcessError as e:
 
292
        log('Error unmounting {}\n{}'.format(mountpoint, e.output))
 
293
        return False
 
294
 
 
295
    if persist:
 
296
        return fstab_remove(mountpoint)
 
297
    return True
 
298
 
 
299
 
 
300
def mounts():
 
301
    """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
 
302
    with open('/proc/mounts') as f:
 
303
        # [['/mount/point','/dev/path'],[...]]
 
304
        system_mounts = [m[1::-1] for m in [l.strip().split()
 
305
                                            for l in f.readlines()]]
 
306
    return system_mounts
 
307
 
 
308
 
 
309
def fstab_mount(mountpoint):
 
310
    """Mount filesystem using fstab"""
 
311
    cmd_args = ['mount', mountpoint]
 
312
    try:
 
313
        subprocess.check_output(cmd_args)
 
314
    except subprocess.CalledProcessError as e:
 
315
        log('Error unmounting {}\n{}'.format(mountpoint, e.output))
 
316
        return False
 
317
    return True
 
318
 
 
319
 
 
320
def file_hash(path, hash_type='md5'):
 
321
    """
 
322
    Generate a hash checksum of the contents of 'path' or None if not found.
 
323
 
 
324
    :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
 
325
                          such as md5, sha1, sha256, sha512, etc.
 
326
    """
 
327
    if os.path.exists(path):
 
328
        h = getattr(hashlib, hash_type)()
 
329
        with open(path, 'rb') as source:
 
330
            h.update(source.read())
 
331
        return h.hexdigest()
 
332
    else:
 
333
        return None
 
334
 
 
335
 
 
336
def path_hash(path):
 
337
    """
 
338
    Generate a hash checksum of all files matching 'path'. Standard wildcards
 
339
    like '*' and '?' are supported, see documentation for the 'glob' module for
 
340
    more information.
 
341
 
 
342
    :return: dict: A { filename: hash } dictionary for all matched files.
 
343
                   Empty if none found.
 
344
    """
 
345
    return {
 
346
        filename: file_hash(filename)
 
347
        for filename in glob.iglob(path)
 
348
    }
 
349
 
 
350
 
 
351
def check_hash(path, checksum, hash_type='md5'):
 
352
    """
 
353
    Validate a file using a cryptographic checksum.
 
354
 
 
355
    :param str checksum: Value of the checksum used to validate the file.
 
356
    :param str hash_type: Hash algorithm used to generate `checksum`.
 
357
        Can be any hash alrgorithm supported by :mod:`hashlib`,
 
358
        such as md5, sha1, sha256, sha512, etc.
 
359
    :raises ChecksumError: If the file fails the checksum
 
360
 
 
361
    """
 
362
    actual_checksum = file_hash(path, hash_type)
 
363
    if checksum != actual_checksum:
 
364
        raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
 
365
 
 
366
 
 
367
class ChecksumError(ValueError):
 
368
    pass
 
369
 
 
370
 
 
371
def restart_on_change(restart_map, stopstart=False):
 
372
    """Restart services based on configuration files changing
 
373
 
 
374
    This function is used a decorator, for example::
 
375
 
 
376
        @restart_on_change({
 
377
            '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
 
378
            '/etc/apache/sites-enabled/*': [ 'apache2' ]
 
379
            })
 
380
        def config_changed():
 
381
            pass  # your code here
 
382
 
 
383
    In this example, the cinder-api and cinder-volume services
 
384
    would be restarted if /etc/ceph/ceph.conf is changed by the
 
385
    ceph_client_changed function. The apache2 service would be
 
386
    restarted if any file matching the pattern got changed, created
 
387
    or removed. Standard wildcards are supported, see documentation
 
388
    for the 'glob' module for more information.
 
389
    """
 
390
    def wrap(f):
 
391
        def wrapped_f(*args, **kwargs):
 
392
            checksums = {path: path_hash(path) for path in restart_map}
 
393
            f(*args, **kwargs)
 
394
            restarts = []
 
395
            for path in restart_map:
 
396
                if path_hash(path) != checksums[path]:
 
397
                    restarts += restart_map[path]
 
398
            services_list = list(OrderedDict.fromkeys(restarts))
 
399
            if not stopstart:
 
400
                for service_name in services_list:
 
401
                    service('restart', service_name)
 
402
            else:
 
403
                for action in ['stop', 'start']:
 
404
                    for service_name in services_list:
 
405
                        service(action, service_name)
 
406
        return wrapped_f
 
407
    return wrap
 
408
 
 
409
 
 
410
def lsb_release():
 
411
    """Return /etc/lsb-release in a dict"""
 
412
    d = {}
 
413
    with open('/etc/lsb-release', 'r') as lsb:
 
414
        for l in lsb:
 
415
            k, v = l.split('=')
 
416
            d[k.strip()] = v.strip()
 
417
    return d
 
418
 
 
419
 
 
420
def pwgen(length=None):
 
421
    """Generate a random pasword."""
 
422
    if length is None:
 
423
        # A random length is ok to use a weak PRNG
 
424
        length = random.choice(range(35, 45))
 
425
    alphanumeric_chars = [
 
426
        l for l in (string.ascii_letters + string.digits)
 
427
        if l not in 'l0QD1vAEIOUaeiou']
 
428
    # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
 
429
    # actual password
 
430
    random_generator = random.SystemRandom()
 
431
    random_chars = [
 
432
        random_generator.choice(alphanumeric_chars) for _ in range(length)]
 
433
    return(''.join(random_chars))
 
434
 
 
435
 
 
436
def is_phy_iface(interface):
 
437
    """Returns True if interface is not virtual, otherwise False."""
 
438
    if interface:
 
439
        sys_net = '/sys/class/net'
 
440
        if os.path.isdir(sys_net):
 
441
            for iface in glob.glob(os.path.join(sys_net, '*')):
 
442
                if '/virtual/' in os.path.realpath(iface):
 
443
                    continue
 
444
 
 
445
                if interface == os.path.basename(iface):
 
446
                    return True
 
447
 
 
448
    return False
 
449
 
 
450
 
 
451
def get_bond_master(interface):
 
452
    """Returns bond master if interface is bond slave otherwise None.
 
453
 
 
454
    NOTE: the provided interface is expected to be physical
 
455
    """
 
456
    if interface:
 
457
        iface_path = '/sys/class/net/%s' % (interface)
 
458
        if os.path.exists(iface_path):
 
459
            if '/virtual/' in os.path.realpath(iface_path):
 
460
                return None
 
461
 
 
462
            master = os.path.join(iface_path, 'master')
 
463
            if os.path.exists(master):
 
464
                master = os.path.realpath(master)
 
465
                # make sure it is a bond master
 
466
                if os.path.exists(os.path.join(master, 'bonding')):
 
467
                    return os.path.basename(master)
 
468
 
 
469
    return None
 
470
 
 
471
 
 
472
def list_nics(nic_type=None):
 
473
    '''Return a list of nics of given type(s)'''
 
474
    if isinstance(nic_type, six.string_types):
 
475
        int_types = [nic_type]
 
476
    else:
 
477
        int_types = nic_type
 
478
 
 
479
    interfaces = []
 
480
    if nic_type:
 
481
        for int_type in int_types:
 
482
            cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
 
483
            ip_output = subprocess.check_output(cmd).decode('UTF-8')
 
484
            ip_output = ip_output.split('\n')
 
485
            ip_output = (line for line in ip_output if line)
 
486
            for line in ip_output:
 
487
                if line.split()[1].startswith(int_type):
 
488
                    matched = re.search('.*: (' + int_type +
 
489
                                        r'[0-9]+\.[0-9]+)@.*', line)
 
490
                    if matched:
 
491
                        iface = matched.groups()[0]
 
492
                    else:
 
493
                        iface = line.split()[1].replace(":", "")
 
494
 
 
495
                    if iface not in interfaces:
 
496
                        interfaces.append(iface)
 
497
    else:
 
498
        cmd = ['ip', 'a']
 
499
        ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
 
500
        ip_output = (line.strip() for line in ip_output if line)
 
501
 
 
502
        key = re.compile('^[0-9]+:\s+(.+):')
 
503
        for line in ip_output:
 
504
            matched = re.search(key, line)
 
505
            if matched:
 
506
                iface = matched.group(1)
 
507
                iface = iface.partition("@")[0]
 
508
                if iface not in interfaces:
 
509
                    interfaces.append(iface)
 
510
 
 
511
    return interfaces
 
512
 
 
513
 
 
514
def set_nic_mtu(nic, mtu):
 
515
    '''Set MTU on a network interface'''
 
516
    cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
 
517
    subprocess.check_call(cmd)
 
518
 
 
519
 
 
520
def get_nic_mtu(nic):
 
521
    cmd = ['ip', 'addr', 'show', nic]
 
522
    ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
 
523
    mtu = ""
 
524
    for line in ip_output:
 
525
        words = line.split()
 
526
        if 'mtu' in words:
 
527
            mtu = words[words.index("mtu") + 1]
 
528
    return mtu
 
529
 
 
530
 
 
531
def get_nic_hwaddr(nic):
 
532
    cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
 
533
    ip_output = subprocess.check_output(cmd).decode('UTF-8')
 
534
    hwaddr = ""
 
535
    words = ip_output.split()
 
536
    if 'link/ether' in words:
 
537
        hwaddr = words[words.index('link/ether') + 1]
 
538
    return hwaddr
 
539
 
 
540
 
 
541
def cmp_pkgrevno(package, revno, pkgcache=None):
 
542
    '''Compare supplied revno with the revno of the installed package
 
543
 
 
544
    *  1 => Installed revno is greater than supplied arg
 
545
    *  0 => Installed revno is the same as supplied arg
 
546
    * -1 => Installed revno is less than supplied arg
 
547
 
 
548
    This function imports apt_cache function from charmhelpers.fetch if
 
549
    the pkgcache argument is None. Be sure to add charmhelpers.fetch if
 
550
    you call this function, or pass an apt_pkg.Cache() instance.
 
551
    '''
 
552
    import apt_pkg
 
553
    if not pkgcache:
 
554
        from charmhelpers.fetch import apt_cache
 
555
        pkgcache = apt_cache()
 
556
    pkg = pkgcache[package]
 
557
    return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
 
558
 
 
559
 
 
560
@contextmanager
 
561
def chdir(d):
 
562
    cur = os.getcwd()
 
563
    try:
 
564
        yield os.chdir(d)
 
565
    finally:
 
566
        os.chdir(cur)
 
567
 
 
568
 
 
569
def chownr(path, owner, group, follow_links=True):
 
570
    uid = pwd.getpwnam(owner).pw_uid
 
571
    gid = grp.getgrnam(group).gr_gid
 
572
    if follow_links:
 
573
        chown = os.chown
 
574
    else:
 
575
        chown = os.lchown
 
576
 
 
577
    for root, dirs, files in os.walk(path):
 
578
        for name in dirs + files:
 
579
            full = os.path.join(root, name)
 
580
            broken_symlink = os.path.lexists(full) and not os.path.exists(full)
 
581
            if not broken_symlink:
 
582
                chown(full, uid, gid)
 
583
 
 
584
 
 
585
def lchownr(path, owner, group):
 
586
    chownr(path, owner, group, follow_links=False)