Copy a deploy script to a remote server, execute it, and remove it

def deploy_script(host,
                  port=22,
                  timeout=900,
                  username='root',
                  password=None,
                  key_filename=None,
                  script=None,
                  name=None,
                  sock_dir=None,
                  provider=None,
                  conf_file=None,
                  start_action=None,
                  make_master=False,
                  master_pub=None,
                  master_pem=None,
                  master_conf=None,
                  minion_pub=None,
                  minion_pem=None,
                  minion_conf=None,
                  keep_tmp=False,
                  script_args=None,
                  script_env=None,
                  ssh_timeout=15,
                  maxtries=15,
                  make_syndic=False,
                  make_minion=True,
                  display_ssh_output=True,
                  preseed_minion_keys=None,
                  parallel=False,
                  sudo_password=None,
                  sudo=False,
                  tty=None,
                  vm_=None,
                  opts=None,
                  tmp_dir='/tmp/.saltcloud',
                  file_map=None,
                  master_sign_pub_file=None,
                  cloud_grains=None,
                  force_minion_config=False,
                  **kwargs):
    '''
    Copy a deploy script to a remote server, execute it, and remove it
    '''
    if not isinstance(opts, dict):
        opts = {}
    vm_ = vm_ or {}  # if None, default to empty dict
    cloud_grains = cloud_grains or {}

    tmp_dir = '{0}-{1}'.format(tmp_dir.rstrip('/'), uuid.uuid4())
    deploy_command = salt.config.get_cloud_config_value(
        'deploy_command', vm_, opts,
        default=os.path.join(tmp_dir, 'deploy.sh'))
    if key_filename is not None and not os.path.isfile(key_filename):
        raise SaltCloudConfigError(
            'The defined key_filename \'{0}\' does not exist'.format(
                key_filename
            )
        )

    gateway = None
    if 'gateway' in kwargs:
        gateway = kwargs['gateway']

    starttime = time.localtime()
    log.debug(
        'Deploying %s at %s',
        host, time.strftime('%Y-%m-%d %H:%M:%S', starttime)
    )
    known_hosts_file = kwargs.get('known_hosts_file', '/dev/null')
    hard_timeout = opts.get('hard_timeout', None)

    if wait_for_port(host=host, port=port, gateway=gateway):
        log.debug('SSH port %s on %s is available', port, host)
        if wait_for_passwd(host, port=port, username=username,
                           password=password, key_filename=key_filename,
                           ssh_timeout=ssh_timeout,
                           display_ssh_output=display_ssh_output,
                           gateway=gateway, known_hosts_file=known_hosts_file,
                           maxtries=maxtries, hard_timeout=hard_timeout):

            log.debug('Logging into %s:%s as %s', host, port, username)
            ssh_kwargs = {
                'hostname': host,
                'port': port,
                'username': username,
                'timeout': ssh_timeout,
                'display_ssh_output': display_ssh_output,
                'sudo_password': sudo_password,
                'sftp': opts.get('use_sftp', False)
            }
            ssh_kwargs.update(__ssh_gateway_config_dict(gateway))
            if key_filename:
                log.debug('Using %s as the key_filename', key_filename)
                ssh_kwargs['key_filename'] = key_filename
            elif password and kwargs.get('has_ssh_agent', False) is False:
                ssh_kwargs['password'] = password

            if root_cmd('test -e \'{0}\''.format(tmp_dir), tty, sudo,
                        allow_failure=True, **ssh_kwargs):
                ret = root_cmd(('sh -c "( mkdir -p -m 700 \'{0}\' )"').format(tmp_dir),
                               tty, sudo, **ssh_kwargs)
                if ret:
                    raise SaltCloudSystemExit(
                        'Can\'t create temporary '
                        'directory in {0} !'.format(tmp_dir)
                    )
            if sudo:
                comps = tmp_dir.lstrip('/').rstrip('/').split('/')
                if comps:
                    if len(comps) > 1 or comps[0] != 'tmp':
                        ret = root_cmd(
                            'chown {0} "{1}"'.format(username, tmp_dir),
                            tty, sudo, **ssh_kwargs
                        )
                        if ret:
                            raise SaltCloudSystemExit(
                                'Cant set {0} ownership on {1}'.format(
                                    username, tmp_dir))

            if not isinstance(file_map, dict):
                file_map = {}

            # Copy an arbitrary group of files to the target system
            remote_dirs = []
            file_map_success = []
            file_map_fail = []
            for map_item in file_map:
                local_file = map_item
                remote_file = file_map[map_item]
                if not os.path.exists(map_item):
                    log.error(
                        'The local file "%s" does not exist, and will not be '
                        'copied to "%s" on the target system',
                        local_file, remote_file
                    )
                    file_map_fail.append({local_file: remote_file})
                    continue

                if os.path.isdir(local_file):
                    dir_name = os.path.basename(local_file)
                    remote_dir = os.path.join(os.path.dirname(remote_file),
                                              dir_name)
                else:
                    remote_dir = os.path.dirname(remote_file)

                if remote_dir not in remote_dirs:
                    root_cmd('mkdir -p \'{0}\''.format(remote_dir), tty, sudo, **ssh_kwargs)
                    if ssh_kwargs['username'] != 'root':
                        root_cmd(
                            'chown {0} \'{1}\''.format(
                                ssh_kwargs['username'], remote_dir
                            ),
                            tty, sudo, **ssh_kwargs
                        )
                    remote_dirs.append(remote_dir)
                ssh_file(
                    opts, remote_file, kwargs=ssh_kwargs, local_file=local_file
                )
                file_map_success.append({local_file: remote_file})

            # Minion configuration
            if minion_pem:
                ssh_file(opts, '{0}/minion.pem'.format(tmp_dir), minion_pem, ssh_kwargs)
                ret = root_cmd('chmod 600 \'{0}/minion.pem\''.format(tmp_dir),
                               tty, sudo, **ssh_kwargs)
                if ret:
                    raise SaltCloudSystemExit(
                        'Can\'t set perms on {0}/minion.pem'.format(tmp_dir))
            if minion_pub:
                ssh_file(opts, '{0}/minion.pub'.format(tmp_dir), minion_pub, ssh_kwargs)

            if master_sign_pub_file:
                ssh_file(opts, '{0}/master_sign.pub'.format(tmp_dir), kwargs=ssh_kwargs, local_file=master_sign_pub_file)

            if minion_conf:
                if not isinstance(minion_conf, dict):
                    # Let's not just fail regarding this change, specially
                    # since we can handle it
                    raise DeprecationWarning(
                        '`salt.utils.cloud.deploy_script now only accepts '
                        'dictionaries for it\'s `minion_conf` parameter. '
                        'Loading YAML...'
                    )
                minion_grains = minion_conf.pop('grains', {})
                if minion_grains:
                    ssh_file(
                        opts,
                        '{0}/grains'.format(tmp_dir),
                        salt_config_to_yaml(minion_grains),
                        ssh_kwargs
                    )
                if cloud_grains and opts.get('enable_cloud_grains', True):
                    minion_conf['grains'] = {'salt-cloud': cloud_grains}
                ssh_file(
                    opts,
                    '{0}/minion'.format(tmp_dir),
                    salt_config_to_yaml(minion_conf),
                    ssh_kwargs
                )

            # Master configuration
            if master_pem:
                ssh_file(opts, '{0}/master.pem'.format(tmp_dir), master_pem, ssh_kwargs)
                ret = root_cmd('chmod 600 \'{0}/master.pem\''.format(tmp_dir),
                               tty, sudo, **ssh_kwargs)
                if ret:
                    raise SaltCloudSystemExit(
                        'Cant set perms on {0}/master.pem'.format(tmp_dir))

            if master_pub:
                ssh_file(opts, '{0}/master.pub'.format(tmp_dir), master_pub, ssh_kwargs)

            if master_conf:
                if not isinstance(master_conf, dict):
                    # Let's not just fail regarding this change, specially
                    # since we can handle it
                    raise DeprecationWarning(
                        '`salt.utils.cloud.deploy_script now only accepts '
                        'dictionaries for it\'s `master_conf` parameter. '
                        'Loading from YAML ...'
                    )

                ssh_file(
                    opts,
                    '{0}/master'.format(tmp_dir),
                    salt_config_to_yaml(master_conf),
                    ssh_kwargs
                )

            # XXX: We need to make these paths configurable
            preseed_minion_keys_tempdir = '{0}/preseed-minion-keys'.format(
                tmp_dir)
            if preseed_minion_keys is not None:
                # Create remote temp dir
                ret = root_cmd(
                    'mkdir \'{0}\''.format(preseed_minion_keys_tempdir),
                    tty, sudo, **ssh_kwargs
                )
                if ret:
                    raise SaltCloudSystemExit(
                        'Cant create {0}'.format(preseed_minion_keys_tempdir))
                ret = root_cmd(
                    'chmod 700 \'{0}\''.format(preseed_minion_keys_tempdir),
                    tty, sudo, **ssh_kwargs
                )
                if ret:
                    raise SaltCloudSystemExit(
                        'Can\'t set perms on {0}'.format(
                            preseed_minion_keys_tempdir))
                if ssh_kwargs['username'] != 'root':
                    root_cmd(
                        'chown {0} \'{1}\''.format(
                            ssh_kwargs['username'], preseed_minion_keys_tempdir
                        ),
                        tty, sudo, **ssh_kwargs
                    )

                # Copy pre-seed minion keys
                for minion_id, minion_key in six.iteritems(preseed_minion_keys):
                    rpath = os.path.join(
                        preseed_minion_keys_tempdir, minion_id
                    )
                    ssh_file(opts, rpath, minion_key, ssh_kwargs)

                if ssh_kwargs['username'] != 'root':
                    root_cmd(
                        'chown -R root \'{0}\''.format(
                            preseed_minion_keys_tempdir
                        ),
                        tty, sudo, **ssh_kwargs
                    )
                    if ret:
                        raise SaltCloudSystemExit(
                            'Can\'t set ownership for {0}'.format(
                                preseed_minion_keys_tempdir))

            # Run any pre-flight commands before running deploy scripts
            preflight_cmds = kwargs.get('preflight_cmds', [])
            for command in preflight_cmds:
                cmd_ret = root_cmd(command, tty, sudo, **ssh_kwargs)
                if cmd_ret:
                    raise SaltCloudSystemExit(
                        'Pre-flight command failed: \'{0}\''.format(command)
                    )

            # The actual deploy script
            if script:
                # got strange escaping issues with sudoer, going onto a
                # subshell fixes that
                ssh_file(opts, '{0}/deploy.sh'.format(tmp_dir), script, ssh_kwargs)
                ret = root_cmd(
                    ('sh -c "( chmod +x \'{0}/deploy.sh\' )";'
                     'exit $?').format(tmp_dir),
                    tty, sudo, **ssh_kwargs)
                if ret:
                    raise SaltCloudSystemExit(
                        'Can\'t set perms on {0}/deploy.sh'.format(tmp_dir))

            time_used = time.mktime(time.localtime()) - time.mktime(starttime)
            newtimeout = timeout - time_used
            queue = None
            process = None
            # Consider this code experimental. It causes Salt Cloud to wait
            # for the minion to check in, and then fire a startup event.
            # Disabled if parallel because it doesn't work!
            if start_action and not parallel:
                queue = multiprocessing.Queue()
                process = multiprocessing.Process(
                    target=check_auth, kwargs=dict(
                        name=name, sock_dir=sock_dir,
                        timeout=newtimeout, queue=queue
                    )
                )
                log.debug('Starting new process to wait for salt-minion')
                process.start()

            # Run the deploy script
            if script:
                if 'bootstrap-salt' in script:
                    deploy_command += ' -c \'{0}\''.format(tmp_dir)
                    if force_minion_config:
                        deploy_command += ' -F'
                    if make_syndic is True:
                        deploy_command += ' -S'
                    if make_master is True:
                        deploy_command += ' -M'
                    if make_minion is False:
                        deploy_command += ' -N'
                    if keep_tmp is True:
                        deploy_command += ' -K'
                    if preseed_minion_keys is not None:
                        deploy_command += ' -k \'{0}\''.format(
                            preseed_minion_keys_tempdir
                        )
                if script_args:
                    deploy_command += ' {0}'.format(script_args)

                if script_env:
                    if not isinstance(script_env, dict):
                        raise SaltCloudSystemExit(
                            'The \'script_env\' configuration setting NEEDS '
                            'to be a dictionary not a {0}'.format(
                                type(script_env)
                            )
                        )
                    environ_script_contents = ['#!/bin/sh']
                    for key, value in six.iteritems(script_env):
                        environ_script_contents.append(
                            'setenv {0} \'{1}\' >/dev/null 2>&1 || '
                            'export {0}=\'{1}\''.format(key, value)
                        )
                    environ_script_contents.append(deploy_command)

                    # Upload our environ setter wrapper
                    ssh_file(
                        opts,
                        '{0}/environ-deploy-wrapper.sh'.format(tmp_dir),
                        '\n'.join(environ_script_contents),
                        ssh_kwargs
                    )
                    root_cmd(
                        'chmod +x \'{0}/environ-deploy-wrapper.sh\''.format(tmp_dir),
                        tty, sudo, **ssh_kwargs
                    )
                    # The deploy command is now our wrapper
                    deploy_command = '\'{0}/environ-deploy-wrapper.sh\''.format(
                        tmp_dir,
                    )
                if root_cmd(deploy_command, tty, sudo, **ssh_kwargs) != 0:
                    raise SaltCloudSystemExit(
                        'Executing the command \'{0}\' failed'.format(
                            deploy_command
                        )
                    )
                log.debug('Executed command \'%s\'', deploy_command)

                # Remove the deploy script
                if not keep_tmp:
                    root_cmd('rm -f \'{0}/deploy.sh\''.format(tmp_dir),
                             tty, sudo, **ssh_kwargs)
                    log.debug('Removed %s/deploy.sh', tmp_dir)
                    if script_env:
                        root_cmd(
                            'rm -f \'{0}/environ-deploy-wrapper.sh\''.format(
                                tmp_dir
                            ),
                            tty, sudo, **ssh_kwargs
                        )
                        log.debug('Removed %s/environ-deploy-wrapper.sh', tmp_dir)

            if keep_tmp:
                log.debug('Not removing deployment files from %s/', tmp_dir)
            else:
                # Remove minion configuration
                if minion_pub:
                    root_cmd('rm -f \'{0}/minion.pub\''.format(tmp_dir),
                             tty, sudo, **ssh_kwargs)
                    log.debug('Removed %s/minion.pub', tmp_dir)
                if minion_pem:
                    root_cmd('rm -f \'{0}/minion.pem\''.format(tmp_dir),
                             tty, sudo, **ssh_kwargs)
                    log.debug('Removed %s/minion.pem', tmp_dir)
                if minion_conf:
                    root_cmd('rm -f \'{0}/grains\''.format(tmp_dir),
                             tty, sudo, **ssh_kwargs)
                    log.debug('Removed %s/grains', tmp_dir)
                    root_cmd('rm -f \'{0}/minion\''.format(tmp_dir),
                             tty, sudo, **ssh_kwargs)
                    log.debug('Removed %s/minion', tmp_dir)
                if master_sign_pub_file:
                    root_cmd('rm -f {0}/master_sign.pub'.format(tmp_dir),
                             tty, sudo, **ssh_kwargs)
                    log.debug('Removed %s/master_sign.pub', tmp_dir)

                # Remove master configuration
                if master_pub:
                    root_cmd('rm -f \'{0}/master.pub\''.format(tmp_dir),
                             tty, sudo, **ssh_kwargs)
                    log.debug('Removed %s/master.pub', tmp_dir)
                if master_pem:
                    root_cmd('rm -f \'{0}/master.pem\''.format(tmp_dir),
                             tty, sudo, **ssh_kwargs)
                    log.debug('Removed %s/master.pem', tmp_dir)
                if master_conf:
                    root_cmd('rm -f \'{0}/master\''.format(tmp_dir),
                             tty, sudo, **ssh_kwargs)
                    log.debug('Removed %s/master', tmp_dir)

                # Remove pre-seed keys directory
                if preseed_minion_keys is not None:
                    root_cmd(
                        'rm -rf \'{0}\''.format(
                            preseed_minion_keys_tempdir
                        ), tty, sudo, **ssh_kwargs
                    )
                    log.debug('Removed %s', preseed_minion_keys_tempdir)

            if start_action and not parallel:
                queuereturn = queue.get()
                process.join()
                if queuereturn and start_action:
                    # client = salt.client.LocalClient(conf_file)
                    # output = client.cmd_iter(
                    # host, 'state.highstate', timeout=timeout
                    # )
                    # for line in output:
                    #    print(line)
                    log.info('Executing %s on the salt-minion', start_action)
                    root_cmd(
                        'salt-call {0}'.format(start_action),
                        tty, sudo, **ssh_kwargs
                    )
                    log.info(
                        'Finished executing %s on the salt-minion',
                        start_action
                    )
            # Fire deploy action
            fire_event(
                'event',
                '{0} has been deployed at {1}'.format(name, host),
                'salt/cloud/{0}/deploy_script'.format(name),
                args={
                    'name': name,
                    'host': host
                },
                sock_dir=opts.get(
                    'sock_dir',
                    os.path.join(__opts__['sock_dir'], 'master')),
                transport=opts.get('transport', 'zeromq')
            )
            if file_map_fail or file_map_success:
                return {
                    'File Upload Success': file_map_success,
                    'File Upload Failure': file_map_fail,
                }
            return True
    return False