~openstack-charmers-next/charms/vivid/hacluster/trunk

« back to all changes in this revision

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

  • Committer: Liam Young
  • Date: 2016-03-30 09:07:46 UTC
  • mfrom: (63.1.4 trunk)
  • Revision ID: liam.young@canonical.com-20160330090746-rwwe91yjqi9j9ry3
[gnuoy, r=james-page] Add pause/resume actions

Show diffs side-by-side

added added

removed removed

Lines of Context:
30
30
import string
31
31
import subprocess
32
32
import hashlib
 
33
import functools
 
34
import itertools
33
35
from contextlib import contextmanager
34
36
from collections import OrderedDict
35
37
 
63
65
    return service_result
64
66
 
65
67
 
66
 
def service_pause(service_name, init_dir=None):
 
68
def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
67
69
    """Pause a system service.
68
70
 
69
71
    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, '{}.conf.override'.format(service_name))
76
 
    with open(override_path, 'w') as fh:
77
 
        fh.write("manual\n")
 
72
    stopped = True
 
73
    if service_running(service_name):
 
74
        stopped = service_stop(service_name)
 
75
    upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
 
76
    sysv_file = os.path.join(initd_dir, service_name)
 
77
    if init_is_systemd():
 
78
        service('disable', service_name)
 
79
    elif os.path.exists(upstart_file):
 
80
        override_path = os.path.join(
 
81
            init_dir, '{}.override'.format(service_name))
 
82
        with open(override_path, 'w') as fh:
 
83
            fh.write("manual\n")
 
84
    elif os.path.exists(sysv_file):
 
85
        subprocess.check_call(["update-rc.d", service_name, "disable"])
 
86
    else:
 
87
        raise ValueError(
 
88
            "Unable to detect {0} as SystemD, Upstart {1} or"
 
89
            " SysV {2}".format(
 
90
                service_name, upstart_file, sysv_file))
78
91
    return stopped
79
92
 
80
93
 
81
 
def service_resume(service_name, init_dir=None):
 
94
def service_resume(service_name, init_dir="/etc/init",
 
95
                   initd_dir="/etc/init.d"):
82
96
    """Resume a system service.
83
97
 
84
98
    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, '{}.conf.override'.format(service_name))
90
 
    if os.path.exists(override_path):
91
 
        os.unlink(override_path)
92
 
    started = service_start(service_name)
 
99
    upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
 
100
    sysv_file = os.path.join(initd_dir, service_name)
 
101
    if init_is_systemd():
 
102
        service('enable', service_name)
 
103
    elif os.path.exists(upstart_file):
 
104
        override_path = os.path.join(
 
105
            init_dir, '{}.override'.format(service_name))
 
106
        if os.path.exists(override_path):
 
107
            os.unlink(override_path)
 
108
    elif os.path.exists(sysv_file):
 
109
        subprocess.check_call(["update-rc.d", service_name, "enable"])
 
110
    else:
 
111
        raise ValueError(
 
112
            "Unable to detect {0} as SystemD, Upstart {1} or"
 
113
            " SysV {2}".format(
 
114
                service_name, upstart_file, sysv_file))
 
115
 
 
116
    started = service_running(service_name)
 
117
    if not started:
 
118
        started = service_start(service_name)
93
119
    return started
94
120
 
95
121
 
96
122
def service(action, service_name):
97
123
    """Control a system service"""
98
 
    cmd = ['service', service_name, action]
 
124
    if init_is_systemd():
 
125
        cmd = ['systemctl', action, service_name]
 
126
    else:
 
127
        cmd = ['service', service_name, action]
99
128
    return subprocess.call(cmd) == 0
100
129
 
101
130
 
102
 
def service_running(service):
 
131
def service_running(service_name):
103
132
    """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
 
133
    if init_is_systemd():
 
134
        return service('is-active', service_name)
110
135
    else:
111
 
        if ("start/running" in output or "is running" in output):
112
 
            return True
113
 
        else:
 
136
        try:
 
137
            output = subprocess.check_output(
 
138
                ['service', service_name, 'status'],
 
139
                stderr=subprocess.STDOUT).decode('UTF-8')
 
140
        except subprocess.CalledProcessError:
114
141
            return False
 
142
        else:
 
143
            if ("start/running" in output or "is running" in output or
 
144
                    "up and running" in output):
 
145
                return True
 
146
            else:
 
147
                return False
115
148
 
116
149
 
117
150
def service_available(service_name):
126
159
        return True
127
160
 
128
161
 
129
 
def adduser(username, password=None, shell='/bin/bash', system_user=False):
130
 
    """Add a user to the system"""
 
162
SYSTEMD_SYSTEM = '/run/systemd/system'
 
163
 
 
164
 
 
165
def init_is_systemd():
 
166
    """Return True if the host system uses systemd, False otherwise."""
 
167
    return os.path.isdir(SYSTEMD_SYSTEM)
 
168
 
 
169
 
 
170
def adduser(username, password=None, shell='/bin/bash', system_user=False,
 
171
            primary_group=None, secondary_groups=None):
 
172
    """Add a user to the system.
 
