* sync charm-helpers to classic charms * change openstack-origin/source default to antelope * align testing with antelope * add new antelope bundles * add antelope bundles to tests.yaml * add antelope tests to osci.yaml and .zuul.yaml * update build-on and run-on bases Change-Id: Iaeabaaf9d59fc8b50a685ddc5c91f4cf4f2b748f
		
			
				
	
	
		
			1307 lines
		
	
	
		
			48 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1307 lines
		
	
	
		
			48 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# Copyright 2014-2021 Canonical Limited.
 | 
						|
#
 | 
						|
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
# you may not use this file except in compliance with the License.
 | 
						|
# You may obtain a copy of the License at
 | 
						|
#
 | 
						|
#  http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
#
 | 
						|
# Unless required by applicable law or agreed to in writing, software
 | 
						|
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
# See the License for the specific language governing permissions and
 | 
						|
# limitations under the License.
 | 
						|
 | 
						|
"""Tools for working with the host system"""
 | 
						|
# Copyright 2012 Canonical Ltd.
 | 
						|
#
 | 
						|
# Authors:
 | 
						|
#  Nick Moffitt <nick.moffitt@canonical.com>
 | 
						|
#  Matthew Wedgwood <matthew.wedgwood@canonical.com>
 | 
						|
 | 
						|
import errno
 | 
						|
import os
 | 
						|
import re
 | 
						|
import pwd
 | 
						|
import glob
 | 
						|
import grp
 | 
						|
import random
 | 
						|
import string
 | 
						|
import subprocess
 | 
						|
import hashlib
 | 
						|
import functools
 | 
						|
import itertools
 | 
						|
 | 
						|
from contextlib import contextmanager
 | 
						|
from collections import OrderedDict, defaultdict
 | 
						|
from .hookenv import log, INFO, DEBUG, local_unit, charm_name
 | 
						|
from .fstab import Fstab
 | 
						|
from charmhelpers.osplatform import get_platform
 | 
						|
 | 
						|
__platform__ = get_platform()
 | 
						|
if __platform__ == "ubuntu":
 | 
						|
    from charmhelpers.core.host_factory.ubuntu import (  # NOQA:F401
 | 
						|
        service_available,
 | 
						|
        add_new_group,
 | 
						|
        lsb_release,
 | 
						|
        cmp_pkgrevno,
 | 
						|
        CompareHostReleases,
 | 
						|
        get_distrib_codename,
 | 
						|
        arch
 | 
						|
    )  # flake8: noqa -- ignore F401 for this import
 | 
						|
elif __platform__ == "centos":
 | 
						|
    from charmhelpers.core.host_factory.centos import (  # NOQA:F401
 | 
						|
        service_available,
 | 
						|
        add_new_group,
 | 
						|
        lsb_release,
 | 
						|
        cmp_pkgrevno,
 | 
						|
        CompareHostReleases,
 | 
						|
    )  # flake8: noqa -- ignore F401 for this import
 | 
						|
 | 
						|
UPDATEDB_PATH = '/etc/updatedb.conf'
 | 
						|
CA_CERT_DIR = '/usr/local/share/ca-certificates'
 | 
						|
 | 
						|
 | 
						|
def service_start(service_name, **kwargs):
 | 
						|
    """Start a system service.
 | 
						|
 | 
						|
    The specified service name is managed via the system level init system.
 | 
						|
    Some init systems (e.g. upstart) require that additional arguments be
 | 
						|
    provided in order to directly control service instances whereas other init
 | 
						|
    systems allow for addressing instances of a service directly by name (e.g.
 | 
						|
    systemd).
 | 
						|
 | 
						|
    The kwargs allow for the additional parameters to be passed to underlying
 | 
						|
    init systems for those systems which require/allow for them. For example,
 | 
						|
    the ceph-osd upstart script requires the id parameter to be passed along
 | 
						|
    in order to identify which running daemon should be reloaded. The follow-
 | 
						|
    ing example stops the ceph-osd service for instance id=4:
 | 
						|
 | 
						|
    service_stop('ceph-osd', id=4)
 | 
						|
 | 
						|
    :param service_name: the name of the service to stop
 | 
						|
    :param **kwargs: additional parameters to pass to the init system when
 | 
						|
                     managing services. These will be passed as key=value
 | 
						|
                     parameters to the init system's commandline. kwargs
 | 
						|
                     are ignored for systemd enabled systems.
 | 
						|
    """
 | 
						|
    return service('start', service_name, **kwargs)
 | 
						|
 | 
						|
 | 
						|
def service_stop(service_name, **kwargs):
 | 
						|
    """Stop a system service.
 | 
						|
 | 
						|
    The specified service name is managed via the system level init system.
 | 
						|
    Some init systems (e.g. upstart) require that additional arguments be
 | 
						|
    provided in order to directly control service instances whereas other init
 | 
						|
    systems allow for addressing instances of a service directly by name (e.g.
 | 
						|
    systemd).
 | 
						|
 | 
						|
    The kwargs allow for the additional parameters to be passed to underlying
 | 
						|
    init systems for those systems which require/allow for them. For example,
 | 
						|
    the ceph-osd upstart script requires the id parameter to be passed along
 | 
						|
    in order to identify which running daemon should be reloaded. The follow-
 | 
						|
    ing example stops the ceph-osd service for instance id=4:
 | 
						|
 | 
						|
    service_stop('ceph-osd', id=4)
 | 
						|
 | 
						|
    :param service_name: the name of the service to stop
 | 
						|
    :param **kwargs: additional parameters to pass to the init system when
 | 
						|
                     managing services. These will be passed as key=value
 | 
						|
                     parameters to the init system's commandline. kwargs
 | 
						|
                     are ignored for systemd enabled systems.
 | 
						|
    """
 | 
						|
    return service('stop', service_name, **kwargs)
 | 
						|
 | 
						|
 | 
						|
def service_enable(service_name, **kwargs):
 | 
						|
    """Enable a system service.
 | 
						|
 | 
						|
    The specified service name is managed via the system level init system.
 | 
						|
    Some init systems (e.g. upstart) require that additional arguments be
 | 
						|
    provided in order to directly control service instances whereas other init
 | 
						|
    systems allow for addressing instances of a service directly by name (e.g.
 | 
						|
    systemd).
 | 
						|
 | 
						|
    The kwargs allow for the additional parameters to be passed to underlying
 | 
						|
    init systems for those systems which require/allow for them. For example,
 | 
						|
    the ceph-osd upstart script requires the id parameter to be passed along
 | 
						|
    in order to identify which running daemon should be restarted. The follow-
 | 
						|
    ing example restarts the ceph-osd service for instance id=4:
 | 
						|
 | 
						|
    service_enable('ceph-osd', id=4)
 | 
						|
 | 
						|
    :param service_name: the name of the service to enable
 | 
						|
    :param **kwargs: additional parameters to pass to the init system when
 | 
						|
                     managing services. These will be passed as key=value
 | 
						|
                     parameters to the init system's commandline. kwargs
 | 
						|
                     are ignored for init systems not allowing additional
 | 
						|
                     parameters via the commandline (systemd).
 | 
						|
    """
 | 
						|
    return service('enable', service_name, **kwargs)
 | 
						|
 | 
						|
 | 
						|
def service_restart(service_name, **kwargs):
 | 
						|
    """Restart a system service.
 | 
						|
 | 
						|
    The specified service name is managed via the system level init system.
 | 
						|
    Some init systems (e.g. upstart) require that additional arguments be
 | 
						|
    provided in order to directly control service instances whereas other init
 | 
						|
    systems allow for addressing instances of a service directly by name (e.g.
 | 
						|
    systemd).
 | 
						|
 | 
						|
    The kwargs allow for the additional parameters to be passed to underlying
 | 
						|
    init systems for those systems which require/allow for them. For example,
 | 
						|
    the ceph-osd upstart script requires the id parameter to be passed along
 | 
						|
    in order to identify which running daemon should be restarted. The follow-
 | 
						|
    ing example restarts the ceph-osd service for instance id=4:
 | 
						|
 | 
						|
    service_restart('ceph-osd', id=4)
 | 
						|
 | 
						|
    :param service_name: the name of the service to restart
 | 
						|
    :param **kwargs: additional parameters to pass to the init system when
 | 
						|
                     managing services. These will be passed as key=value
 | 
						|
                     parameters to the init system's commandline. kwargs
 | 
						|
                     are ignored for init systems not allowing additional
 | 
						|
                     parameters via the commandline (systemd).
 | 
						|
    """
 | 
						|
    return service('restart', service_name)
 | 
						|
 | 
						|
 | 
						|
