# -*- coding: utf-8 -*- ''' Virtual machine image management tools ''' from __future__ import absolute_import # Import python libs import os import shutil import logging import tempfile # Import salt libs import salt.crypt import salt.utils import salt.utils.cloud 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.fopen(file_) as fic: return 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 {0} at {1}'.format(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 {0}'.format(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 {0}'.format(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 {0}'.format(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 {0}.' .format(salt.syspaths.BOOTSTRAP)) res = False else: log.warning('No useful action performed on {0}'.format(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.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 {0}'.format(pubkeyfn)) log.debug('Writing minion.pem to {0}'.format(privkeyfn)) with salt.utils.fopen(pubkeyfn, 'w') as fic: fic.write(_file_or_content(pub_key)) with salt.utils.fopen(privkeyfn, 'w') as fic: fic.write(_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.fopen(pubkeyfn) as fp_: pubkey = 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 arg = 'stable {0}'.format('.'.join(salt.version.__version__.split('.')[:2])) cmd = 'if type salt-minion; then exit 0; ' cmd += 'else sh {0} -c /tmp {1}; fi'.format( os.path.join(tmppath, 'bootstrap-salt.sh'), arg) 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.fopen(resolv, 'rb') as fp_: conts = fp_.read() if 'nameserver' not 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)