saltstack/salt

View on GitHub
salt/utils/versions.py

Summary

Maintainability
D
2 days
Test Coverage
# -*- coding: utf-8 -*-
'''
    :copyright: Copyright 2017 by the SaltStack Team, see AUTHORS for more details.
    :license: Apache 2.0, see LICENSE for more details.


    salt.utils.versions
    ~~~~~~~~~~~~~~~~~~~

    Version parsing based on distutils.version which works under python 3
    because on python 3 you can no longer compare strings against integers.
'''

# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals
import logging
import numbers
import sys
import warnings
# pylint: disable=blacklisted-module,no-name-in-module
from distutils.version import StrictVersion as _StrictVersion
from distutils.version import LooseVersion as _LooseVersion
# pylint: enable=blacklisted-module,no-name-in-module

# Import Salt libs
import salt.version

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

log = logging.getLogger(__name__)


class StrictVersion(_StrictVersion):
    def parse(self, vstring):
        _StrictVersion.parse(self, vstring)

    def _cmp(self, other):
        if isinstance(other, six.string_types):
            other = StrictVersion(other)
        return _StrictVersion._cmp(self, other)


class LooseVersion(_LooseVersion):

    def parse(self, vstring):
        _LooseVersion.parse(self, vstring)

        if six.PY3:
            # Convert every part of the version to string in order to be able to compare
            self._str_version = [
                six.text_type(vp).zfill(8) if isinstance(vp, int) else vp for vp in self.version]

    if six.PY3:
        def _cmp(self, other):
            if isinstance(other, six.string_types):
                other = LooseVersion(other)

            string_in_version = False
            for part in self.version + other.version:
                if not isinstance(part, int):
                    string_in_version = True
                    break

            if string_in_version is False:
                return _LooseVersion._cmp(self, other)

            # If we reached this far, it means at least a part of the version contains a string
            # In python 3, strings and integers are not comparable
            if self._str_version == other._str_version:
                return 0
            if self._str_version < other._str_version:
                return -1
            if self._str_version > other._str_version:
                return 1


def warn_until(version,
               message,
               category=DeprecationWarning,
               stacklevel=None,
               _version_info_=None,
               _dont_call_warnings=False):
    '''
    Helper function to raise a warning, by default, a ``DeprecationWarning``,
    until the provided ``version``, after which, a ``RuntimeError`` will
    be raised to remind the developers to remove the warning because the
    target version has been reached.

    :param version: The version info or name after which the warning becomes a
                    ``RuntimeError``. For example ``(0, 17)`` or ``Hydrogen``
                    or an instance of :class:`salt.version.SaltStackVersion`.
    :param message: The warning message to be displayed.
    :param category: The warning class to be thrown, by default
                     ``DeprecationWarning``
    :param stacklevel: There should be no need to set the value of
                       ``stacklevel``. Salt should be able to do the right thing.
    :param _version_info_: In order to reuse this function for other SaltStack
                           projects, they need to be able to provide the
                           version info to compare to.
    :param _dont_call_warnings: This parameter is used just to get the
                                functionality until the actual error is to be
                                issued. When we're only after the salt version
                                checks to raise a ``RuntimeError``.
    '''
    if not isinstance(version, (tuple,
                                six.string_types,
                                salt.version.SaltStackVersion)):
        raise RuntimeError(
            'The \'version\' argument should be passed as a tuple, string or '
            'an instance of \'salt.version.SaltStackVersion\'.'
        )
    elif isinstance(version, tuple):
        version = salt.version.SaltStackVersion(*version)
    elif isinstance(version, six.string_types):
        version = salt.version.SaltStackVersion.from_name(version)

    if stacklevel is None:
        # Attribute the warning to the calling function, not to warn_until()
        stacklevel = 2

    if _version_info_ is None:
        _version_info_ = salt.version.__version_info__

    _version_ = salt.version.SaltStackVersion(*_version_info_)

    if _version_ >= version:
        import inspect
        caller = inspect.getframeinfo(sys._getframe(stacklevel - 1))
        raise RuntimeError(
            'The warning triggered on filename \'{filename}\', line number '
            '{lineno}, is supposed to be shown until version '
            '{until_version} is released. Current version is now '
            '{salt_version}. Please remove the warning.'.format(
                filename=caller.filename,
                lineno=caller.lineno,
                until_version=version.formatted_version,
                salt_version=_version_.formatted_version
            ),
        )

    if _dont_call_warnings is False:
        def _formatwarning(message,
                           category,
                           filename,
                           lineno,
                           line=None):  # pylint: disable=W0613
            '''
            Replacement for warnings.formatwarning that disables the echoing of
            the 'line' parameter.
            '''
            return '{0}:{1}: {2}: {3}\n'.format(
                filename, lineno, category.__name__, message
            )
        saved = warnings.formatwarning
        warnings.formatwarning = _formatwarning
        warnings.warn(
            message.format(version=version.formatted_version),
            category,
            stacklevel=stacklevel
        )
        warnings.formatwarning = saved