173
 
 
174
    Will log but otherwise succeed if the user already exists.
 
175
 
 
176
    :param str username: Username to create
 
177
    :param str password: Password for user; if ``None``, create a system user
 
178
    :param str shell: The default shell for the user
 
179
    :param bool system_user: Whether to create a login or system user
 
180
    :param str primary_group: Primary group for user; defaults to username
 
181
    :param list secondary_groups: Optional list of additional groups
 
182
 
 
183
    :returns: The password database entry struct, as returned by `pwd.getpwnam`
 
184
    """
131
185
    try:
132
186
        user_info = pwd.getpwnam(username)
133
187
        log('user {0} already exists!'.format(username))
142
196
                '--shell', shell,
143
197
                '--password', password,
144
198
            ])
 
199
        if not primary_group:
 
200
            try:
 
201
                grp.getgrnam(username)
 
202
                primary_group = username  # avoid "group exists" error
 
203
            except KeyError:
 
204
                pass
 
205
        if primary_group:
 
206
            cmd.extend(['-g', primary_group])
 
207
        if secondary_groups:
 
208
            cmd.extend(['-G', ','.join(secondary_groups)])
145
209
        cmd.append(username)
146
210
        subprocess.check_call(cmd)
147
211
        user_info = pwd.getpwnam(username)
148
212
    return user_info
149
213
 
150
214
 
 
215
def user_exists(username):
 
216
    """Check if a user exists"""
 
217
    try:
 
218
        pwd.getpwnam(username)
 
219
        user_exists = True
 
220
    except KeyError:
 
221
        user_exists = False
 
222
    return user_exists
 
223
 
 
224
 
151
225
def add_group(group_name, system_group=False):
152
226
    """Add a group to the system"""
153
227
    try:
229
303
 
230
304
 
231
305
def fstab_remove(mp):
232
 
    """Remove the given mountpoint entry from /etc/fstab
233
 
    """
 
306
    """Remove the given mountpoint entry from /etc/fstab"""
234
307
    return Fstab.remove_by_mountpoint(mp)
235
308
 
236
309
 
237
310
def fstab_add(dev, mp, fs, options=None):
238
 
    """Adds the given device entry to the /etc/fstab file
239
 
    """
 
311
    """Adds the given device entry to the /etc/fstab file"""
240
312
    return Fstab.add(dev, mp, fs, options=options)
241
313
 
242
314
 
280
352
    return system_mounts
281
353
 
282
354
 
 
355
def fstab_mount(mountpoint):
 
356
    """Mount filesystem using fstab"""
 
357
    cmd_args = ['mount', mountpoint]
 
358
    try:
 
359
        subprocess.check_output(cmd_args)
 
360
    except subprocess.CalledProcessError as e:
 
361
        log('Error unmounting {}\n{}'.format(mountpoint, e.output))
 
362
        return False
 
363
    return True
 
364
 
 
365
 
283
366
def file_hash(path, hash_type='md5'):
284
 
    """
285
 
    Generate a hash checksum of the contents of 'path' or None if not found.
 
367
    """Generate a hash checksum of the contents of 'path' or None if not found.
286
368
 
287
369
    :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
288
370
                          such as md5, sha1, sha256, sha512, etc.
297
379
 
298
380
 
299
381
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.
 
382
    """Generate a hash checksum of all files matching 'path'. Standard
 
383
    wildcards like '*' and '?' are supported, see documentation for the 'glob'
 
384
    module for more information.
304
385
 
305
386
    :return: dict: A { filename: hash } dictionary for all matched files.
306
387
                   Empty if none found.
312
393
 
313
394
 
314
395
def check_hash(path, checksum, hash_type='md5'):
315
 
    """
316
 
    Validate a file using a cryptographic checksum.
 
396
    """Validate a file using a cryptographic checksum.
317
397
 
318
398
    :param str checksum: Value of the checksum used to validate the file.
319
399
    :param str hash_type: Hash algorithm used to generate `checksum`.
328
408
 
329
409
 
330
410
class ChecksumError(ValueError):
 
411
    """A class derived from Value error to indicate the checksum failed."""
331
412
    pass
332
413
 
333
414
 
349
430
    restarted if any file matching the pattern got changed, created
350
431
    or removed. Standard wildcards are supported, see documentation
351
432
    for the 'glob' module for more information.
 