def service_reload(service_name, restart_on_failure=False, **kwargs):
 | 
						|
    """Reload a system service, optionally falling back to restart if
 | 
						|
    reload fails.
 | 
						|
 | 
						|
    The specified service name is managed via the system level init system.
 | 
						|
    Some init systems (e.g. upstart) require that additional arguments be
 | 
						|
    provided in order to directly control service instances whereas other init
 | 
						|
    systems allow for addressing instances of a service directly by name (e.g.
 | 
						|
    systemd).
 | 
						|
 | 
						|
    The kwargs allow for the additional parameters to be passed to underlying
 | 
						|
    init systems for those systems which require/allow for them. For example,
 | 
						|
    the ceph-osd upstart script requires the id parameter to be passed along
 | 
						|
    in order to identify which running daemon should be reloaded. The follow-
 | 
						|
    ing example restarts the ceph-osd service for instance id=4:
 | 
						|
 | 
						|
    service_reload('ceph-osd', id=4)
 | 
						|
 | 
						|
    :param service_name: the name of the service to reload
 | 
						|
    :param restart_on_failure: boolean indicating whether to fallback to a
 | 
						|
                               restart if the reload fails.
 | 
						|
    :param **kwargs: additional parameters to pass to the init system when
 | 
						|
                     managing services. These will be passed as key=value
 | 
						|
                     parameters to the  init system's commandline. kwargs
 | 
						|
                     are ignored for init systems not allowing additional
 | 
						|
                     parameters via the commandline (systemd).
 | 
						|
    """
 | 
						|
    service_result = service('reload', service_name, **kwargs)
 | 
						|
    if not service_result and restart_on_failure:
 | 
						|
        service_result = service('restart', service_name, **kwargs)
 | 
						|
    return service_result
 | 
						|
 | 
						|
 | 
						|
def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
 | 
						|
                  **kwargs):
 | 
						|
    """Pause a system service.
 | 
						|
 | 
						|
    Stop it, and prevent it from starting again at boot.
 | 
						|
 | 
						|
    :param service_name: the name of the service to pause
 | 
						|
    :param init_dir: path to the upstart init directory
 | 
						|
    :param initd_dir: path to the sysv init directory
 | 
						|
    :param **kwargs: additional parameters to pass to the init system when
 | 
						|
                     managing services. These will be passed as key=value
 | 
						|
                     parameters to the init system's commandline. kwargs
 | 
						|
                     are ignored for init systems which do not support
 | 
						|
                     key=value arguments via the commandline.
 | 
						|
    """
 | 
						|
    stopped = True
 | 
						|
    if service_running(service_name, **kwargs):
 | 
						|
        stopped = service_stop(service_name, **kwargs)
 | 
						|
    upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
 | 
						|
    sysv_file = os.path.join(initd_dir, service_name)
 | 
						|
    if init_is_systemd(service_name=service_name):
 | 
						|
        service('disable', service_name)
 | 
						|
        service('mask', service_name)
 | 
						|
    elif os.path.exists(upstart_file):
 | 
						|
        override_path = os.path.join(
 | 
						|
            init_dir, '{}.override'.format(service_name))
 | 
						|
        with open(override_path, 'w') as fh:
 | 
						|
            fh.write("manual\n")
 | 
						|
    elif os.path.exists(sysv_file):
 | 
						|
        subprocess.check_call(["update-rc.d", service_name, "disable"])
 | 
						|
    else:
 | 
						|
        raise ValueError(
 | 
						|
            "Unable to detect {0} as SystemD, Upstart {1} or"
 | 
						|
            " SysV {2}".format(
 | 
						|
                service_name, upstart_file, sysv_file))
 | 
						|
    return stopped
 | 
						|
 | 
						|
 | 
						|
def service_resume(service_name, init_dir="/etc/init",
 | 
						|
                   initd_dir="/etc/init.d", **kwargs):
 | 
						|
    """Resume a system service.
 | 
						|
 | 
						|
    Re-enable starting again at boot. Start the service.
 | 
						|
 | 
						|
    :param service_name: the name of the service to resume
 | 
						|
    :param init_dir: the path to the init dir
 | 
						|
    :param initd dir: the path to the initd dir
 | 
						|
    :param **kwargs: additional parameters to pass to the init system when
 | 
						|
                     managing services. These will be passed as key=value
 | 
						|
                     parameters to the init system's commandline. kwargs
 | 
						|
                     are ignored for systemd enabled systems.
 | 
						|
    """
 | 
						|
    upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
 | 
						|
    sysv_file = os.path.join(initd_dir, service_name)
 | 
						|
    if init_is_systemd(service_name=service_name):
 | 
						|
        service('unmask', service_name)
 | 
						|
        service('enable', service_name)
 | 
						|
    elif os.path.exists(upstart_file):
 | 
						|
        override_path = os.path.join(
 | 
						|
            init_dir, '{}.override'.format(service_name))
 | 
						|
        if os.path.exists(override_path):
 | 
						|
            os.unlink(override_path)
 | 
						|
    elif os.path.exists(sysv_file):
 | 
						|
        subprocess.check_call(["update-rc.d", service_name, "enable"])
 | 
						|
    else:
 | 
						|
        raise ValueError(
 | 
						|
            "Unable to detect {0} as SystemD, Upstart {1} or"
 | 
						|
            " SysV {2}".format(
 | 
						|
                service_name, upstart_file, sysv_file))
 | 
						|
    started = service_running(service_name, **kwargs)
 | 
						|
 | 
						|
    if not started:
 | 
						|
        started = service_start(service_name, **kwargs)
 | 
						|
    return started
 | 
						|
 | 
						|
 | 
						|
def service(action, service_name=None, **kwargs):
 | 
						|
    """Control a system service.
 | 
						|
 | 
						|
    :param action: the action to take on the service
 | 
						|
    :param service_name: the name of the service to perform th action on
 | 
						|
    :param **kwargs: additional params to be passed to the service command in
 | 
						|
                    the form of key=value.
 | 
						|
    """
 | 
						|
    if init_is_systemd(service_name=service_name):
 | 
						|
        cmd = ['systemctl', action]
 | 
						|
        if service_name is not None:
 | 
						|
            cmd.append(service_name)
 | 
						|
    else:
 | 
						|
        cmd = ['service', service_name, action]
 | 
						|
        for key, value in kwargs.items():
 | 
						|
            parameter = '%s=%s' % (key, value)
 | 
						|
            cmd.append(parameter)
 | 
						|
    return subprocess.call(cmd) == 0
 | 
						|
 | 
						|
 | 
						|
_UPSTART_CONF = "/etc/init/{}.conf"
 | 
						|
_INIT_D_CONF = "/etc/init.d/{}"
 | 
						|
 | 
						|
 | 
						|