def kwargs_warn_until(kwargs,
                      version,
                      category=DeprecationWarning,
                      stacklevel=None,
                      _version_info_=None,
                      _dont_call_warnings=False):
    '''
    Helper function to raise a warning (by default, a ``DeprecationWarning``)
    when unhandled keyword arguments are passed to function, until the
    provided ``version_info``, after which, a ``RuntimeError`` will be raised
    to remind the developers to remove the ``**kwargs`` because the target
    version has been reached.
    This function is used to help deprecate unused legacy ``**kwargs`` that
    were added to function parameters lists to preserve backwards compatibility
    when removing a parameter. See
    :ref:`the deprecation development docs <deprecations>`
    for the modern strategy for deprecating a function parameter.

    :param kwargs: The caller's ``**kwargs`` argument value (a ``dict``).
    :param version: The version info or name after which the warning becomes a
                    ``RuntimeError``. For example ``(0, 17)`` or ``Hydrogen``
                    or an instance of :class:`salt.version.SaltStackVersion`.
    :param category: The warning class to be thrown, by default
                     ``DeprecationWarning``
    :param stacklevel: There should be no need to set the value of
                       ``stacklevel``. Salt should be able to do the right thing.
    :param _version_info_: In order to reuse this function for other SaltStack
                           projects, they need to be able to provide the
                           version info to compare to.
    :param _dont_call_warnings: This parameter is used just to get the
                                functionality until the actual error is to be
                                issued. When we're only after the salt version
                                checks to raise a ``RuntimeError``.
    '''
    if not isinstance(version, (tuple,
                                six.string_types,
                                salt.version.SaltStackVersion)):
        raise RuntimeError(
            'The \'version\' argument should be passed as a tuple, string or '
            'an instance of \'salt.version.SaltStackVersion\'.'
        )
    elif isinstance(version, tuple):
        version = salt.version.SaltStackVersion(*version)
    elif isinstance(version, six.string_types):
        version = salt.version.SaltStackVersion.from_name(version)

    if stacklevel is None:
        # Attribute the warning to the calling function,
        # not to kwargs_warn_until() or warn_until()
        stacklevel = 3

    if _version_info_ is None:
        _version_info_ = salt.version.__version_info__

    _version_ = salt.version.SaltStackVersion(*_version_info_)

    if kwargs or _version_.info >= version.info:
        arg_names = ', '.join('\'{0}\''.format(key) for key in kwargs)
        warn_until(
            version,
            message='The following parameter(s) have been deprecated and '
                    'will be removed in \'{0}\': {1}.'.format(version.string,
                                                              arg_names),
            category=category,
            stacklevel=stacklevel,
            _version_info_=_version_.info,
            _dont_call_warnings=_dont_call_warnings
        )


