saltstack/salt

View on GitHub
salt/states/virtualenv_mod.py

Summary

Maintainability
F
3 days
Test Coverage
# -*- coding: utf-8 -*-
'''
Setup of Python virtualenv sandboxes.

.. versionadded:: 0.17.0
'''

# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals
import logging
import os

# Import Salt libs
import salt.version
import salt.utils.functools
import salt.utils.platform
import salt.utils.versions
from salt.exceptions import CommandExecutionError, CommandNotFoundError

# Import 3rd-party libs
from salt.ext import six

log = logging.getLogger(__name__)

# Define the module's virtual name
__virtualname__ = 'virtualenv'


def __virtual__():
    return __virtualname__


def managed(name,
            venv_bin=None,
            requirements=None,
            system_site_packages=False,
            distribute=False,
            use_wheel=False,
            clear=False,
            python=None,
            extra_search_dir=None,
            never_download=None,
            prompt=None,
            user=None,
            cwd=None,
            index_url=None,
            extra_index_url=None,
            pre_releases=False,
            no_deps=False,
            pip_download=None,
            pip_download_cache=None,
            pip_exists_action=None,
            pip_ignore_installed=False,
            proxy=None,
            use_vt=False,
            env_vars=None,
            no_use_wheel=False,
            pip_upgrade=False,
            pip_pkgs=None,
            pip_no_cache_dir=False,
            pip_cache_dir=None,
            process_dependency_links=False,
            no_binary=None,
            **kwargs):
    '''
    Create a virtualenv and optionally manage it with pip

    name
        Path to the virtualenv.

    venv_bin: virtualenv
        The name (and optionally path) of the virtualenv command. This can also
        be set globally in the minion config file as ``virtualenv.venv_bin``.

    requirements: None
        Path to a pip requirements file. If the path begins with ``salt://``
        the file will be transferred from the master file server.

    use_wheel: False
        Prefer wheel archives (requires pip >= 1.4).

    python : None
        Python executable used to build the virtualenv

    user: None
        The user under which to run virtualenv and pip.

    cwd: None
        Path to the working directory where `pip install` is executed.

    no_deps: False
        Pass `--no-deps` to `pip install`.

    pip_exists_action: None
        Default action of pip when a path already exists: (s)witch, (i)gnore,
        (w)ipe, (b)ackup.

    proxy: None
        Proxy address which is passed to `pip install`.

    env_vars: None
        Set environment variables that some builds will depend on. For example,
        a Python C-module may have a Makefile that needs INCLUDE_PATH set to
        pick up a header file while compiling.

    no_use_wheel: False
        Force to not use wheel archives (requires pip>=1.4)

    no_binary
        Force to not use binary packages (requires pip >= 7.0.0)
        Accepts either :all: to disable all binary packages, :none: to empty the set,
        or a list of one or more packages

    pip_upgrade: False
        Pass `--upgrade` to `pip install`.

    pip_pkgs: None
        As an alternative to `requirements`, pass a list of pip packages that
        should be installed.

    process_dependency_links: False
        Run pip install with the --process_dependency_links flag.

        .. versionadded:: 2017.7.0

    Also accepts any kwargs that the virtualenv module will. However, some
    kwargs, such as the ``pip`` option, require ``- distribute: True``.

    .. code-block:: yaml

        /var/www/myvirtualenv.com:
          virtualenv.managed:
            - system_site_packages: False
            - requirements: salt://REQUIREMENTS.txt
            - env_vars:
                PATH_VAR: '/usr/local/bin/'
    '''
    ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}

    if 'virtualenv.create' not in __salt__:
        ret['result'] = False
        ret['comment'] = 'Virtualenv was not detected on this system'
        return ret

    if salt.utils.platform.is_windows():
        venv_py = os.path.join(name, 'Scripts', 'python.exe')
    else:
        venv_py = os.path.join(name, 'bin', 'python')
    venv_exists = os.path.exists(venv_py)

    # Bail out early if the specified requirements file can't be found
    if requirements and requirements.startswith('salt://'):
        cached_requirements = __salt__['cp.is_cached'](requirements, __env__)
        if not cached_requirements:
            # It's not cached, let's cache it.
            cached_requirements = __salt__['cp.cache_file'](
                requirements, __env__
            )
        # Check if the master version has changed.
        if cached_requirements and __salt__['cp.hash_file'](requirements, __env__) != \
                __salt__['cp.hash_file'](cached_requirements, __env__):
            cached_requirements = __salt__['cp.cache_file'](
                requirements, __env__
            )
        if not cached_requirements:
            ret.update({
                'result': False,
                'comment': 'pip requirements file \'{0}\' not found'.format(
                    requirements
                )
            })
            return ret
        requirements = cached_requirements

    # If it already exists, grab the version for posterity
    if venv_exists and clear:
        ret['changes']['cleared_packages'] = \
            __salt__['pip.freeze'](bin_env=name)
        ret['changes']['old'] = \
            __salt__['cmd.run_stderr']('{0} -V'.format(venv_py)).strip('\n')

    # Create (or clear) the virtualenv
    if __opts__['test']:
        if venv_exists and clear:
            ret['result'] = None
            ret['comment'] = 'Virtualenv {0} is set to be cleared'.format(name)
            return ret
        if venv_exists and not clear:
            ret['comment'] = 'Virtualenv {0} is already created'.format(name)
            return ret
        ret['result'] = None
        ret['comment'] = 'Virtualenv {0} is set to be created'.format(name)
        return ret

    if not venv_exists or (venv_exists and clear):
        try:
            venv_ret = __salt__['virtualenv.create'](
                name,
                venv_bin=venv_bin,
                system_site_packages=system_site_packages,
                distribute=distribute,
                clear=clear,
                python=python,
                extra_search_dir=extra_search_dir,
                never_download=never_download,
                prompt=prompt,
                user=user,
                use_vt=use_vt,
                **kwargs
            )
        except CommandNotFoundError as err:
            ret['result'] = False
            ret['comment'] = 'Failed to create virtualenv: {0}'.format(err)
            return ret

        if venv_ret['retcode'] != 0:
            ret['result'] = False
            ret['comment'] = venv_ret['stdout'] + venv_ret['stderr']
            return ret

        ret['result'] = True
        ret['changes']['new'] = __salt__['cmd.run_stderr'](
            '{0} -V'.format(venv_py)).strip('\n')

        if clear:
            ret['comment'] = 'Cleared existing virtualenv'
        else:
            ret['comment'] = 'Created new virtualenv'

    elif venv_exists:
        ret['comment'] = 'virtualenv exists'

    # Check that the pip binary supports the 'use_wheel' option
    if use_wheel:
        min_version = '1.4'
        max_version = '9.0.3'
        cur_version = __salt__['pip.version'](bin_env=name)
        too_low = salt.utils.versions.compare(ver1=cur_version, oper='<', ver2=min_version)
        too_high = salt.utils.versions.compare(ver1=cur_version, oper='>', ver2=max_version)
        if too_low or too_high:
            ret['result'] = False
            ret['comment'] = ('The \'use_wheel\' option is only supported in '
                              'pip between {0} and {1}. The version of pip detected '
                              'was {2}.').format(min_version, max_version, cur_version)
            return ret

    # Check that the pip binary supports the 'no_use_wheel' option
    if no_use_wheel:
        min_version = '1.4'
        max_version = '9.0.3'
        cur_version = __salt__['pip.version'](bin_env=name)
        too_low = salt.utils.versions.compare(ver1=cur_version, oper='<', ver2=min_version)
        too_high = salt.utils.versions.compare(ver1=cur_version, oper='>', ver2=max_version)
        if too_low or too_high:
            ret['result'] = False
            ret['comment'] = ('The \'no_use_wheel\' option is only supported in '
                              'pip between {0} and {1}. The version of pip detected '
                              'was {2}.').format(min_version, max_version, cur_version)
            return ret

    # Check that the pip binary supports the 'no_binary' option
    if no_binary:
        min_version = '7.0.0'
        cur_version = __salt__['pip.version'](bin_env=name)
        too_low = salt.utils.versions.compare(ver1=cur_version, oper='<', ver2=min_version)
        if too_low:
            ret['result'] = False
            ret['comment'] = ('The \'no_binary\' option is only supported in '
                              'pip {0} and newer. The version of pip detected '
                              'was {1}.').format(min_version, cur_version)
            return ret

    # Populate the venv via a requirements file
    if requirements or pip_pkgs:
        try:
            before = set(__salt__['pip.freeze'](bin_env=name, user=user, use_vt=use_vt))
        except CommandExecutionError as exc:
            ret['result'] = False
            ret['comment'] = exc.strerror
            return ret

        if requirements:

            if isinstance(requirements, six.string_types):
                req_canary = requirements.split(',')[0]
            elif isinstance(requirements, list):
                req_canary = requirements[0]
            else:
                raise TypeError(
                    'pip requirements must be either a string or a list'
                )

            if req_canary != os.path.abspath(req_canary):
                cwd = os.path.dirname(os.path.abspath(req_canary))

        pip_ret = __salt__['pip.install'](
            pkgs=pip_pkgs,
            requirements=requirements,
            process_dependency_links=process_dependency_links,
            bin_env=name,
            use_wheel=use_wheel,
            no_use_wheel=no_use_wheel,
            no_binary=no_binary,
            user=user,
            cwd=cwd,
            index_url=index_url,
            extra_index_url=extra_index_url,
            download=pip_download,
            download_cache=pip_download_cache,
            pre_releases=pre_releases,
            exists_action=pip_exists_action,
            ignore_installed=pip_ignore_installed,
            upgrade=pip_upgrade,
            no_deps=no_deps,
            proxy=proxy,
            use_vt=use_vt,
            env_vars=env_vars,
            no_cache_dir=pip_no_cache_dir,
            cache_dir=pip_cache_dir,
            **kwargs
        )
        ret['result'] &= pip_ret['retcode'] == 0
        if pip_ret['retcode'] > 0:
            ret['comment'] = '{0}\n{1}\n{2}'.format(ret['comment'],
                                                    pip_ret['stdout'],
                                                    pip_ret['stderr'])

        after = set(__salt__['pip.freeze'](bin_env=name))

        new = list(after - before)
        old = list(before - after)

        if new or old:
            ret['changes']['packages'] = {
                'new': new if new else '',
                'old': old if old else ''}
    return ret


manage = salt.utils.functools.alias_function(managed, 'manage')