def service_running(service_name, **kwargs):
 | 
						|
    """Determine whether a system service is running.
 | 
						|
 | 
						|
    :param service_name: the name of the service
 | 
						|
    :param **kwargs: additional args to pass to the service command. This is
 | 
						|
                     used to pass additional key=value arguments to the
 | 
						|
                     service command line for managing specific instance
 | 
						|
                     units (e.g. service ceph-osd status id=2). The kwargs
 | 
						|
                     are ignored in systemd services.
 | 
						|
    """
 | 
						|
    if init_is_systemd(service_name=service_name):
 | 
						|
        return service('is-active', service_name)
 | 
						|
    else:
 | 
						|
        if os.path.exists(_UPSTART_CONF.format(service_name)):
 | 
						|
            try:
 | 
						|
                cmd = ['status', service_name]
 | 
						|
                for key, value in kwargs.items():
 | 
						|
                    parameter = '%s=%s' % (key, value)
 | 
						|
                    cmd.append(parameter)
 | 
						|
                output = subprocess.check_output(
 | 
						|
                    cmd, stderr=subprocess.STDOUT).decode('UTF-8')
 | 
						|
            except subprocess.CalledProcessError:
 | 
						|
                return False
 | 
						|
            else:
 | 
						|
                # This works for upstart scripts where the 'service' command
 | 
						|
                # returns a consistent string to represent running
 | 
						|
                # 'start/running'
 | 
						|
                if ("start/running" in output or
 | 
						|
                        "is running" in output or
 | 
						|
                        "up and running" in output):
 | 
						|
                    return True
 | 
						|
        elif os.path.exists(_INIT_D_CONF.format(service_name)):
 | 
						|
            # Check System V scripts init script return codes
 | 
						|
            return service('status', service_name)
 | 
						|
        return False
 | 
						|
 | 
						|
 | 
						|
SYSTEMD_SYSTEM = '/run/systemd/system'
 | 
						|
 | 
						|
 | 
						|
def init_is_systemd(service_name=None):
 | 
						|
    """
 | 
						|
    Returns whether the host uses systemd for the specified service.
 | 
						|
 | 
						|
    @param Optional[str] service_name: specific name of service
 | 
						|
    """
 | 
						|
    if str(service_name).startswith("snap."):
 | 
						|
        return True
 | 
						|
    if lsb_release()['DISTRIB_CODENAME'] == 'trusty':
 | 
						|
        return False
 | 
						|
    return os.path.isdir(SYSTEMD_SYSTEM)
 | 
						|
 | 
						|
 | 
						|
def adduser(username, password=None, shell='/bin/bash',
 | 
						|
            system_user=False, primary_group=None,
 | 
						|
            secondary_groups=None, uid=None, home_dir=None):
 | 
						|
    """Add a user to the system.
 | 
						|
 | 
						|
    Will log but otherwise succeed if the user already exists.
 | 
						|
 | 
						|
    :param str username: Username to create
 | 
						|
    :param str password: Password for user; if ``None``, create a system user
 | 
						|
    :param str shell: The default shell for the user
 | 
						|
    :param bool system_user: Whether to create a login or system user
 | 
						|
    :param str primary_group: Primary group for user; defaults to username
 | 
						|
    :param list secondary_groups: Optional list of additional groups
 | 
						|
    :param int uid: UID for user being created
 | 
						|
    :param str home_dir: Home directory for user
 | 
						|
 | 
						|
    :returns: The password database entry struct, as returned by `pwd.getpwnam`
 | 
						|
    """
 | 
						|
    try:
 | 
						|
        user_info = pwd.getpwnam(username)
 | 
						|
        log('user {0} already exists!'.format(username))
 | 
						|
        if uid:
 | 
						|
            user_info = pwd.getpwuid(int(uid))
 | 
						|
            log('user with uid {0} already exists!'.format(uid))
 | 
						|
    except KeyError:
 | 
						|
        log('creating user {0}'.format(username))
 | 
						|
        cmd = ['useradd']
 | 
						|
        if uid:
 | 
						|
            cmd.extend(['--uid', str(uid)])
 | 
						|
        if home_dir:
 | 
						|
            cmd.extend(['--home', str(home_dir)])
 | 
						|
        if system_user or password is None:
 | 
						|
            cmd.append('--system')
 | 
						|
        else:
 | 
						|
            cmd.extend([
 | 
						|
                '--create-home',
 | 
						|
                '--shell', shell,
 | 
						|
                '--password', password,
 | 
						|
            ])
 | 
						|
        if not primary_group:
 | 
						|
            try:
 | 
						|
                grp.getgrnam(username)
 | 
						|
                primary_group = username  # avoid "group exists" error
 | 
						|
            except KeyError:
 | 
						|
                pass
 | 
						|
        if primary_group:
 | 
						|
            cmd.extend(['-g', primary_group])
 | 
						|
        if secondary_groups:
 | 
						|
            cmd.extend(['-G', ','.join(secondary_groups)])
 | 
						|
        cmd.append(username)
 | 
						|
        subprocess.check_call(cmd)
 | 
						|
        user_info = pwd.getpwnam(username)
 | 
						|
    return user_info
 | 
						|
 | 
						|
 | 
						|
def user_exists(username):
 | 
						|
    """Check if a user exists"""
 | 
						|
    try:
 | 
						|
        pwd.getpwnam(username)
 | 
						|
        user_exists = True
 | 
						|
    except KeyError:
 | 
						|
        user_exists = False
 | 
						|
    return user_exists
 | 
						|
 | 
						|
 | 
						|
def uid_exists(uid):
 | 
						|
    """Check if a uid exists"""
 | 
						|
    try:
 | 
						|
        pwd.getpwuid(uid)
 | 
						|
        uid_exists = True
 | 
						|
    except KeyError:
 | 
						|
        uid_exists = False
 | 
						|
    return uid_exists
 | 
						|
 | 
						|
 | 
						|
def group_exists(groupname):
 | 
						|
    """Check if a group exists"""
 | 
						|
    try:
 | 
						|
        grp.getgrnam(groupname)
 | 
						|
        group_exists = True
 | 
						|
    except KeyError:
 | 
						|
        group_exists = False
 | 
						|
    return group_exists
 | 
						|
 | 
						|
 | 
						|
def gid_exists(gid):
 | 
						|
    """Check if a gid exists"""
 | 
						|
    try:
 | 
						|
        grp.getgrgid(gid)
 | 
						|
        gid_exists = True
 | 
						|
    except KeyError:
 | 
						|
        gid_exists = False
 | 
						|
    return gid_exists
 | 
						|
 | 
						|
 | 
						|
def add_group(group_name, system_group=False, gid=None):
 | 
						|
    """Add a group to the system
 | 
						|
 | 
						|
    Will log but otherwise succeed if the group already exists.
 | 
						|
 | 
						|
    :param str group_name: group to create
 | 
						|
    :param bool system_group: Create system group
 | 
						|
    :param int gid: GID for user being created
 | 
						|
 | 
						|
    :returns: The password database entry struct, as returned by `grp.getgrnam`
 | 
						|
    """
 | 
						|
    try:
 | 
						|
        group_info = grp.getgrnam(group_name)
 | 
						|
        log('group {0} already exists!'.format(group_name))
 | 
						|
        if gid:
 | 
						|
            group_info = grp.getgrgid(gid)
 | 
						|
            log('group with gid {0} already exists!'.format(gid))
 | 
						|
    except KeyError:
 | 
						|
        log('creating group {0}'.format(group_name))
 | 
						|
        add_new_group(group_name, system_group, gid)
 | 
						|
        group_info = grp.getgrnam(group_name)
 | 
						|
    return group_info
 | 
						|
 | 
						|
 | 
						|
def add_user_to_group(username, group):
 | 
						|
    """Add a user to a group"""
 | 
						|
    cmd = ['gpasswd', '-a', username, group]
 | 
						|
    log("Adding user {} to group {}".format(username, group))
 | 
						|
    subprocess.check_call(cmd)
 | 
						|
 | 
						|
 | 
						|
