1"""
2Installation of Python Packages Using pip
3=========================================
4
5These states manage system installed python packages. Note that pip must be
6installed for these states to be available, so pip states should include a
7requisite to a pkg.installed state for the package which provides pip
8(``python-pip`` in most cases). Example:
9
10.. code-block:: yaml
11
12    python-pip:
13      pkg.installed
14
15    virtualenvwrapper:
16      pip.installed:
17        - require:
18          - pkg: python-pip
19"""
20
21
22import logging
23import re
24import sys
25import types
26
27import salt.utils.data
28import salt.utils.versions
29from salt.exceptions import CommandExecutionError, CommandNotFoundError
30
31try:
32    import pkg_resources
33
34    HAS_PKG_RESOURCES = True
35except ImportError:
36    HAS_PKG_RESOURCES = False
37
38
39# pylint: disable=import-error
40
41
42def purge_pip():
43    """
44    Purge pip and its sub-modules
45    """
46    # Remove references to the loaded pip module above so reloading works
47    if "pip" not in sys.modules:
48        return
49    pip_related_entries = [
50        (k, v)
51        for (k, v) in sys.modules.items()
52        if getattr(v, "__module__", "").startswith("pip.")
53        or (isinstance(v, types.ModuleType) and v.__name__.startswith("pip."))
54    ]
55    for name, entry in pip_related_entries:
56        sys.modules.pop(name)
57        del entry
58
59    if "pip" in globals():
60        del globals()["pip"]
61    if "pip" in locals():
62        del locals()["pip"]
63    sys_modules_pip = sys.modules.pop("pip", None)
64    if sys_modules_pip is not None:
65        del sys_modules_pip
66
67
68def pip_has_internal_exceptions_mod(ver):
69    """
70    True when the pip version has the `pip._internal.exceptions` module
71    """
72    return salt.utils.versions.compare(
73        ver1=ver,
74        oper=">=",
75        ver2="10.0",
76    )
77
78
79def pip_has_exceptions_mod(ver):
80    """
81    True when the pip version has the `pip.exceptions` module
82    """
83    if pip_has_internal_exceptions_mod(ver):
84        return False
85    return salt.utils.versions.compare(ver1=ver, oper=">=", ver2="1.0")
86
87
88try:
89    import pip
90
91    HAS_PIP = True
92except ImportError:
93    HAS_PIP = False
94    purge_pip()
95
96
97if HAS_PIP is True:
98    if not hasattr(purge_pip, "__pip_ver__"):
99        purge_pip.__pip_ver__ = pip.__version__
100    elif purge_pip.__pip_ver__ != pip.__version__:
101        purge_pip()
102        import pip
103
104        purge_pip.__pip_ver__ = pip.__version__
105    if salt.utils.versions.compare(ver1=pip.__version__, oper=">=", ver2="10.0"):
106        from pip._internal.exceptions import (
107            InstallationError,
108        )  # pylint: disable=E0611,E0401
109    elif salt.utils.versions.compare(ver1=pip.__version__, oper=">=", ver2="1.0"):
110        from pip.exceptions import InstallationError  # pylint: disable=E0611,E0401
111    else:
112        InstallationError = ValueError
113
114
115# pylint: enable=import-error
116
117log = logging.getLogger(__name__)
118
119# Define the module's virtual name
120__virtualname__ = "pip"
121
122
123def _from_line(*args, **kwargs):
124    import pip
125
126    if salt.utils.versions.compare(ver1=pip.__version__, oper=">=", ver2="18.1"):
127        import pip._internal.req.constructors  # pylint: disable=E0611,E0401
128
129        return pip._internal.req.constructors.install_req_from_line(*args, **kwargs)
130    elif salt.utils.versions.compare(ver1=pip.__version__, oper=">=", ver2="10.0"):
131        import pip._internal.req  # pylint: disable=E0611,E0401
132
133        return pip._internal.req.InstallRequirement.from_line(*args, **kwargs)
134    else:
135        import pip.req  # pylint: disable=E0611,E0401
136
137        return pip.req.InstallRequirement.from_line(*args, **kwargs)
138
139
140def __virtual__():
141    """
142    Only load if the pip module is available in __salt__
143    """
144    if HAS_PKG_RESOURCES is False:
145        return False, "The pkg_resources python library is not installed"
146    if "pip.list" in __salt__:
147        return __virtualname__
148    return False
149
150
151def _fulfills_version_spec(version, version_spec):
152    """
153    Check version number against version specification info and return a
154    boolean value based on whether or not the version number meets the
155    specified version.
156    """
157    for oper, spec in version_spec:
158        if oper is None:
159            continue
160        if not salt.utils.versions.compare(
161            ver1=version, oper=oper, ver2=spec, cmp_func=_pep440_version_cmp
162        ):
163            return False
164    return True
165
166
167def _check_pkg_version_format(pkg):
168    """
169    Takes a package name and version specification (if any) and checks it using
170    the pip library.
171    """
172
173    ret = {"result": False, "comment": None, "prefix": None, "version_spec": None}
174
175    if not HAS_PIP:
176        ret["comment"] = (
177            "An importable Python 2 pip module is required but could not be "
178            "found on your system. This usually means that the system's pip "
179            "package is not installed properly."
180        )
181
182        return ret
183
184    from_vcs = False
185    try:
186        # Get the requirement object from the pip library
187        try:
188            # With pip < 1.2, the __version__ attribute does not exist and
189            # vcs+URL urls are not properly parsed.
190            # The next line is meant to trigger an AttributeError and
191            # handle lower pip versions
192            log.debug("Installed pip version: %s", pip.__version__)
193            install_req = _from_line(pkg)
194        except AttributeError:
195            log.debug("Installed pip version is lower than 1.2")
196            supported_vcs = ("git", "svn", "hg", "bzr")
197            if pkg.startswith(supported_vcs):
198                for vcs in supported_vcs:
199                    if pkg.startswith(vcs):
200                        from_vcs = True
201                        install_req = _from_line(pkg.split("{}+".format(vcs))[-1])
202                        break
203            else:
204                install_req = _from_line(pkg)
205    except (ValueError, InstallationError) as exc:
206        ret["result"] = False
207        if not from_vcs and "=" in pkg and "==" not in pkg:
208            ret["comment"] = (
209                "Invalid version specification in package {}. '=' is "
210                "not supported, use '==' instead.".format(pkg)
211            )
212            return ret
213        ret["comment"] = "pip raised an exception while parsing '{}': {}".format(
214            pkg, exc
215        )
216        return ret
217
218    if install_req.req is None:
219        # This is most likely an url and there's no way to know what will
220        # be installed before actually installing it.
221        ret["result"] = True
222        ret["prefix"] = ""
223        ret["version_spec"] = []
224    else:
225        ret["result"] = True
226        try:
227            ret["prefix"] = install_req.req.project_name
228            ret["version_spec"] = install_req.req.specs
229        except Exception:  # pylint: disable=broad-except
230            ret["prefix"] = re.sub("[^A-Za-z0-9.]+", "-", install_req.name)
231            if hasattr(install_req, "specifier"):
232                specifier = install_req.specifier
233            else:
234                specifier = install_req.req.specifier
235            ret["version_spec"] = [(spec.operator, spec.version) for spec in specifier]
236
237    return ret
238
239
240def _check_if_installed(
241    prefix,
242    state_pkg_name,
243    version_spec,
244    ignore_installed,
245    force_reinstall,
246    upgrade,
247    user,
248    cwd,
249    bin_env,
250    env_vars,
251    index_url,
252    extra_index_url,
253    pip_list=False,
254    **kwargs
255):
256    """
257    Takes a package name and version specification (if any) and checks it is
258    installed
259
260    Keyword arguments include:
261        pip_list: optional dict of installed pip packages, and their versions,
262            to search through to check if the package is installed. If not
263            provided, one will be generated in this function by querying the
264            system.
265
266    Returns:
267     result: None means the command failed to run
268     result: True means the package is installed
269     result: False means the package is not installed
270    """
271    ret = {"result": False, "comment": None}
272
273    # If we are not passed a pip list, get one:
274    pip_list = salt.utils.data.CaseInsensitiveDict(
275        pip_list
276        or __salt__["pip.list"](
277            prefix, bin_env=bin_env, user=user, cwd=cwd, env_vars=env_vars, **kwargs
278        )
279    )
280
281    # If the package was already installed, check
282    # the ignore_installed and force_reinstall flags
283    if ignore_installed is False and prefix in pip_list:
284        if force_reinstall is False and not upgrade:
285            # Check desired version (if any) against currently-installed
286            if (
287                any(version_spec)
288                and _fulfills_version_spec(pip_list[prefix], version_spec)
289            ) or (not any(version_spec)):
290                ret["result"] = True
291                ret["comment"] = "Python package {} was already installed".format(
292                    state_pkg_name
293                )
294                return ret
295        if force_reinstall is False and upgrade:
296            # Check desired version (if any) against currently-installed
297            include_alpha = False
298            include_beta = False
299            include_rc = False
300            if any(version_spec):
301                for spec in version_spec:
302                    if "a" in spec[1]:
303                        include_alpha = True
304                    if "b" in spec[1]:
305                        include_beta = True
306                    if "rc" in spec[1]:
307                        include_rc = True
308            available_versions = __salt__["pip.list_all_versions"](
309                prefix,
310                bin_env=bin_env,
311                include_alpha=include_alpha,
312                include_beta=include_beta,
313                include_rc=include_rc,
314                user=user,
315                cwd=cwd,
316                index_url=index_url,
317                extra_index_url=extra_index_url,
318            )
319            desired_version = ""
320            if any(version_spec) and available_versions:
321                for version in reversed(available_versions):
322                    if _fulfills_version_spec(version, version_spec):
323                        desired_version = version
324                        break
325            elif available_versions:
326                desired_version = available_versions[-1]
327            if not desired_version:
328                ret["result"] = True
329                ret["comment"] = (
330                    "Python package {} was already "
331                    "installed and\nthe available upgrade "
332                    "doesn't fulfills the version "
333                    "requirements".format(prefix)
334                )
335                return ret
336            if _pep440_version_cmp(pip_list[prefix], desired_version) == 0:
337                ret["result"] = True
338                ret["comment"] = "Python package {} was already installed".format(
339                    state_pkg_name
340                )
341                return ret
342
343    return ret
344
345
346def _pep440_version_cmp(pkg1, pkg2, ignore_epoch=False):
347    """
348    Compares two version strings using pkg_resources.parse_version.
349    Return -1 if version1 < version2, 0 if version1 ==version2,
350    and 1 if version1 > version2. Return None if there was a problem
351    making the comparison.
352    """
353    if HAS_PKG_RESOURCES is False:
354        log.warning(
355            "The pkg_resources packages was not loaded. Please install setuptools."
356        )
357        return None
358    normalize = lambda x: str(x).split("!", 1)[-1] if ignore_epoch else str(x)
359    pkg1 = normalize(pkg1)
360    pkg2 = normalize(pkg2)
361
362    try:
363        if pkg_resources.parse_version(pkg1) < pkg_resources.parse_version(pkg2):
364            return -1
365        if pkg_resources.parse_version(pkg1) == pkg_resources.parse_version(pkg2):
366            return 0
367        if pkg_resources.parse_version(pkg1) > pkg_resources.parse_version(pkg2):
368            return 1
369    except Exception as exc:  # pylint: disable=broad-except
370        log.exception(exc)
371    return None
372
373
374def installed(
375    name,
376    pkgs=None,
377    pip_bin=None,
378    requirements=None,
379    bin_env=None,
380    use_wheel=False,
381    no_use_wheel=False,
382    log=None,
383    proxy=None,
384    timeout=None,
385    repo=None,
386    editable=None,
387    find_links=None,
388    index_url=None,
389    extra_index_url=None,
390    no_index=False,
391    mirrors=None,
392    build=None,
393    target=None,
394    download=None,
395    download_cache=None,
396    source=None,
397    upgrade=False,
398    force_reinstall=False,
399    ignore_installed=False,
400    exists_action=None,
401    no_deps=False,
402    no_install=False,
403    no_download=False,
404    install_options=None,
405    global_options=None,
406    user=None,
407    cwd=None,
408    pre_releases=False,
409    cert=None,
410    allow_all_external=False,
411    allow_external=None,
412    allow_unverified=None,
413    process_dependency_links=False,
414    env_vars=None,
415    use_vt=False,
416    trusted_host=None,
417    no_cache_dir=False,
418    cache_dir=None,
419    no_binary=None,
420    extra_args=None,
421    **kwargs
422):
423    """
424    Make sure the package is installed
425
426    name
427        The name of the python package to install. You can also specify version
428        numbers here using the standard operators ``==, >=, <=``. If
429        ``requirements`` is given, this parameter will be ignored.
430
431    Example:
432
433    .. code-block:: yaml
434
435        django:
436          pip.installed:
437            - name: django >= 1.6, <= 1.7
438            - require:
439              - pkg: python-pip
440
441    This will install the latest Django version greater than 1.6 but less
442    than 1.7.
443
444    requirements
445        Path to a pip requirements file. If the path begins with salt://
446        the file will be transferred from the master file server.
447
448    user
449        The user under which to run pip
450
451    use_wheel : False
452        Prefer wheel archives (requires pip>=1.4)
453
454    no_use_wheel : False
455        Force to not use wheel archives (requires pip>=1.4)
456
457    no_binary
458        Force to not use binary packages (requires pip >= 7.0.0)
459        Accepts either :all: to disable all binary packages, :none: to empty the set,
460        or a list of one or more packages
461
462    Example:
463
464    .. code-block:: yaml
465
466        django:
467          pip.installed:
468            - no_binary: ':all:'
469
470        flask:
471          pip.installed:
472            - no_binary:
473              - itsdangerous
474              - click
475
476    log
477        Log file where a complete (maximum verbosity) record will be kept
478
479    proxy
480        Specify a proxy in the form
481        user:passwd@proxy.server:port. Note that the
482        user:password@ is optional and required only if you
483        are behind an authenticated proxy.  If you provide
484        user@proxy.server:port then you will be prompted for a
485        password.
486
487    timeout
488        Set the socket timeout (default 15 seconds)
489
490    editable
491        install something editable (i.e.
492        git+https://github.com/worldcompany/djangoembed.git#egg=djangoembed)
493
494    find_links
495        URL to look for packages at
496
497    index_url
498        Base URL of Python Package Index
499
500    extra_index_url
501        Extra URLs of package indexes to use in addition to ``index_url``
502
503    no_index
504        Ignore package index
505
506    mirrors
507        Specific mirror URL(s) to query (automatically adds --use-mirrors)
508
509    build
510        Unpack packages into ``build`` dir
511
512    target
513        Install packages into ``target`` dir
514
515    download
516        Download packages into ``download`` instead of installing them
517
518    download_cache
519        Cache downloaded packages in ``download_cache`` dir
520
521    source
522        Check out ``editable`` packages into ``source`` dir
523
524    upgrade
525        Upgrade all packages to the newest available version
526
527    force_reinstall
528        When upgrading, reinstall all packages even if they are already
529        up-to-date.
530
531    ignore_installed
532        Ignore the installed packages (reinstalling instead)
533
534    exists_action
535        Default action when a path already exists: (s)witch, (i)gnore, (w)ipe,
536        (b)ackup
537
538    no_deps
539        Ignore package dependencies
540
541    no_install
542        Download and unpack all packages, but don't actually install them
543
544    no_cache_dir:
545        Disable the cache.
546
547    cwd
548        Current working directory to run pip from
549
550    pre_releases
551        Include pre-releases in the available versions
552
553    cert
554        Provide a path to an alternate CA bundle
555
556    allow_all_external
557        Allow the installation of all externally hosted files
558
559    allow_external
560        Allow the installation of externally hosted files (comma separated list)
561
562    allow_unverified
563        Allow the installation of insecure and unverifiable files (comma separated list)
564
565    process_dependency_links
566        Enable the processing of dependency links
567
568    bin_env : None
569        Absolute path to a virtual environment directory or absolute path to
570        a pip executable. The example below assumes a virtual environment
571        has been created at ``/foo/.virtualenvs/bar``.
572
573    env_vars
574        Add or modify environment variables. Useful for tweaking build steps,
575        such as specifying INCLUDE or LIBRARY paths in Makefiles, build scripts or
576        compiler calls.  This must be in the form of a dictionary or a mapping.
577
578        Example:
579
580        .. code-block:: yaml
581
582            django:
583              pip.installed:
584                - name: django_app
585                - env_vars:
586                    CUSTOM_PATH: /opt/django_app
587                    VERBOSE: True
588
589    use_vt
590        Use VT terminal emulation (see output while installing)
591
592    trusted_host
593        Mark this host as trusted, even though it does not have valid or any
594        HTTPS.
595
596    Example:
597
598    .. code-block:: yaml
599
600        django:
601          pip.installed:
602            - name: django >= 1.6, <= 1.7
603            - bin_env: /foo/.virtualenvs/bar
604            - require:
605              - pkg: python-pip
606
607    Or
608
609    Example:
610
611    .. code-block:: yaml
612
613        django:
614          pip.installed:
615            - name: django >= 1.6, <= 1.7
616            - bin_env: /foo/.virtualenvs/bar/bin/pip
617            - require:
618              - pkg: python-pip
619
620    .. admonition:: Attention
621
622        The following arguments are deprecated, do not use.
623
624    pip_bin : None
625        Deprecated, use ``bin_env``
626
627    .. versionchanged:: 0.17.0
628        ``use_wheel`` option added.
629
630    install_options
631
632        Extra arguments to be supplied to the setup.py install command.
633        If you are using an option with a directory path, be sure to use
634        absolute path.
635
636        Example:
637
638        .. code-block:: yaml
639
640            django:
641              pip.installed:
642                - name: django
643                - install_options:
644                  - --prefix=/blah
645                - require:
646                  - pkg: python-pip
647
648    global_options
649        Extra global options to be supplied to the setup.py call before the
650        install command.
651
652        .. versionadded:: 2014.1.3
653
654    .. admonition:: Attention
655
656        As of Salt 0.17.0 the pip state **needs** an importable pip module.
657        This usually means having the system's pip package installed or running
658        Salt from an active `virtualenv`_.
659
660        The reason for this requirement is because ``pip`` already does a
661        pretty good job parsing its own requirements. It makes no sense for
662        Salt to do ``pip`` requirements parsing and validation before passing
663        them to the ``pip`` library. It's functionality duplication and it's
664        more error prone.
665
666
667    .. admonition:: Attention
668
669        Please set ``reload_modules: True`` to have the salt minion
670        import this module after installation.
671
672
673    Example:
674
675    .. code-block:: yaml
676
677        pyopenssl:
678            pip.installed:
679                - name: pyOpenSSL
680                - reload_modules: True
681                - exists_action: i
682
683    extra_args
684        pip keyword and positional arguments not yet implemented in salt
685
686        .. code-block:: yaml
687
688            pandas:
689              pip.installed:
690                - name: pandas
691                - extra_args:
692                  - --latest-pip-kwarg: param
693                  - --latest-pip-arg
694
695        .. warning::
696
697            If unsupported options are passed here that are not supported in a
698            minion's version of pip, a `No such option error` will be thrown.
699
700
701    .. _`virtualenv`: http://www.virtualenv.org/en/latest/
702    """
703    if pip_bin and not bin_env:
704        bin_env = pip_bin
705
706    # If pkgs is present, ignore name
707    if pkgs:
708        if not isinstance(pkgs, list):
709            return {
710                "name": name,
711                "result": False,
712                "changes": {},
713                "comment": "pkgs argument must be formatted as a list",
714            }
715    else:
716        pkgs = [name]
717
718    # Assumption: If `pkg` is not an `string`, it's a `collections.OrderedDict`
719    # prepro = lambda pkg: pkg if type(pkg) == str else \
720    #     ' '.join((pkg.items()[0][0], pkg.items()[0][1].replace(',', ';')))
721    # pkgs = ','.join([prepro(pkg) for pkg in pkgs])
722    prepro = (
723        lambda pkg: pkg
724        if isinstance(pkg, str)
725        else " ".join((pkg.items()[0][0], pkg.items()[0][1]))
726    )
727    pkgs = [prepro(pkg) for pkg in pkgs]
728
729    ret = {"name": ";".join(pkgs), "result": None, "comment": "", "changes": {}}
730
731    try:
732        cur_version = __salt__["pip.version"](bin_env)
733    except (CommandNotFoundError, CommandExecutionError) as err:
734        ret["result"] = None
735        ret["comment"] = "Error installing '{}': {}".format(name, err)
736        return ret
737    # Check that the pip binary supports the 'use_wheel' option
738    if use_wheel:
739        min_version = "1.4"
740        max_version = "9.0.3"
741        too_low = salt.utils.versions.compare(
742            ver1=cur_version, oper="<", ver2=min_version
743        )
744        too_high = salt.utils.versions.compare(
745            ver1=cur_version, oper=">", ver2=max_version
746        )
747        if too_low or too_high:
748            ret["result"] = False
749            ret["comment"] = (
750                "The 'use_wheel' option is only supported in "
751                "pip between {} and {}. The version of pip detected "
752                "was {}.".format(min_version, max_version, cur_version)
753            )
754            return ret
755
756    # Check that the pip binary supports the 'no_use_wheel' option
757    if no_use_wheel:
758        min_version = "1.4"
759        max_version = "9.0.3"
760        too_low = salt.utils.versions.compare(
761            ver1=cur_version, oper="<", ver2=min_version
762        )
763        too_high = salt.utils.versions.compare(
764            ver1=cur_version, oper=">", ver2=max_version
765        )
766        if too_low or too_high:
767            ret["result"] = False
768            ret["comment"] = (
769                "The 'no_use_wheel' option is only supported in "
770                "pip between {} and {}. The version of pip detected "
771                "was {}.".format(min_version, max_version, cur_version)
772            )
773            return ret
774
775    # Check that the pip binary supports the 'no_binary' option
776    if no_binary:
777        min_version = "7.0.0"
778        too_low = salt.utils.versions.compare(
779            ver1=cur_version, oper="<", ver2=min_version
780        )
781        if too_low:
782            ret["result"] = False
783            ret["comment"] = (
784                "The 'no_binary' option is only supported in "
785                "pip {} and newer. The version of pip detected "
786                "was {}.".format(min_version, cur_version)
787            )
788            return ret
789
790    # Get the packages parsed name and version from the pip library.
791    # This only is done when there is no requirements or editable parameter.
792    pkgs_details = []
793    if pkgs and not (requirements or editable):
794        comments = []
795        for pkg in iter(pkgs):
796            out = _check_pkg_version_format(pkg)
797            if out["result"] is False:
798                ret["result"] = False
799                comments.append(out["comment"])
800            elif out["result"] is True:
801                pkgs_details.append((out["prefix"], pkg, out["version_spec"]))
802
803        if ret["result"] is False:
804            ret["comment"] = "\n".join(comments)
805            return ret
806
807    # If a requirements file is specified, only install the contents of the
808    # requirements file. Similarly, using the --editable flag with pip should
809    # also ignore the "name" and "pkgs" parameters.
810    target_pkgs = []
811    already_installed_comments = []
812    if requirements or editable:
813        comments = []
814        # Append comments if this is a dry run.
815        if __opts__["test"]:
816            ret["result"] = None
817            if requirements:
818                # TODO: Check requirements file against currently-installed
819                # packages to provide more accurate state output.
820                comments.append(
821                    "Requirements file '{}' will be processed.".format(requirements)
822                )
823            if editable:
824                comments.append(
825                    "Package will be installed in editable mode (i.e. "
826                    'setuptools "develop mode") from {}.'.format(editable)
827                )
828            ret["comment"] = " ".join(comments)
829            return ret
830
831    # No requirements case.
832    # Check pre-existence of the requested packages.
833    else:
834        # Attempt to pre-cache a the current pip list
835        try:
836            pip_list = __salt__["pip.list"](bin_env=bin_env, user=user, cwd=cwd)
837        # If we fail, then just send False, and we'll try again in the next function call
838        except Exception as exc:  # pylint: disable=broad-except
839            log.exception(exc)
840            pip_list = False
841
842        for prefix, state_pkg_name, version_spec in pkgs_details:
843
844            if prefix:
845                out = _check_if_installed(
846                    prefix,
847                    state_pkg_name,
848                    version_spec,
849                    ignore_installed,
850                    force_reinstall,
851                    upgrade,
852                    user,
853                    cwd,
854                    bin_env,
855                    env_vars,
856                    index_url,
857                    extra_index_url,
858                    pip_list,
859                    **kwargs
860                )
861                # If _check_if_installed result is None, something went wrong with
862                # the command running. This way we keep stateful output.
863                if out["result"] is None:
864                    ret["result"] = False
865                    ret["comment"] = out["comment"]
866                    return ret
867            else:
868                out = {"result": False, "comment": None}
869
870            result = out["result"]
871
872            # The package is not present. Add it to the pkgs to install.
873            if result is False:
874                # Replace commas (used for version ranges) with semicolons
875                # (which are not supported) in name so it does not treat
876                # them as multiple packages.
877                target_pkgs.append((prefix, state_pkg_name.replace(",", ";")))
878
879                # Append comments if this is a dry run.
880                if __opts__["test"]:
881                    # If there is more than one package, compute
882                    # the total amount and exit
883                    if len(pkgs_details) > 1:
884                        msg = "Python package(s) set to be installed:"
885                        for pkg in pkgs_details:
886                            msg += "\n"
887                            msg += pkg[1]
888                            ret["comment"] = msg
889                    else:
890                        msg = "Python package {0} is set to be installed"
891                        ret["comment"] = msg.format(state_pkg_name)
892                    ret["result"] = None
893                    return ret
894
895            # The package is already present and will not be reinstalled.
896            elif result is True:
897                # Append comment stating its presence
898                already_installed_comments.append(out["comment"])
899
900            # The command pip.list failed. Abort.
901            elif result is None:
902                ret["result"] = None
903                ret["comment"] = out["comment"]
904                return ret
905
906        # No packages to install.
907        if not target_pkgs:
908            ret["result"] = True
909            aicomms = "\n".join(already_installed_comments)
910            last_line = "All specified packages are already installed" + (
911                " and up-to-date" if upgrade else ""
912            )
913            ret["comment"] = aicomms + ("\n" if aicomms else "") + last_line
914            return ret
915
916    # Construct the string that will get passed to the install call
917    pkgs_str = ",".join([state_name for _, state_name in target_pkgs])
918
919    # Call to install the package. Actual installation takes place here
920    pip_install_call = __salt__["pip.install"](
921        pkgs="{}".format(pkgs_str) if pkgs_str else "",
922        requirements=requirements,
923        bin_env=bin_env,
924        use_wheel=use_wheel,
925        no_use_wheel=no_use_wheel,
926        no_binary=no_binary,
927        log=log,
928        proxy=proxy,
929        timeout=timeout,
930        editable=editable,
931        find_links=find_links,
932        index_url=index_url,
933        extra_index_url=extra_index_url,
934        no_index=no_index,
935        mirrors=mirrors,
936        build=build,
937        target=target,
938        download=download,
939        download_cache=download_cache,
940        source=source,
941        upgrade=upgrade,
942        force_reinstall=force_reinstall,
943        ignore_installed=ignore_installed,
944        exists_action=exists_action,
945        no_deps=no_deps,
946        no_install=no_install,
947        no_download=no_download,
948        install_options=install_options,
949        global_options=global_options,
950        user=user,
951        cwd=cwd,
952        pre_releases=pre_releases,
953        cert=cert,
954        allow_all_external=allow_all_external,
955        allow_external=allow_external,
956        allow_unverified=allow_unverified,
957        process_dependency_links=process_dependency_links,
958        saltenv=__env__,
959        env_vars=env_vars,
960        use_vt=use_vt,
961        trusted_host=trusted_host,
962        no_cache_dir=no_cache_dir,
963        extra_args=extra_args,
964        disable_version_check=True,
965        **kwargs
966    )
967
968    if pip_install_call and pip_install_call.get("retcode", 1) == 0:
969        ret["result"] = True
970
971        if requirements or editable:
972            comments = []
973            if requirements:
974                PIP_REQUIREMENTS_NOCHANGE = [
975                    "Requirement already satisfied",
976                    "Requirement already up-to-date",
977                    "Requirement not upgraded",
978                    "Collecting",
979                    "Cloning",
980                    "Cleaning up...",
981                    "Looking in indexes",
982                ]
983                for line in pip_install_call.get("stdout", "").split("\n"):
984                    if not any(
985                        [line.strip().startswith(x) for x in PIP_REQUIREMENTS_NOCHANGE]
986                    ):
987                        ret["changes"]["requirements"] = True
988                if ret["changes"].get("requirements"):
989                    comments.append(
990                        "Successfully processed requirements file {}.".format(
991                            requirements
992                        )
993                    )
994                else:
995                    comments.append("Requirements were already installed.")
996
997            if editable:
998                comments.append(
999                    "Package successfully installed from VCS checkout {}.".format(
1000                        editable
1001                    )
1002                )
1003                ret["changes"]["editable"] = True
1004            ret["comment"] = " ".join(comments)
1005        else:
1006
1007            # Check that the packages set to be installed were installed.
1008            # Create comments reporting success and failures
1009            pkg_404_comms = []
1010
1011            already_installed_packages = set()
1012            for line in pip_install_call.get("stdout", "").split("\n"):
1013                # Output for already installed packages:
1014                # 'Requirement already up-to-date: jinja2 in /usr/local/lib/python2.7/dist-packages\nCleaning up...'
1015                if line.startswith("Requirement already up-to-date: "):
1016                    package = line.split(":", 1)[1].split()[0]
1017                    already_installed_packages.add(package.lower())
1018
1019            for prefix, state_name in target_pkgs:
1020
1021                # Case for packages that are not an URL
1022                if prefix:
1023                    pipsearch = salt.utils.data.CaseInsensitiveDict(
1024                        __salt__["pip.list"](
1025                            prefix,
1026                            bin_env,
1027                            user=user,
1028                            cwd=cwd,
1029                            env_vars=env_vars,
1030                            **kwargs
1031                        )
1032                    )
1033
1034                    # If we didn't find the package in the system after
1035                    # installing it report it
1036                    if not pipsearch:
1037                        pkg_404_comms.append(
1038                            "There was no error installing package '{}' "
1039                            "although it does not show when calling "
1040                            "'pip.freeze'.".format(pkg)
1041                        )
1042                    else:
1043                        if (
1044                            prefix in pipsearch
1045                            and prefix.lower() not in already_installed_packages
1046                        ):
1047                            ver = pipsearch[prefix]
1048                            ret["changes"]["{}=={}".format(prefix, ver)] = "Installed"
1049                # Case for packages that are an URL
1050                else:
1051                    ret["changes"]["{}==???".format(state_name)] = "Installed"
1052
1053            # Set comments
1054            aicomms = "\n".join(already_installed_comments)
1055            succ_comm = (
1056                "All packages were successfully installed"
1057                if not pkg_404_comms
1058                else "\n".join(pkg_404_comms)
1059            )
1060            ret["comment"] = aicomms + ("\n" if aicomms else "") + succ_comm
1061
1062            return ret
1063
1064    elif pip_install_call:
1065        ret["result"] = False
1066        if "stdout" in pip_install_call:
1067            error = "Error: {} {}".format(
1068                pip_install_call["stdout"], pip_install_call["stderr"]
1069            )
1070        else:
1071            error = "Error: {}".format(pip_install_call["comment"])
1072
1073        if requirements or editable:
1074            comments = []
1075            if requirements:
1076                comments.append(
1077                    'Unable to process requirements file "{}"'.format(requirements)
1078                )
1079            if editable:
1080                comments.append(
1081                    "Unable to install from VCS checkout {}.".format(editable)
1082                )
1083            comments.append(error)
1084            ret["comment"] = " ".join(comments)
1085        else:
1086            pkgs_str = ", ".join([state_name for _, state_name in target_pkgs])
1087            aicomms = "\n".join(already_installed_comments)
1088            error_comm = "Failed to install packages: {}. {}".format(pkgs_str, error)
1089            ret["comment"] = aicomms + ("\n" if aicomms else "") + error_comm
1090    else:
1091        ret["result"] = False
1092        ret["comment"] = "Could not install package"
1093
1094    return ret
1095
1096
1097def removed(
1098    name,
1099    requirements=None,
1100    bin_env=None,
1101    log=None,
1102    proxy=None,
1103    timeout=None,
1104    user=None,
1105    cwd=None,
1106    use_vt=False,
1107):
1108    """
1109    Make sure that a package is not installed.
1110
1111    name
1112        The name of the package to uninstall
1113    user
1114        The user under which to run pip
1115    bin_env : None
1116        the pip executable or virtualenenv to use
1117    use_vt
1118        Use VT terminal emulation (see output while installing)
1119    """
1120    ret = {"name": name, "result": None, "comment": "", "changes": {}}
1121
1122    try:
1123        pip_list = __salt__["pip.list"](bin_env=bin_env, user=user, cwd=cwd)
1124    except (CommandExecutionError, CommandNotFoundError) as err:
1125        ret["result"] = False
1126        ret["comment"] = "Error uninstalling '{}': {}".format(name, err)
1127        return ret
1128
1129    if name not in pip_list:
1130        ret["result"] = True
1131        ret["comment"] = "Package is not installed."
1132        return ret
1133
1134    if __opts__["test"]:
1135        ret["result"] = None
1136        ret["comment"] = "Package {} is set to be removed".format(name)
1137        return ret
1138
1139    if __salt__["pip.uninstall"](
1140        pkgs=name,
1141        requirements=requirements,
1142        bin_env=bin_env,
1143        log=log,
1144        proxy=proxy,
1145        timeout=timeout,
1146        user=user,
1147        cwd=cwd,
1148        use_vt=use_vt,
1149    ):
1150        ret["result"] = True
1151        ret["changes"][name] = "Removed"
1152        ret["comment"] = "Package was successfully removed."
1153    else:
1154        ret["result"] = False
1155        ret["comment"] = "Could not remove package."
1156    return ret
1157
1158
1159def uptodate(name, bin_env=None, user=None, cwd=None, use_vt=False):
1160    """
1161    .. versionadded:: 2015.5.0
1162
1163    Verify that the system is completely up to date.
1164
1165    name
1166        The name has no functional value and is only used as a tracking
1167        reference
1168    user
1169        The user under which to run pip
1170    bin_env
1171        the pip executable or virtualenenv to use
1172    use_vt
1173        Use VT terminal emulation (see output while installing)
1174    """
1175    ret = {"name": name, "changes": {}, "result": False, "comment": "Failed to update."}
1176
1177    try:
1178        packages = __salt__["pip.list_upgrades"](bin_env=bin_env, user=user, cwd=cwd)
1179    except Exception as e:  # pylint: disable=broad-except
1180        ret["comment"] = str(e)
1181        return ret
1182
1183    if not packages:
1184        ret["comment"] = "System is already up-to-date."
1185        ret["result"] = True
1186        return ret
1187    elif __opts__["test"]:
1188        ret["comment"] = "System update will be performed"
1189        ret["result"] = None
1190        return ret
1191
1192    updated = __salt__["pip.upgrade"](
1193        bin_env=bin_env, user=user, cwd=cwd, use_vt=use_vt
1194    )
1195
1196    if updated.get("result") is False:
1197        ret.update(updated)
1198    elif updated:
1199        ret["changes"] = updated
1200        ret["comment"] = "Upgrade successful."
1201        ret["result"] = True
1202    else:
1203        ret["comment"] = "Upgrade failed."
1204
1205    return ret
1206