~hopem/charms/trusty/swift-proxy/lp1510865-stable-backport

« back to all changes in this revision

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

  • Committer: James Page
  • Date: 2015-10-22 13:24:57 UTC
  • Revision ID: james.page@ubuntu.com-20151022132457-4p14oifelnzjz5n3
Tags: 15.10
15.10 Charm release

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