saltstack/salt

View on GitHub
salt/modules/seed.py

Summary

Maintainability
B
6 hrs
Test Coverage
# -*- coding: utf-8 -*-
'''
Virtual machine image management tools
'''

from __future__ import absolute_import, unicode_literals, print_function

# Import python libs
import os
import shutil
import logging
import tempfile

# Import salt libs
import salt.crypt
import salt.utils.cloud
import salt.utils.files
import salt.config
import salt.syspaths
import uuid


# Set up logging
log = logging.getLogger(__name__)

# Don't shadow built-in's.
__func_alias__ = {
    'apply_': 'apply'
}


def _file_or_content(file_):
    if os.path.exists(file_):
        with salt.utils.files.fopen(file_) as fic:
            return salt.utils.stringutils.to_unicode(fic.read())
    return file_


def prep_bootstrap(mpt):
    '''
    Update and get the random script to a random place

    CLI Example:

    .. code-block:: bash

        salt '*' seed.prep_bootstrap /tmp

    '''
    # Verify that the boostrap script is downloaded
    bs_ = __salt__['config.gather_bootstrap_script']()
    fpd_ = os.path.join(mpt, 'tmp', "{0}".format(
        uuid.uuid4()))
    if not os.path.exists(fpd_):
        os.makedirs(fpd_)
    os.chmod(fpd_, 0o700)
    fp_ = os.path.join(fpd_, os.path.basename(bs_))
    # Copy script into tmp
    shutil.copy(bs_, fp_)
    tmppath = fpd_.replace(mpt, '')
    return fp_, tmppath


def _mount(path, ftype, root=None):
    mpt = None
    if ftype == 'block':
        mpt = tempfile.mkdtemp()
        if not __salt__['mount.mount'](mpt, path):
            os.rmdir(mpt)
            return None
    elif ftype == 'dir':
        return path
    elif ftype == 'file':
        if 'guestfs.mount' in __salt__:
            util = 'guestfs'
        elif 'qemu_nbd.init' in __salt__:
            util = 'qemu_nbd'
        else:
            return None
        mpt = __salt__['mount.mount'](path, device=root, util=util)
        if not mpt:
            return None
    return mpt


def _umount(mpt, ftype):
    if ftype == 'block':
        __salt__['mount.umount'](mpt)
        os.rmdir(mpt)
    elif ftype == 'file':
        __salt__['mount.umount'](mpt, util='qemu_nbd')


def apply_(path, id_=None, config=None, approve_key=True, install=True,
           prep_install=False, pub_key=None, priv_key=None, mount_point=None):
    '''
    Seed a location (disk image, directory, or block device) with the
    minion config, approve the minion's key, and/or install salt-minion.

    CLI Example:

    .. code-block:: bash

        salt 'minion' seed.apply path id [config=config_data] \\
                [gen_key=(true|false)] [approve_key=(true|false)] \\
                [install=(true|false)]

    path
        Full path to the directory, device, or disk image  on the target
        minion's file system.

    id
        Minion id with which to seed the path.

    config
        Minion configuration options. By default, the 'master' option is set to
        the target host's 'master'.

    approve_key
        Request a pre-approval of the generated minion key. Requires
        that the salt-master be configured to either auto-accept all keys or
        expect a signing request from the target host. Default: true.

    install
        Install salt-minion, if absent. Default: true.

    prep_install
        Prepare the bootstrap script, but don't run it. Default: false
    '''
    stats = __salt__['file.stats'](path, follow_symlinks=True)
    if not stats:
        return '{0} does not exist'.format(path)
    ftype = stats['type']
    path = stats['target']
    log.debug('Mounting %s at %s', ftype, path)
    try:
        os.makedirs(path)
    except OSError:
        # The directory already exists
        pass

    mpt = _mount(path, ftype, mount_point)

    if not mpt:
        return '{0} could not be mounted'.format(path)

    tmp = os.path.join(mpt, 'tmp')
    log.debug('Attempting to create directory %s', tmp)
    try:
        os.makedirs(tmp)
    except OSError:
        if not os.path.isdir(tmp):
            raise
    cfg_files = mkconfig(config, tmp=tmp, id_=id_, approve_key=approve_key,
                         pub_key=pub_key, priv_key=priv_key)

    if _check_install(mpt):
        # salt-minion is already installed, just move the config and keys
        # into place
        log.info('salt-minion pre-installed on image, '
                 'configuring as %s', id_)
        minion_config = salt.config.minion_config(cfg_files['config'])
        pki_dir = minion_config['pki_dir']
        if not os.path.isdir(os.path.join(mpt, pki_dir.lstrip('/'))):
            __salt__['file.makedirs'](
                os.path.join(mpt, pki_dir.lstrip('/'), '')
            )
        os.rename(cfg_files['privkey'], os.path.join(
            mpt, pki_dir.lstrip('/'), 'minion.pem'))
        os.rename(cfg_files['pubkey'], os.path.join(
            mpt, pki_dir.lstrip('/'), 'minion.pub'))
        os.rename(cfg_files['config'], os.path.join(mpt, 'etc/salt/minion'))
        res = True
    elif install:
        log.info('Attempting to install salt-minion to %s', mpt)
        res = _install(mpt)
    elif prep_install:
        log.error('The prep_install option is no longer supported. Please use '
                  'the bootstrap script installed with Salt, located at %s.',
                  salt.syspaths.BOOTSTRAP)
        res = False
    else:
        log.warning('No useful action performed on %s', mpt)
        res = False

    _umount(mpt, ftype)
    return res