def version_cmp(pkg1, pkg2, ignore_epoch=False):
    '''
    Compares two version strings using salt.utils.versions.LooseVersion. This
    is a fallback for providers which don't have a version comparison utility
    built into them.  Return -1 if version1 < version2, 0 if version1 ==
    version2, and 1 if version1 > version2. Return None if there was a problem
    making the comparison.
    '''
    normalize = lambda x: six.text_type(x).split(':', 1)[-1] \
                if ignore_epoch else six.text_type(x)
    pkg1 = normalize(pkg1)
    pkg2 = normalize(pkg2)

    try:
        # pylint: disable=no-member
        if LooseVersion(pkg1) < LooseVersion(pkg2):
            return -1
        elif LooseVersion(pkg1) == LooseVersion(pkg2):
            return 0
        elif LooseVersion(pkg1) > LooseVersion(pkg2):
            return 1
    except Exception as exc:
        log.exception(exc)
    return None


def compare(ver1='', oper='==', ver2='', cmp_func=None, ignore_epoch=False):
    '''
    Compares two version numbers. Accepts a custom function to perform the
    cmp-style version comparison, otherwise uses version_cmp().
    '''
    cmp_map = {'<': (-1,), '<=': (-1, 0), '==': (0,),
               '>=': (0, 1), '>': (1,)}
    if oper not in ('!=',) and oper not in cmp_map:
        log.error('Invalid operator \'%s\' for version comparison', oper)
        return False

    if cmp_func is None:
        cmp_func = version_cmp

    cmp_result = cmp_func(ver1, ver2, ignore_epoch=ignore_epoch)
    if cmp_result is None:
        return False

    # Check if integer/long
    if not isinstance(cmp_result, numbers.Integral):
        log.error('The version comparison function did not return an '
                  'integer/long.')
        return False

    if oper == '!=':
        return cmp_result not in cmp_map['==']
    else:
        # Gracefully handle cmp_result not in (-1, 0, 1).
        if cmp_result < -1:
            cmp_result = -1
        elif cmp_result > 1:
            cmp_result = 1

        return cmp_result in cmp_map[oper]


def check_boto_reqs(boto_ver=None,
                    boto3_ver=None,
                    botocore_ver=None,
                    check_boto=True,
                    check_boto3=True):
    '''
    Checks for the version of various required boto libs in one central location. Most
    boto states and modules rely on a single version of the boto, boto3, or botocore libs.
    However, some require newer versions of any of these dependencies. This function allows
    the module to pass in a version to override the default minimum required version.

    This function is useful in centralizing checks for ``__virtual__()`` functions in the
    various, and many, boto modules and states.

    boto_ver
        The minimum required version of the boto library. Defaults to ``2.0.0``.

    boto3_ver
        The minimum required version of the boto3 library. Defaults to ``1.2.6``.

    botocore_ver
        The minimum required version of the botocore library. Defaults to ``1.3.23``.

    check_boto
        Boolean defining whether or not to check for boto deps. This defaults to ``True`` as
        most boto modules/states rely on boto, but some do not.

    check_boto3
        Boolean defining whether or not to check for boto3 (and therefore botocore) deps.
        This defaults to ``True`` as most boto modules/states rely on boto3/botocore, but
        some do not.
    '''
    if check_boto is True:
        try:
            # Late import so we can only load these for this function
            import boto
            has_boto = True
        except ImportError:
            has_boto = False

        if boto_ver is None:
            boto_ver = '2.0.0'

        if not has_boto or version_cmp(boto.__version__, boto_ver) == -1:
            return False, 'A minimum version of boto {0} is required.'.format(boto_ver)

    if check_boto3 is True:
        try:
            # Late import so we can only load these for this function
            import boto3
            import botocore
            has_boto3 = True
        except ImportError:
            has_boto3 = False

        # boto_s3_bucket module requires boto3 1.2.6 and botocore 1.3.23 for
        # idempotent ACL operations via the fix in https://github.com/boto/boto3/issues/390
        if boto3_ver is None:
            boto3_ver = '1.2.6'
        if botocore_ver is None:
            botocore_ver = '1.3.23'

        if not has_boto3 or version_cmp(boto3.__version__, boto3_ver) == -1:
            return False, 'A minimum version of boto3 {0} is required.'.format(boto3_ver)
        elif version_cmp(botocore.__version__, botocore_ver) == -1:
            return False, 'A minimum version of botocore {0} is required'.format(botocore_ver)

    return True