def chage(username, lastday=None, expiredate=None, inactive=None,
 | 
						|
          mindays=None, maxdays=None, root=None, warndays=None):
 | 
						|
    """Change user password expiry information
 | 
						|
 | 
						|
    :param str username: User to update
 | 
						|
    :param str lastday: Set when password was changed in YYYY-MM-DD format
 | 
						|
    :param str expiredate: Set when user's account will no longer be
 | 
						|
                           accessible in YYYY-MM-DD format.
 | 
						|
                           -1 will remove an account expiration date.
 | 
						|
    :param str inactive: Set the number of days of inactivity after a password
 | 
						|
                         has expired before the account is locked.
 | 
						|
                         -1 will remove an account's inactivity.
 | 
						|
    :param str mindays: Set the minimum number of days between password
 | 
						|
                        changes to MIN_DAYS.
 | 
						|
                        0 indicates the password can be changed anytime.
 | 
						|
    :param str maxdays: Set the maximum number of days during which a
 | 
						|
                        password is valid.
 | 
						|
                        -1 as MAX_DAYS will remove checking maxdays
 | 
						|
    :param str root: Apply changes in the CHROOT_DIR directory
 | 
						|
    :param str warndays: Set the number of days of warning before a password
 | 
						|
                         change is required
 | 
						|
    :raises subprocess.CalledProcessError: if call to chage fails
 | 
						|
    """
 | 
						|
    cmd = ['chage']
 | 
						|
    if root:
 | 
						|
        cmd.extend(['--root', root])
 | 
						|
    if lastday:
 | 
						|
        cmd.extend(['--lastday', lastday])
 | 
						|
    if expiredate:
 | 
						|
        cmd.extend(['--expiredate', expiredate])
 | 
						|
    if inactive:
 | 
						|
        cmd.extend(['--inactive', inactive])
 | 
						|
    if mindays:
 | 
						|
        cmd.extend(['--mindays', mindays])
 | 
						|
    if maxdays:
 | 
						|
        cmd.extend(['--maxdays', maxdays])
 | 
						|
    if warndays:
 | 
						|
        cmd.extend(['--warndays', warndays])
 | 
						|
    cmd.append(username)
 | 
						|
    subprocess.check_call(cmd)
 | 
						|
 | 
						|
 | 
						|
remove_password_expiry = functools.partial(chage, expiredate='-1', inactive='-1', mindays='0', maxdays='-1')
 | 
						|
 | 
						|
 | 
						|
def rsync(from_path, to_path, flags='-r', options=None, timeout=None):
 | 
						|
    """Replicate the contents of a path"""
 | 
						|
    options = options or ['--delete', '--executability']
 | 
						|
    cmd = ['/usr/bin/rsync', flags]
 | 
						|
    if timeout:
 | 
						|
        cmd = ['timeout', str(timeout)] + cmd
 | 
						|
    cmd.extend(options)
 | 
						|
    cmd.append(from_path)
 | 
						|
    cmd.append(to_path)
 | 
						|
    log(" ".join(cmd))
 | 
						|
    return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip()
 | 
						|
 | 
						|
 | 
						|
def symlink(source, destination):
 | 
						|
    """Create a symbolic link"""
 | 
						|
    log("Symlinking {} as {}".format(source, destination))
 | 
						|
    cmd = [
 | 
						|
        'ln',
 | 
						|
        '-sf',
 | 
						|
        source,
 | 
						|
        destination,
 | 
						|
    ]
 | 
						|
    subprocess.check_call(cmd)
 | 
						|
 | 
						|
 | 
						|
def mkdir(path, owner='root', group='root', perms=0o555, force=False):
 | 
						|
    """Create a directory"""
 | 
						|
    log("Making dir {} {}:{} {:o}".format(path, owner, group,
 | 
						|
                                          perms))
 | 
						|
    uid = pwd.getpwnam(owner).pw_uid
 | 
						|
    gid = grp.getgrnam(group).gr_gid
 | 
						|
    realpath = os.path.abspath(path)
 | 
						|
    path_exists = os.path.exists(realpath)
 | 
						|
    if path_exists and force:
 | 
						|
        if not os.path.isdir(realpath):
 | 
						|
            log("Removing non-directory file {} prior to mkdir()".format(path))
 | 
						|
            os.unlink(realpath)
 | 
						|
            os.makedirs(realpath, perms)
 | 
						|
    elif not path_exists:
 | 
						|
        os.makedirs(realpath, perms)
 | 
						|
    os.chown(realpath, uid, gid)
 | 
						|
    os.chmod(realpath, perms)
 | 
						|
 | 
						|
 | 
						|
def write_file(path, content, owner='root', group='root', perms=0o444):
 | 
						|
    """Create or overwrite a file with the contents of a byte string."""
 | 
						|
    uid = pwd.getpwnam(owner).pw_uid
 | 
						|
    gid = grp.getgrnam(group).gr_gid
 | 
						|
    # lets see if we can grab the file and compare the context, to avoid doing
 | 
						|
    # a write.
 | 
						|
    existing_content = None
 | 
						|
    existing_uid, existing_gid, existing_perms = None, None, None
 | 
						|
    try:
 | 
						|
        with open(path, 'rb') as target:
 | 
						|
            existing_content = target.read()
 | 
						|
        stat = os.stat(path)
 | 
						|
        existing_uid, existing_gid, existing_perms = (
 | 
						|
            stat.st_uid, stat.st_gid, stat.st_mode
 | 
						|
        )
 | 
						|
    except Exception:
 | 
						|
        pass
 | 
						|
    if content != existing_content:
 | 
						|
        log("Writing file {} {}:{} {:o}".format(path, owner, group, perms),
 | 
						|
            level=DEBUG)
 | 
						|
        with open(path, 'wb') as target:
 | 
						|
            os.fchown(target.fileno(), uid, gid)
 | 
						|
            os.fchmod(target.fileno(), perms)
 | 
						|
            if isinstance(content, str):
 | 
						|
                content = content.encode('UTF-8')
 | 
						|
            target.write(content)
 | 
						|
        return
 | 
						|
    # the contents were the same, but we might still need to change the
 | 
						|
    # ownership or permissions.
 | 
						|
    if existing_uid != uid:
 | 
						|
        log("Changing uid on already existing content: {} -> {}"
 | 
						|
            .format(existing_uid, uid), level=DEBUG)
 | 
						|
        os.chown(path, uid, -1)
 | 
						|
    if existing_gid != gid:
 | 
						|
        log("Changing gid on already existing content: {} -> {}"
 | 
						|
            .format(existing_gid, gid), level=DEBUG)
 | 
						|
        os.chown(path, -1, gid)
 | 
						|
    if existing_perms != perms:
 | 
						|
        log("Changing permissions on existing content: {} -> {}"
 | 
						|
            .format(existing_perms, perms), level=DEBUG)
 | 
						|
        os.chmod(path, perms)
 | 
						|
 | 
						|
 | 
						|
def fstab_remove(mp):
 | 
						|
    """Remove the given mountpoint entry from /etc/fstab"""
 | 
						|
    return Fstab.remove_by_mountpoint(mp)
 | 
						|
 | 
						|
 | 
						|
def fstab_add(dev, mp, fs, options=None):
 | 
						|
    """Adds the given device entry to the /etc/fstab file"""
 | 
						|
    return Fstab.add(dev, mp, fs, options=options)
 | 
						|
 | 
						|
 | 
						|
def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
 | 
						|
    """Mount a filesystem at a particular mountpoint"""
 | 
						|
    cmd_args = ['mount']
 | 
						|
    if options is not None:
 | 
						|
        cmd_args.extend(['-o', options])
 | 
						|
    cmd_args.extend([device, mountpoint])
 | 
						|
    try:
 | 
						|
        subprocess.check_output(cmd_args)
 | 
						|
    except subprocess.CalledProcessError as e:
 | 
						|
        log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
 | 
						|
        return False
 | 
						|
 | 
						|
    if persist:
 | 
						|
        return fstab_add(device, mountpoint, filesystem, options=options)
 | 
						|
    return True
 | 
						|
 | 
						|
 | 
						|
def umount(mountpoint, persist=False):
 | 
						|
    """Unmount a filesystem"""
 | 
						|
    cmd_args = ['umount', mountpoint]
 | 
						|
    try:
 | 
						|
        subprocess.check_output(cmd_args)
 | 
						|
    except subprocess.CalledProcessError as e:
 | 
						|
        log('Error unmounting {}\n{}'.format(mountpoint, e.output))
 | 
						|
        return False
 | 
						|
 | 
						|
    if persist:
 | 
						|
        return fstab_remove(mountpoint)
 | 
						|
    return True
 | 
						|
 | 
						|
 | 
						|