def mkconfig(config=None,
             tmp=None,
             id_=None,
             approve_key=True,
             pub_key=None,
             priv_key=None):
    '''
    Generate keys and config and put them in a tmp directory.

    pub_key
        absolute path or file content of an optional preseeded salt key

    priv_key
        absolute path or file content of an optional preseeded salt key

    CLI Example:

    .. code-block:: bash

        salt 'minion' seed.mkconfig [config=config_data] [tmp=tmp_dir] \\
                [id_=minion_id] [approve_key=(true|false)]
    '''
    if tmp is None:
        tmp = tempfile.mkdtemp()
    if config is None:
        config = {}
    if 'master' not in config and __opts__['master'] != 'salt':
        config['master'] = __opts__['master']
    if id_:
        config['id'] = id_

    # Write the new minion's config to a tmp file
    tmp_config = os.path.join(tmp, 'minion')
    with salt.utils.files.fopen(tmp_config, 'w+') as fp_:
        fp_.write(salt.utils.cloud.salt_config_to_yaml(config))

    # Generate keys for the minion
    pubkeyfn = os.path.join(tmp, 'minion.pub')
    privkeyfn = os.path.join(tmp, 'minion.pem')
    preseeded = pub_key and priv_key
    if preseeded:
        log.debug('Writing minion.pub to %s', pubkeyfn)
        log.debug('Writing minion.pem to %s', privkeyfn)
        with salt.utils.files.fopen(pubkeyfn, 'w') as fic:
            fic.write(salt.utils.stringutils.to_str(_file_or_content(pub_key)))
        with salt.utils.files.fopen(privkeyfn, 'w') as fic:
            fic.write(salt.utils.stringutils.to_str(_file_or_content(priv_key)))
        os.chmod(pubkeyfn, 0o600)
        os.chmod(privkeyfn, 0o600)
    else:
        salt.crypt.gen_keys(tmp, 'minion', 2048)
    if approve_key and not preseeded:
        with salt.utils.files.fopen(pubkeyfn) as fp_:
            pubkey = salt.utils.stringutils.to_unicode(fp_.read())
            __salt__['pillar.ext']({'virtkey': [id_, pubkey]})

    return {'config': tmp_config, 'pubkey': pubkeyfn, 'privkey': privkeyfn}


def _install(mpt):
    '''
    Determine whether salt-minion is installed and, if not,
    install it.
    Return True if install is successful or already installed.
    '''
    _check_resolv(mpt)
    boot_, tmppath = (prep_bootstrap(mpt)
             or salt.syspaths.BOOTSTRAP)
    # Exec the chroot command
    cmd = 'if type salt-minion; then exit 0; '
    cmd += 'else sh {0} -c /tmp; fi'.format(os.path.join(tmppath, 'bootstrap-salt.sh'))
    return not __salt__['cmd.run_chroot'](mpt, cmd, python_shell=True)['retcode']


def _check_resolv(mpt):
    '''
    Check that the resolv.conf is present and populated
    '''
    resolv = os.path.join(mpt, 'etc/resolv.conf')
    replace = False
    if os.path.islink(resolv):
        resolv = os.path.realpath(resolv)
        if not os.path.isdir(os.path.dirname(resolv)):
            os.makedirs(os.path.dirname(resolv))
    if not os.path.isfile(resolv):
        replace = True
    if not replace:
        with salt.utils.files.fopen(resolv, 'rb') as fp_:
            conts = salt.utils.stringutils.to_unicode(fp_.read())
            if 'nameserver' not in conts:
                replace = True
            if 'nameserver 127.0.0.1' in conts:
                replace = True
    if replace:
        shutil.copy('/etc/resolv.conf', resolv)


def _check_install(root):
    sh_ = '/bin/sh'
    if os.path.isfile(os.path.join(root, 'bin/bash')):
        sh_ = '/bin/bash'

    cmd = ('if ! type salt-minion; then exit 1; fi')
    cmd = 'chroot \'{0}\' {1} -c \'{2}\''.format(
        root,
        sh_,
        cmd)

    return not __salt__['cmd.retcode'](cmd,
                                       output_loglevel='quiet',
                                       python_shell=True)