433
 
 
434
    @param restart_map: {path_file_name: [service_name, ...]
 
435
    @param stopstart: DEFAULT false; whether to stop, start OR restart
 
436
    @returns result from decorated function
352
437
    """
353
438
    def wrap(f):
 
439
        @functools.wraps(f)
354
440
        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)
 
441
            return restart_on_change_helper(
 
442
                (lambda: f(*args, **kwargs)), restart_map, stopstart)
369
443
        return wrapped_f
370
444
    return wrap
371
445
 
372
446
 
 
447
def restart_on_change_helper(lambda_f, restart_map, stopstart=False):
 
448
    """Helper function to perform the restart_on_change function.
 
449
 
 
450
    This is provided for decorators to restart services if files described
 
451
    in the restart_map have changed after an invocation of lambda_f().
 
452
 
 
453
    @param lambda_f: function to call.
 
454
    @param restart_map: {file: [service, ...]}
 
455
    @param stopstart: whether to stop, start or restart a service
 
456
    @returns result of lambda_f()
 
457
    """
 
458
    checksums = {path: path_hash(path) for path in restart_map}
 
459
    r = lambda_f()
 
460
    # create a list of lists of the services to restart
 
461
    restarts = [restart_map[path]
 
462
                for path in restart_map
 
463
                if path_hash(path) != checksums[path]]
 
464
    # create a flat list of ordered services without duplicates from lists
 
465
    services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
 
466
    if services_list:
 
467
        actions = ('stop', 'start') if stopstart else ('restart',)
 
468
        for action in actions:
 
469
            for service_name in services_list:
 
470
                service(action, service_name)
 
471
    return r
 
472
 
 
473
 
373
474
def lsb_release():
374
475
    """Return /etc/lsb-release in a dict"""
375
476
    d = {}
396
497
    return(''.join(random_chars))
397
498
 
398
499
 
399
 
def list_nics(nic_type):
400
 
    '''Return a list of nics of given type(s)'''
 
500
def is_phy_iface(interface):
 
501
    """Returns True if interface is not virtual, otherwise False."""
 
502
    if interface:
 
503
        sys_net = '/sys/class/net'
 
504
        if os.path.isdir(sys_net):
 
505
            for iface in glob.glob(os.path.join(sys_net, '*')):
 
506
                if '/virtual/' in os.path.realpath(iface):
 
507
                    continue
 
508
 
 
509
                if interface == os.path.basename(iface):
 
510
                    return True
 
511
 
 
512
    return False
 
513
 
 
514
 
 
515
def get_bond_master(interface):
 
516
    """Returns bond master if interface is bond slave otherwise None.
 
517
 
 
518
    NOTE: the provided interface is expected to be physical
 
519
    """
 
520
    if interface:
 
521
        iface_path = '/sys/class/net/%s' % (interface)
 
522
        if os.path.exists(iface_path):
 
523
            if '/virtual/' in os.path.realpath(iface_path):
 
524
                return None
 
525
 
 
526
            master = os.path.join(iface_path, 'master')
 
527
            if os.path.exists(master):
 
528
                master = os.path.realpath(master)
 
529
                # make sure it is a bond master
 
530
                if os.path.exists(os.path.join(master, 'bonding')):
 
531
                    return os.path.basename(master)
 
532
 
 
533
    return None
 
534
 
 
535
 
 
536
def list_nics(nic_type=None):
 
537
    """Return a list of nics of given type(s)"""
401
538
    if isinstance(nic_type, six.string_types):
402
539
        int_types = [nic_type]
403
540
    else:
404
541
        int_types = nic_type
 
542
 
405
543
    interfaces = []
406
 
    for int_type in int_types:
407
 
        cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
 
544
    if nic_type:
 
545
        for int_type in int_types:
 
546
            cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
 
547
            ip_output = subprocess.check_output(cmd).decode('UTF-8')
 
548
            ip_output = ip_output.split('\n')
 
549
            ip_output = (line for line in ip_output if line)
 
550
            for line in ip_output:
 
551
                if line.split()[1].startswith(int_type):
 
552
                    matched = re.search('.*: (' + int_type +
 
553
                                        r'[0-9]+\.[0-9]+)@.*', line)
 
554
                    if matched:
 
555
                        iface = matched.groups()[0]
 
556
                    else:
 
557
                        iface = line.split()[1].replace(":", "")
 
558
 
 
559
                    if iface not in interfaces:
 
560
                        interfaces.append(iface)
 
561
    else:
 
562
        cmd = ['ip', 'a']
408
563
        ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
409
 
        ip_output = (line for line in ip_output if line)
 
564
        ip_output = (line.strip() for line in ip_output if line)
 
565
 
 
566
        key = re.compile('^[0-9]+:\s+(.+):')
410
567
        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)
 
568
            matched = re.search(key, line)
 
569
            if matched:
 
570
                iface = matched.group(1)
 
571
                iface = iface.partition("@")[0]
 
572
                if iface not in interfaces:
 
573
                    interfaces.append(iface)
418
574
 
419
575
    return interfaces
420
576
 
421
577
 
422
578
def set_nic_mtu(nic, mtu):
423
 
    '''Set MTU on a network interface'''
 
579
    """Set the Maximum Transmission Unit (MTU) on a network interface."""
424
580
    cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
425
581
    subprocess.check_call(cmd)
426
582
 
427
583
 
428
584
def get_nic_mtu(nic):
 
585
    """Return the Maximum Transmission Unit (MTU) for a network interface."""
429
586
    cmd = ['ip', 'addr', 'show', nic]
430
587
    ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
431
588
    mtu = ""
437
594
 
438
595
 
439
596
def get_nic_hwaddr(nic):
 
597
    """Return the Media Access Control (MAC) for a network interface."""
440
598
    cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
441
599
    ip_output = subprocess.check_output(cmd).decode('UTF-8')
442
600
    hwaddr = ""
447
605
 
448
606
 
449
607
def cmp_pkgrevno(package, revno, pkgcache=None):
450
 
    '''Compare supplied revno with the revno of the installed package
 
608
    """Compare supplied revno with the revno of the installed package
451
609
 
452
610
    *  1 => Installed revno is greater than supplied arg
453
611
    *  0 => Installed revno is the same as supplied arg
456
614
    This function imports apt_cache function from charmhelpers.fetch if
457
615
    the pkgcache argument is None. Be sure to add charmhelpers.fetch if
458
616
    you call this function, or pass an apt_pkg.Cache() instance.
459
 
    '''
 
617
    """
460
618
    import apt_pkg
461
619
    if not pkgcache:
462
620
        from charmhelpers.fetch import apt_cache
466
624
 
467
625
 
468
626
@contextmanager
469
 
def chdir(d):
 
627
def chdir(directory):
 
628
    """Change the current working directory to a different directory for a code
 
629
    block and return the previous directory after the block exits. Useful to
 
630
    run commands from a specificed directory.
 
631
 
 
632
    :param str directory: The directory path to change to for this context.
 
633
    """
470
634
    cur = os.getcwd()
471
635
    try:
472
 
        yield os.chdir(d)
 
636
        yield os.chdir(directory)
473
637
    finally:
474
638
        os.chdir(cur)
475
639
 
476
640
 
477
 
def chownr(path, owner, group, follow_links=True):
 
641
def chownr(path, owner, group, follow_links=True, chowntopdir=False):
 
642
    """Recursively change user and group ownership of files and directories
 
643
    in given path. Doesn't chown path itself by default, only its children.
 
644
 
 
645
    :param str path: The string path to start changing ownership.
 
646
    :param str owner: The owner string to use when looking up the uid.
 
647
    :param str group: The group string to use when looking up the gid.
 
648
    :param bool follow_links: Also Chown links if True
 
649
    :param bool chowntopdir: Also chown path itself if True
 
650
    """
478
651
    uid = pwd.getpwnam(owner).pw_uid
479
652
    gid = grp.getgrnam(group).gr_gid
480
653
    if follow_links:
482
655
    else:
483
656
        chown = os.lchown
484
657
 
 
658
    if chowntopdir:
 
659
        broken_symlink = os.path.lexists(path) and not os.path.exists(path)
 
660
        if not broken_symlink:
 
661
            chown(path, uid, gid)
485
662
    for root, dirs, files in os.walk(path):
486
663
        for name in dirs + files:
487
664
            full = os.path.join(root, name)
491
668
 
492
669
 
493
670
def lchownr(path, owner, group):
 
671
    """Recursively change user and group ownership of files and directories
 
672
    in a given path, not following symbolic links. See the documentation for
 
673
    'os.lchown' for more information.
 
674
 
 
675
    :param str path: The string path to start changing ownership.
 
676
    :param str owner: The owner string to use when looking up the uid.
 
677
    :param str group: The group string to use when looking up the gid.
 
678
    """
494
679
    chownr(path, owner, group, follow_links=False)
 
680
 
 
681
 
 
682
def get_total_ram():
 
683
    """The total amount of system RAM in bytes.
 
684
 
 
685
    This is what is reported by the OS, and may be overcommitted when
 
686
    there are multiple containers hosted on the same machine.
 
687
    """
 
688
    with open('/proc/meminfo', 'r') as f:
 
689
        for line in f.readlines():
 
690
            if line:
 
691
                key, value, unit = line.split()
 
692
                if key == 'MemTotal:':
 
693
                    assert unit == 'kB', 'Unknown unit'
 
694
                    return int(value) * 1024  # Classic, not KiB.
 
695
        raise NotImplementedError()