def mounts():
 | 
						|
    """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
 | 
						|
    with open('/proc/mounts') as f:
 | 
						|
        # [['/mount/point','/dev/path'],[...]]
 | 
						|
        system_mounts = [m[1::-1] for m in [l.strip().split()
 | 
						|
                                            for l in f.readlines()]]
 | 
						|
    return system_mounts
 | 
						|
 | 
						|
 | 
						|
def fstab_mount(mountpoint):
 | 
						|
    """Mount filesystem using fstab"""
 | 
						|
    cmd_args = ['mount', mountpoint]
 | 
						|
    try:
 | 
						|
        subprocess.check_output(cmd_args)
 | 
						|
    except subprocess.CalledProcessError as e:
 | 
						|
        log('Error unmounting {}\n{}'.format(mountpoint, e.output))
 | 
						|
        return False
 | 
						|
    return True
 | 
						|
 | 
						|
 | 
						|
def file_hash(path, hash_type='md5'):
 | 
						|
    """Generate a hash checksum of the contents of 'path' or None if not found.
 | 
						|
 | 
						|
    :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
 | 
						|
                          such as md5, sha1, sha256, sha512, etc.
 | 
						|
    """
 | 
						|
    if os.path.exists(path):
 | 
						|
        h = getattr(hashlib, hash_type)()
 | 
						|
        with open(path, 'rb') as source:
 | 
						|
            h.update(source.read())
 | 
						|
        return h.hexdigest()
 | 
						|
    else:
 | 
						|
        return None
 | 
						|
 | 
						|
 | 
						|
def path_hash(path):
 | 
						|
    """Generate a hash checksum of all files matching 'path'. Standard
 | 
						|
    wildcards like '*' and '?' are supported, see documentation for the 'glob'
 | 
						|
    module for more information.
 | 
						|
 | 
						|
    :return: dict: A { filename: hash } dictionary for all matched files.
 | 
						|
                   Empty if none found.
 | 
						|
    """
 | 
						|
    return {
 | 
						|
        filename: file_hash(filename)
 | 
						|
        for filename in glob.iglob(path)
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
def check_hash(path, checksum, hash_type='md5'):
 | 
						|
    """Validate a file using a cryptographic checksum.
 | 
						|
 | 
						|
    :param str checksum: Value of the checksum used to validate the file.
 | 
						|
    :param str hash_type: Hash algorithm used to generate `checksum`.
 | 
						|
        Can be any hash algorithm supported by :mod:`hashlib`,
 | 
						|
        such as md5, sha1, sha256, sha512, etc.
 | 
						|
    :raises ChecksumError: If the file fails the checksum
 | 
						|
 | 
						|
    """
 | 
						|
    actual_checksum = file_hash(path, hash_type)
 | 
						|
    if checksum != actual_checksum:
 | 
						|
        raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
 | 
						|
 | 
						|
 | 
						|
class ChecksumError(ValueError):
 | 
						|
    """A class derived from Value error to indicate the checksum failed."""
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class restart_on_change(object):
 | 
						|
    """Decorator and context manager to handle restarts.
 | 
						|
 | 
						|
    Usage:
 | 
						|
 | 
						|
       @restart_on_change(restart_map, ...)
 | 
						|
       def function_that_might_trigger_a_restart(...)
 | 
						|
           ...
 | 
						|
 | 
						|
    Or:
 | 
						|
 | 
						|
       with restart_on_change(restart_map, ...):
 | 
						|
           do_stuff_that_might_trigger_a_restart()
 | 
						|
           ...
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, restart_map, stopstart=False, restart_functions=None,
 | 
						|
                 can_restart_now_f=None, post_svc_restart_f=None,
 | 
						|
                 pre_restarts_wait_f=None):
 | 
						|
        """
 | 
						|
        :param restart_map: {file: [service, ...]}
 | 
						|
        :type restart_map: Dict[str, List[str,]]
 | 
						|
        :param stopstart: whether to stop, start or restart a service
 | 
						|
        :type stopstart: booleean
 | 
						|
        :param restart_functions: nonstandard functions to use to restart
 | 
						|
                                  services {svc: func, ...}
 | 
						|
        :type restart_functions: Dict[str, Callable[[str], None]]
 | 
						|
        :param can_restart_now_f: A function used to check if the restart is
 | 
						|
                                  permitted.
 | 
						|
        :type can_restart_now_f: Callable[[str, List[str]], boolean]
 | 
						|
        :param post_svc_restart_f: A function run after a service has
 | 
						|
                                   restarted.
 | 
						|
        :type post_svc_restart_f: Callable[[str], None]
 | 
						|
        :param pre_restarts_wait_f: A function called before any restarts.
 | 
						|
        :type pre_restarts_wait_f: Callable[None, None]
 | 
						|
        """
 | 
						|
        self.restart_map = restart_map
 | 
						|
        self.stopstart = stopstart
 | 
						|
        self.restart_functions = restart_functions
 | 
						|
        self.can_restart_now_f = can_restart_now_f
 | 
						|
        self.post_svc_restart_f = post_svc_restart_f
 | 
						|
        self.pre_restarts_wait_f = pre_restarts_wait_f
 | 
						|
 | 
						|
    def __call__(self, f):
 | 
						|
        """Work like a decorator.
 | 
						|
 | 
						|
        Returns a wrapped function that performs the restart if triggered.
 | 
						|
 | 
						|
        :param f: The function that is being wrapped.
 | 
						|
        :type f: Callable[[Any], Any]
 | 
						|
        :returns: the wrapped function
 | 
						|
        :rtype: Callable[[Any], Any]
 | 
						|
        """
 | 
						|
        @functools.wraps(f)
 | 
						|
        def wrapped_f(*args, **kwargs):
 | 
						|
            return restart_on_change_helper(
 | 
						|
                (lambda: f(*args, **kwargs)),
 | 
						|
                self.restart_map,
 | 
						|
                stopstart=self.stopstart,
 | 
						|
                restart_functions=self.restart_functions,
 | 
						|
                can_restart_now_f=self.can_restart_now_f,
 | 
						|
                post_svc_restart_f=self.post_svc_restart_f,
 | 
						|
                pre_restarts_wait_f=self.pre_restarts_wait_f)
 | 
						|
        return wrapped_f
 | 
						|
 | 
						|
    def __enter__(self):
 | 
						|
        """Enter the runtime context related to this object. """
 | 
						|
        self.checksums = _pre_restart_on_change_helper(self.restart_map)
 | 
						|
 | 
						|
    def __exit__(self, exc_type, exc_val, exc_tb):
 | 
						|
        """Exit the runtime context related to this object.
 | 
						|
 | 
						|
        The parameters describe the exception that caused the context to be
 | 
						|
        exited. If the context was exited without an exception, all three
 | 
						|
        arguments will be None.
 | 
						|
        """
 | 
						|
        if exc_type is None:
 | 
						|
            _post_restart_on_change_helper(
 | 
						|
                self.checksums,
 | 
						|
                self.restart_map,
 | 
						|
                stopstart=self.stopstart,
 | 
						|
                restart_functions=self.restart_functions,
 | 
						|
                can_restart_now_f=self.can_restart_now_f,
 | 
						|
                post_svc_restart_f=self.post_svc_restart_f,
 | 
						|
                pre_restarts_wait_f=self.pre_restarts_wait_f)
 | 
						|
        # All is good, so return False; any exceptions will propagate.
 | 
						|
        return False
 | 
						|
 | 
						|
 | 
						|
def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
 | 
						|
                             restart_functions=None,
 | 
						|
                             can_restart_now_f=None,
 | 
						|
                             post_svc_restart_f=None,
 | 
						|
                             pre_restarts_wait_f=None):
 | 
						|
    """Helper function to perform the restart_on_change function.
 | 
						|
 | 
						|
    This is provided for decorators to restart services if files described
 | 
						|
    in the restart_map have changed after an invocation of lambda_f().
 | 
						|
 | 
						|
    This functions allows for a number of helper functions to be passed.
 | 
						|
 | 
						|
    `restart_functions` is a map with a service as the key and the
 | 
						|
    corresponding value being the function to call to restart the service. For
 | 
						|
    example if `restart_functions={'some-service': my_restart_func}` then
 | 
						|
    `my_restart_func` should a function which takes one argument which is the
 | 
						|
    service name to be retstarted.
 | 
						|
 | 
						|
    `can_restart_now_f` is a function which checks that a restart is permitted.
 | 
						|
    It should return a bool which indicates if a restart is allowed and should
 | 
						|
    take a service name (str) and a list of changed files (List[str]) as
 | 
						|
    arguments.
 | 
						|
 | 
						|
    `post_svc_restart_f` is a function which runs after a service has been
 | 
						|
    restarted. It takes the service name that was restarted as an argument.
 | 
						|
 | 
						|
    `pre_restarts_wait_f` is a function which is called before any restarts
 | 
						|
    occur. The use case for this is an application which wants to try and
 | 
						|
    stagger restarts between units.
 | 
						|
 | 
						|
    :param lambda_f: function to call.
 | 
						|
    :type lambda_f: Callable[[], ANY]
 | 
						|
    :param restart_map: {file: [service, ...]}
 | 
						|
    :type restart_map: Dict[str, List[str,]]
 | 
						|
    :param stopstart: whether to stop, start or restart a service
 | 
						|
    :type stopstart: booleean
 | 
						|
    :param restart_functions: nonstandard functions to use to restart services
 | 
						|
                              {svc: func, ...}
 | 
						|
    :type restart_functions: Dict[str, Callable[[str], None]]
 | 
						|
    :param can_restart_now_f: A function used to check if the restart is
 | 
						|
                              permitted.
 | 
						|
    :type can_restart_now_f: Callable[[str, List[str]], boolean]
 | 
						|
    :param post_svc_restart_f: A function run after a service has
 | 
						|
                               restarted.
 | 
						|
    :type post_svc_restart_f: Callable[[str], None]
 | 
						|
    :param pre_restarts_wait_f: A function called before any restarts.
 | 
						|
    :type pre_restarts_wait_f: Callable[None, None]
 | 
						|
    :returns: result of lambda_f()
 | 
						|
    :rtype: ANY
 | 
						|
    """
 | 
						|
    checksums = _pre_restart_on_change_helper(restart_map)
 | 
						|
    r = lambda_f()
 | 
						|
    _post_restart_on_change_helper(checksums,
 | 
						|
                                   restart_map,
 | 
						|
                                   stopstart,
 | 
						|
                                   restart_functions,
 | 
						|
                                   can_restart_now_f,
 | 
						|
                                   post_svc_restart_f,
 | 
						|
                                   pre_restarts_wait_f)
 | 
						|
    return r
 | 
						|
 | 
						|
 | 
						|
def _pre_restart_on_change_helper(restart_map):
 | 
						|
    """Take a snapshot of file hashes.
 | 
						|
 | 
						|
    :param restart_map: {file: [service, ...]}
 | 
						|
    :type restart_map: Dict[str, List[str,]]
 | 
						|
    :returns: Dictionary of file paths and the files checksum.
 | 
						|
    :rtype: Dict[str, str]
 | 
						|
    """
 | 
						|
    return {path: path_hash(path) for path in restart_map}
 | 
						|
 | 
						|
 | 
						|
def _post_restart_on_change_helper(checksums,
 | 
						|
                                   restart_map,
 | 
						|
                                   stopstart=False,
 | 
						|
                                   restart_functions=None,
 | 
						|
                                   can_restart_now_f=None,
 | 
						|
                                   post_svc_restart_f=None,
 | 
						|
                                   pre_restarts_wait_f=None):
 | 
						|
    """Check whether files have changed.
 | 
						|
 | 
						|
    :param checksums: Dictionary of file paths and the files checksum.
 | 
						|
    :type checksums: Dict[str, str]
 | 
						|
    :param restart_map: {file: [service, ...]}
 | 
						|
    :type restart_map: Dict[str, List[str,]]
 | 
						|
    :param stopstart: whether to stop, start or restart a service
 | 
						|
    :type stopstart: booleean
 | 
						|
    :param restart_functions: nonstandard functions to use to restart services
 | 
						|
                              {svc: func, ...}
 | 
						|
    :type restart_functions: Dict[str, Callable[[str], None]]
 | 
						|
    :param can_restart_now_f: A function used to check if the restart is
 | 
						|
                              permitted.
 | 
						|
    :type can_restart_now_f: Callable[[str, List[str]], boolean]
 | 
						|
    :param post_svc_restart_f: A function run after a service has
 | 
						|
                               restarted.
 | 
						|
    :type post_svc_restart_f: Callable[[str], None]
 | 
						|
    :param pre_restarts_wait_f: A function called before any restarts.
 | 
						|
    :type pre_restarts_wait_f: Callable[None, None]
 | 
						|
    """
 | 
						|
    if restart_functions is None:
 | 
						|
        restart_functions = {}
 | 
						|
    changed_files = defaultdict(list)
 | 
						|
    restarts = []
 | 
						|
    # create a list of lists of the services to restart
 | 
						|
    for path, services in restart_map.items():
 | 
						|
        if path_hash(path) != checksums[path]:
 | 
						|
            restarts.append(services)
 | 
						|
            for svc in services:
 | 
						|
                changed_files[svc].append(path)
 | 
						|
    # create a flat list of ordered services without duplicates from lists
 | 
						|
    services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
 | 
						|
    if services_list:
 | 
						|
        if pre_restarts_wait_f:
 | 
						|
            pre_restarts_wait_f()
 | 
						|
        actions = ('stop', 'start') if stopstart else ('restart',)
 | 
						|
        for service_name in services_list:
 | 
						|
            if can_restart_now_f:
 | 
						|
                if not can_restart_now_f(service_name,
 | 
						|
                                         changed_files[service_name]):
 | 
						|
                    continue
 | 
						|
            if service_name in restart_functions:
 | 
						|
                restart_functions[service_name](service_name)
 | 
						|
            else:
 | 
						|
                for action in actions:
 | 
						|
                    service(action, service_name)
 | 
						|
            if post_svc_restart_f:
 | 
						|
                post_svc_restart_f(service_name)
 | 
						|
 | 
						|
 | 
						|
def pwgen(length=None):
 | 
						|
    """Generate a random password."""
 | 
						|
    if length is None:
 | 
						|
        # A random length is ok to use a weak PRNG
 | 
						|
        length = random.choice(range(35, 45))
 | 
						|
    alphanumeric_chars = [
 | 
						|
        l for l in (string.ascii_letters + string.digits)
 | 
						|
        if l not in 'l0QD1vAEIOUaeiou']
 | 
						|
    # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
 | 
						|
    # actual password
 | 
						|
    random_generator = random.SystemRandom()
 | 
						|
    random_chars = [
 | 
						|
        random_generator.choice(alphanumeric_chars) for _ in range(length)]
 | 
						|
    return ''.join(random_chars)
 | 
						|
 | 
						|
 | 
						|
def is_phy_iface(interface):
 | 
						|
    """Returns True if interface is not virtual, otherwise False."""
 | 
						|
    if interface:
 | 
						|
        sys_net = '/sys/class/net'
 | 
						|
        if os.path.isdir(sys_net):
 | 
						|
            for iface in glob.glob(os.path.join(sys_net, '*')):
 | 
						|
                if '/virtual/' in os.path.realpath(iface):
 | 
						|
                    continue
 | 
						|
 | 
						|
                if interface == os.path.basename(iface):
 | 
						|
                    return True
 | 
						|
 | 
						|
    return False
 | 
						|
 | 
						|
 | 
						|
def get_bond_master(interface):
 | 
						|
    """Returns bond master if interface is bond slave otherwise None.
 | 
						|
 | 
						|
    NOTE: the provided interface is expected to be physical
 | 
						|
    """
 | 
						|
    if interface:
 | 
						|
        iface_path = '/sys/class/net/%s' % (interface)
 | 
						|
        if os.path.exists(iface_path):
 | 
						|
            if '/virtual/' in os.path.realpath(iface_path):
 | 
						|
                return None
 | 
						|
 | 
						|
            master = os.path.join(iface_path, 'master')
 | 
						|
            if os.path.exists(master):
 | 
						|
                master = os.path.realpath(master)
 | 
						|
                # make sure it is a bond master
 | 
						|
                if os.path.exists(os.path.join(master, 'bonding')):
 | 
						|
                    return os.path.basename(master)
 | 
						|
 | 
						|
    return None
 | 
						|
 | 
						|
 | 
						|
def list_nics(nic_type=None):
 | 
						|
    """Return a list of nics of given type(s)"""
 | 
						|
    if isinstance(nic_type, str):
 | 
						|
        int_types = [nic_type]
 | 
						|
    else:
 | 
						|
        int_types = nic_type
 | 
						|
 | 
						|
    interfaces = []
 | 
						|
    if nic_type:
 | 
						|
        for int_type in int_types:
 | 
						|
            cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
 | 
						|
            ip_output = subprocess.check_output(
 | 
						|
                cmd).decode('UTF-8', errors='replace')
 | 
						|
            ip_output = ip_output.split('\n')
 | 
						|
            ip_output = (line for line in ip_output if line)
 | 
						|
            for line in ip_output:
 | 
						|
                if line.split()[1].startswith(int_type):
 | 
						|
                    matched = re.search('.*: (' + int_type +
 | 
						|
                                        r'[0-9]+\.[0-9]+)@.*', line)
 | 
						|
                    if matched:
 | 
						|
                        iface = matched.groups()[0]
 | 
						|
                    else:
 | 
						|
                        iface = line.split()[1].replace(":", "")
 | 
						|
 | 
						|
                    if iface not in interfaces:
 | 
						|
                        interfaces.append(iface)
 | 
						|
    else:
 | 
						|
        cmd = ['ip', 'a']
 | 
						|
        ip_output = subprocess.check_output(
 | 
						|
            cmd).decode('UTF-8', errors='replace').split('\n')
 | 
						|
        ip_output = (line.strip() for line in ip_output if line)
 | 
						|
 | 
						|
        key = re.compile(r'^[0-9]+:\s+(.+):')
 | 
						|
        for line in ip_output:
 | 
						|
            matched = re.search(key, line)
 | 
						|
            if matched:
 | 
						|
                iface = matched.group(1)
 | 
						|
                iface = iface.partition("@")[0]
 | 
						|
                if iface not in interfaces:
 | 
						|
                    interfaces.append(iface)
 | 
						|
 | 
						|
    return interfaces
 | 
						|
 | 
						|
 | 
						|
def set_nic_mtu(nic, mtu):
 | 
						|
    """Set the Maximum Transmission Unit (MTU) on a network interface."""
 | 
						|
    cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
 | 
						|
    subprocess.check_call(cmd)
 | 
						|
 | 
						|
 | 
						|
def get_nic_mtu(nic):
 | 
						|
    """Return the Maximum Transmission Unit (MTU) for a network interface."""
 | 
						|
    cmd = ['ip', 'addr', 'show', nic]
 | 
						|
    ip_output = subprocess.check_output(
 | 
						|
        cmd).decode('UTF-8', errors='replace').split('\n')
 | 
						|
    mtu = ""
 | 
						|
    for line in ip_output:
 | 
						|
        words = line.split()
 | 
						|
        if 'mtu' in words:
 | 
						|
            mtu = words[words.index("mtu") + 1]
 | 
						|
    return mtu
 | 
						|
 | 
						|
 | 
						|
def get_nic_hwaddr(nic):
 | 
						|
    """Return the Media Access Control (MAC) for a network interface."""
 | 
						|
    cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
 | 
						|
    ip_output = subprocess.check_output(cmd).decode('UTF-8', errors='replace')
 | 
						|
    hwaddr = ""
 | 
						|
    words = ip_output.split()
 | 
						|
    if 'link/ether' in words:
 | 
						|
        hwaddr = words[words.index('link/ether') + 1]
 | 
						|
    return hwaddr
 | 
						|
 | 
						|
 | 
						|
@contextmanager
 | 
						|
def chdir(directory):
 | 
						|
    """Change the current working directory to a different directory for a code
 | 
						|
    block and return the previous directory after the block exits. Useful to
 | 
						|
    run commands from a specified directory.
 | 
						|
 | 
						|
    :param str directory: The directory path to change to for this context.
 | 
						|
    """
 | 
						|
    cur = os.getcwd()
 | 
						|
    try:
 | 
						|
        yield os.chdir(directory)
 | 
						|
    finally:
 | 
						|
        os.chdir(cur)
 | 
						|
 | 
						|
 | 
						|
def chownr(path, owner, group, follow_links=True, chowntopdir=False):
 | 
						|
    """Recursively change user and group ownership of files and directories
 | 
						|
    in given path. Doesn't chown path itself by default, only its children.
 | 
						|
 | 
						|
    :param str path: The string path to start changing ownership.
 | 
						|
    :param str owner: The owner string to use when looking up the uid.
 | 
						|
    :param str group: The group string to use when looking up the gid.
 | 
						|
    :param bool follow_links: Also follow and chown links if True
 | 
						|
    :param bool chowntopdir: Also chown path itself if True
 | 
						|
    """
 | 
						|
    uid = pwd.getpwnam(owner).pw_uid
 | 
						|
    gid = grp.getgrnam(group).gr_gid
 | 
						|
    if follow_links:
 | 
						|
        chown = os.chown
 | 
						|
    else:
 | 
						|
        chown = os.lchown
 | 
						|
 | 
						|
    if chowntopdir:
 | 
						|
        broken_symlink = os.path.lexists(path) and not os.path.exists(path)
 | 
						|
        if not broken_symlink:
 | 
						|
            chown(path, uid, gid)
 | 
						|
    for root, dirs, files in os.walk(path, followlinks=follow_links):
 | 
						|
        for name in dirs + files:
 | 
						|
            full = os.path.join(root, name)
 | 
						|
            try:
 | 
						|
                chown(full, uid, gid)
 | 
						|
            except (IOError, OSError) as e:
 | 
						|
                # Intended to ignore "file not found".
 | 
						|
                if e.errno == errno.ENOENT:
 | 
						|
                    pass
 | 
						|
 | 
						|
 | 
						|
def lchownr(path, owner, group):
 | 
						|
    """Recursively change user and group ownership of files and directories
 | 
						|
    in a given path, not following symbolic links. See the documentation for
 | 
						|
    'os.lchown' for more information.
 | 
						|
 | 
						|
    :param str path: The string path to start changing ownership.
 | 
						|
    :param str owner: The owner string to use when looking up the uid.
 | 
						|
    :param str group: The group string to use when looking up the gid.
 | 
						|
    """
 | 
						|
    chownr(path, owner, group, follow_links=False)
 | 
						|
 | 
						|
 | 
						|
def owner(path):
 | 
						|
    """Returns a tuple containing the username & groupname owning the path.
 | 
						|
 | 
						|
    :param str path: the string path to retrieve the ownership
 | 
						|
    :return tuple(str, str): A (username, groupname) tuple containing the
 | 
						|
                             name of the user and group owning the path.
 | 
						|
    :raises OSError: if the specified path does not exist
 | 
						|
    """
 | 
						|
    stat = os.stat(path)
 | 
						|
    username = pwd.getpwuid(stat.st_uid)[0]
 | 
						|
    groupname = grp.getgrgid(stat.st_gid)[0]
 | 
						|
    return username, groupname
 | 
						|
 | 
						|
 | 
						|
def get_total_ram():
 | 
						|
    """The total amount of system RAM in bytes.
 | 
						|
 | 
						|
    This is what is reported by the OS, and may be overcommitted when
 | 
						|
    there are multiple containers hosted on the same machine.
 | 
						|
    """
 | 
						|
    with open('/proc/meminfo', 'r') as f:
 | 
						|
        for line in f.readlines():
 | 
						|
            if line:
 | 
						|
                key, value, unit = line.split()
 | 
						|
                if key == 'MemTotal:':
 | 
						|
                    assert unit == 'kB', 'Unknown unit'
 | 
						|
                    return int(value) * 1024  # Classic, not KiB.
 | 
						|
        raise NotImplementedError()
 | 
						|
 | 
						|
 | 
						|
UPSTART_CONTAINER_TYPE = '/run/container_type'
 | 
						|
 | 
						|
 | 
						|
def is_container():
 | 
						|
    """Determine whether unit is running in a container
 | 
						|
 | 
						|
    @return: boolean indicating if unit is in a container
 | 
						|
    """
 | 
						|
    if init_is_systemd():
 | 
						|
        # Detect using systemd-detect-virt
 | 
						|
        return subprocess.call(['systemd-detect-virt',
 | 
						|
                                '--container']) == 0
 | 
						|
    else:
 | 
						|
        # Detect using upstart container file marker
 | 
						|
        return os.path.exists(UPSTART_CONTAINER_TYPE)
 | 
						|
 | 
						|
 | 
						|
def add_to_updatedb_prunepath(path, updatedb_path=UPDATEDB_PATH):
 | 
						|
    """Adds the specified path to the mlocate's udpatedb.conf PRUNEPATH list.
 | 
						|
 | 
						|
    This method has no effect if the path specified by updatedb_path does not
 | 
						|
    exist or is not a file.
 | 
						|
 | 
						|
    @param path: string the path to add to the updatedb.conf PRUNEPATHS value
 | 
						|
    @param updatedb_path: the path the updatedb.conf file
 | 
						|
    """
 | 
						|
    if not os.path.exists(updatedb_path) or os.path.isdir(updatedb_path):
 | 
						|
        # If the updatedb.conf file doesn't exist then don't attempt to update
 | 
						|
        # the file as the package providing mlocate may not be installed on
 | 
						|
        # the local system
 | 
						|
        return
 | 
						|
 | 
						|
    with open(updatedb_path, 'r+') as f_id:
 | 
						|
        updatedb_text = f_id.read()
 | 
						|
        output = updatedb(updatedb_text, path)
 | 
						|
        f_id.seek(0)
 | 
						|
        f_id.write(output)
 | 
						|
        f_id.truncate()
 | 
						|
 | 
						|
 | 
						|
def updatedb(updatedb_text, new_path):
 | 
						|
    lines = [line for line in updatedb_text.split("\n")]
 | 
						|
    for i, line in enumerate(lines):
 | 
						|
        if line.startswith("PRUNEPATHS="):
 | 
						|
            paths_line = line.split("=")[1].replace('"', '')
 | 
						|
            paths = paths_line.split(" ")
 | 
						|
            if new_path not in paths:
 | 
						|
                paths.append(new_path)
 | 
						|
                lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths))
 | 
						|
    output = "\n".join(lines)
 | 
						|
    return output
 | 
						|
 | 
						|
 | 
						|
def modulo_distribution(modulo=3, wait=30, non_zero_wait=False):
 | 
						|
    """ Modulo distribution
 | 
						|
 | 
						|
    This helper uses the unit number, a modulo value and a constant wait time
 | 
						|
    to produce a calculated wait time distribution. This is useful in large
 | 
						|
    scale deployments to distribute load during an expensive operation such as
 | 
						|
    service restarts.
 | 
						|
 | 
						|
    If you have 1000 nodes that need to restart 100 at a time 1 minute at a
 | 
						|
    time:
 | 
						|
 | 
						|
      time.wait(modulo_distribution(modulo=100, wait=60))
 | 
						|
      restart()
 | 
						|
 | 
						|
    If you need restarts to happen serially set modulo to the exact number of
 | 
						|
    nodes and set a high constant wait time:
 | 
						|
 | 
						|
      time.wait(modulo_distribution(modulo=10, wait=120))
 | 
						|
      restart()
 | 
						|
 | 
						|
    @param modulo: int The modulo number creates the group distribution
 | 
						|
    @param wait: int The constant time wait value
 | 
						|
    @param non_zero_wait: boolean Override unit % modulo == 0,
 | 
						|
                          return modulo * wait. Used to avoid collisions with
 | 
						|
                          leader nodes which are often given priority.
 | 
						|
    @return: int Calculated time to wait for unit operation
 | 
						|
    """
 | 
						|
    unit_number = int(local_unit().split('/')[1])
 | 
						|
    calculated_wait_time = (unit_number % modulo) * wait
 | 
						|
    if non_zero_wait and calculated_wait_time == 0:
 | 
						|
        return modulo * wait
 | 
						|
    else:
 | 
						|
        return calculated_wait_time
 | 
						|
 | 
						|
 | 
						|
def ca_cert_absolute_path(basename_without_extension):
 | 
						|
    """Returns absolute path to CA certificate.
 | 
						|
 | 
						|
    :param basename_without_extension: Filename without extension
 | 
						|
    :type basename_without_extension: str
 | 
						|
    :returns: Absolute full path
 | 
						|
    :rtype: str
 | 
						|
    """
 | 
						|
    return '{}/{}.crt'.format(CA_CERT_DIR, basename_without_extension)
 | 
						|
 | 
						|
 | 
						|
def install_ca_cert(ca_cert, name=None):
 | 
						|
    """
 | 
						|
    Install the given cert as a trusted CA.
 | 
						|
 | 
						|
    The ``name`` is the stem of the filename where the cert is written, and if
 | 
						|
    not provided, it will default to ``juju-{charm_name}``.
 | 
						|
 | 
						|
    If the cert is empty or None, or is unchanged, nothing is done.
 | 
						|
    """
 | 
						|
    if not ca_cert:
 | 
						|
        return
 | 
						|
    if not isinstance(ca_cert, bytes):
 | 
						|
        ca_cert = ca_cert.encode('utf8')
 | 
						|
    if not name:
 | 
						|
        name = 'juju-{}'.format(charm_name())
 | 
						|
    cert_file = ca_cert_absolute_path(name)
 | 
						|
    new_hash = hashlib.md5(ca_cert).hexdigest()
 | 
						|
    if file_hash(cert_file) == new_hash:
 | 
						|
        return
 | 
						|
    log("Installing new CA cert at: {}".format(cert_file), level=INFO)
 | 
						|
    write_file(cert_file, ca_cert)
 | 
						|
    subprocess.check_call(['update-ca-certificates', '--fresh'])
 | 
						|
 | 
						|
 | 
						|
def get_system_env(key, default=None):
 | 
						|
    """Get data from system environment as represented in ``/etc/environment``.
 | 
						|
 | 
						|
    :param key: Key to look up
 | 
						|
    :type key: str
 | 
						|
    :param default: Value to return if key is not found
 | 
						|
    :type default: any
 | 
						|
    :returns: Value for key if found or contents of default parameter
 | 
						|
    :rtype: any
 | 
						|
    :raises: subprocess.CalledProcessError
 | 
						|
    """
 | 
						|
    env_file = '/etc/environment'
 | 
						|
    # use the shell and env(1) to parse the global environments file.  This is
 | 
						|
    # done to get the correct result even if the user has shell variable
 | 
						|
    # substitutions or other shell logic in that file.
 | 
						|
    output = subprocess.check_output(
 | 
						|
        ['env', '-i', '/bin/bash', '-c',
 | 
						|
         'set -a && source {} && env'.format(env_file)],
 | 
						|
        universal_newlines=True)
 | 
						|
    for k, v in (line.split('=', 1)
 | 
						|
                 for line in output.splitlines() if '=' in line):
 | 
						|
        if k == key:
 | 
						|
            return v
 | 
						|
    else:
 | 
						|
        return default
 |