1""" 2 :copyright: Copyright 2017 by the SaltStack Team, see AUTHORS for more details. 3 :license: Apache 2.0, see LICENSE for more details. 4 5 6 salt.utils.versions 7 ~~~~~~~~~~~~~~~~~~~ 8 9 Version parsing based on distutils.version which works under python 3 10 because on python 3 you can no longer compare strings against integers. 11""" 12 13 14import datetime 15import inspect 16import logging 17import numbers 18import sys 19import warnings 20 21# pylint: disable=blacklisted-module 22from distutils.version import LooseVersion as _LooseVersion 23from distutils.version import StrictVersion as _StrictVersion 24 25# pylint: enable=blacklisted-module 26import salt.version 27 28log = logging.getLogger(__name__) 29 30 31class StrictVersion(_StrictVersion): 32 def parse(self, vstring): 33 _StrictVersion.parse(self, vstring) 34 35 def _cmp(self, other): 36 if isinstance(other, str): 37 other = StrictVersion(other) 38 return _StrictVersion._cmp(self, other) 39 40 41class LooseVersion(_LooseVersion): 42 def parse(self, vstring): 43 _LooseVersion.parse(self, vstring) 44 45 # Convert every part of the version to string in order to be able to compare 46 self._str_version = [ 47 str(vp).zfill(8) if isinstance(vp, int) else vp for vp in self.version 48 ] 49 50 def _cmp(self, other): 51 if isinstance(other, str): 52 other = LooseVersion(other) 53 54 string_in_version = False 55 for part in self.version + other.version: 56 if not isinstance(part, int): 57 string_in_version = True 58 break 59 60 if string_in_version is False: 61 return _LooseVersion._cmp(self, other) 62 63 # If we reached this far, it means at least a part of the version contains a string 64 # In python 3, strings and integers are not comparable 65 if self._str_version == other._str_version: 66 return 0 67 if self._str_version < other._str_version: 68 return -1 69 if self._str_version > other._str_version: 70 return 1 71 72 73def _format_warning(message, category, filename, lineno, line=None): 74 """ 75 Replacement for warnings.formatwarning that disables the echoing of 76 the 'line' parameter. 77 """ 78 return "{}:{}: {}: {}\n".format(filename, lineno, category.__name__, message) 79 80 81def warn_until( 82 version, 83 message, 84 category=DeprecationWarning, 85 stacklevel=None, 86 _version_info_=None, 87 _dont_call_warnings=False, 88): 89 """ 90 Helper function to raise a warning, by default, a ``DeprecationWarning``, 91 until the provided ``version``, after which, a ``RuntimeError`` will 92 be raised to remind the developers to remove the warning because the 93 target version has been reached. 94 95 :param version: The version info or name after which the warning becomes a ``RuntimeError``. 96 For example ``(2019, 2)``, ``3000``, ``Hydrogen`` or an instance of 97 :class:`salt.version.SaltStackVersion` or :class:`salt.version.SaltVersion`. 98 :param message: The warning message to be displayed. 99 :param category: The warning class to be thrown, by default 100 ``DeprecationWarning`` 101 :param stacklevel: There should be no need to set the value of 102 ``stacklevel``. Salt should be able to do the right thing. 103 :param _version_info_: In order to reuse this function for other SaltStack 104 projects, they need to be able to provide the 105 version info to compare to. 106 :param _dont_call_warnings: This parameter is used just to get the 107 functionality until the actual error is to be 108 issued. When we're only after the salt version 109 checks to raise a ``RuntimeError``. 110 """ 111 if isinstance(version, salt.version.SaltVersion): 112 version = salt.version.SaltStackVersion(*version.info) 113 elif isinstance(version, int): 114 version = salt.version.SaltStackVersion(version) 115 elif isinstance(version, tuple): 116 version = salt.version.SaltStackVersion(*version) 117 elif isinstance(version, str): 118 if version.lower() not in salt.version.SaltStackVersion.LNAMES: 119 raise RuntimeError( 120 "Incorrect spelling for the release name in the warn_utils " 121 "call. Expecting one of these release names: {}".format( 122 [vs.name for vs in salt.version.SaltVersionsInfo.versions()] 123 ) 124 ) 125 version = salt.version.SaltStackVersion.from_name(version) 126 elif not isinstance(version, salt.version.SaltStackVersion): 127 raise RuntimeError( 128 "The 'version' argument should be passed as a tuple, integer, string or " 129 "an instance of 'salt.version.SaltVersion' or " 130 "'salt.version.SaltStackVersion'." 131 ) 132 133 if stacklevel is None: 134 # Attribute the warning to the calling function, not to warn_until() 135 stacklevel = 2 136 137 if _version_info_ is None: 138 _version_info_ = salt.version.__version_info__ 139 140 _version_ = salt.version.SaltStackVersion(*_version_info_) 141 142 if _version_ >= version: 143 caller = inspect.getframeinfo(sys._getframe(stacklevel - 1)) 144 raise RuntimeError( 145 "The warning triggered on filename '{filename}', line number " 146 "{lineno}, is supposed to be shown until version " 147 "{until_version} is released. Current version is now " 148 "{salt_version}. Please remove the warning.".format( 149 filename=caller.filename, 150 lineno=caller.lineno, 151 until_version=version.formatted_version, 152 salt_version=_version_.formatted_version, 153 ), 154 ) 155 156 if _dont_call_warnings is False: 157 warnings.warn( 158 message.format(version=version.formatted_version), 159 category, 160 stacklevel=stacklevel, 161 ) 162 163 164def warn_until_date( 165 date, 166 message, 167 category=DeprecationWarning, 168 stacklevel=None, 169 _current_date=None, 170 _dont_call_warnings=False, 171): 172 """ 173 Helper function to raise a warning, by default, a ``DeprecationWarning``, 174 until the provided ``date``, after which, a ``RuntimeError`` will 175 be raised to remind the developers to remove the warning because the 176 target date has been reached. 177 178 :param date: A ``datetime.date`` or ``datetime.datetime`` instance. 179 :param message: The warning message to be displayed. 180 :param category: The warning class to be thrown, by default 181 ``DeprecationWarning`` 182 :param stacklevel: There should be no need to set the value of 183 ``stacklevel``. Salt should be able to do the right thing. 184 :param _dont_call_warnings: This parameter is used just to get the 185 functionality until the actual error is to be 186 issued. When we're only after the date 187 checks to raise a ``RuntimeError``. 188 """ 189 _strptime_fmt = "%Y%m%d" 190 if not isinstance(date, (str, datetime.date, datetime.datetime)): 191 raise RuntimeError( 192 "The 'date' argument should be passed as a 'datetime.date()' or " 193 "'datetime.datetime()' objects or as string parserable by " 194 "'datetime.datetime.strptime()' with the following format '{}'.".format( 195 _strptime_fmt 196 ) 197 ) 198 elif isinstance(date, str): 199 date = datetime.datetime.strptime(date, _strptime_fmt) 200 201 # We're really not interested in the time 202 if isinstance(date, datetime.datetime): 203 date = date.date() 204 205 if stacklevel is None: 206 # Attribute the warning to the calling function, not to warn_until_date() 207 stacklevel = 2 208 209 today = _current_date or datetime.datetime.utcnow().date() 210 if today >= date: 211 caller = inspect.getframeinfo(sys._getframe(stacklevel - 1)) 212 raise RuntimeError( 213 "{message} This warning(now exception) triggered on " 214 "filename '{filename}', line number {lineno}, is " 215 "supposed to be shown until {date}. Today is {today}. " 216 "Please remove the warning.".format( 217 message=message.format(date=date.isoformat(), today=today.isoformat()), 218 filename=caller.filename, 219 lineno=caller.lineno, 220 date=date.isoformat(), 221 today=today.isoformat(), 222 ), 223 ) 224 225 if _dont_call_warnings is False: 226 warnings.warn( 227 message.format(date=date.isoformat(), today=today.isoformat()), 228 category, 229 stacklevel=stacklevel, 230 ) 231 232 233def kwargs_warn_until( 234 kwargs, 235 version, 236 category=DeprecationWarning, 237 stacklevel=None, 238 _version_info_=None, 239 _dont_call_warnings=False, 240): 241 """ 242 Helper function to raise a warning (by default, a ``DeprecationWarning``) 243 when unhandled keyword arguments are passed to function, until the 244 provided ``version_info``, after which, a ``RuntimeError`` will be raised 245 to remind the developers to remove the ``**kwargs`` because the target 246 version has been reached. 247 This function is used to help deprecate unused legacy ``**kwargs`` that 248 were added to function parameters lists to preserve backwards compatibility 249 when removing a parameter. See 250 :ref:`the deprecation development docs <deprecations>` 251 for the modern strategy for deprecating a function parameter. 252 253 :param kwargs: The caller's ``**kwargs`` argument value (a ``dict``). 254 :param version: The version info or name after which the warning becomes a 255 ``RuntimeError``. For example ``(0, 17)`` or ``Hydrogen`` 256 or an instance of :class:`salt.version.SaltStackVersion`. 257 :param category: The warning class to be thrown, by default 258 ``DeprecationWarning`` 259 :param stacklevel: There should be no need to set the value of 260 ``stacklevel``. Salt should be able to do the right thing. 261 :param _version_info_: In order to reuse this function for other SaltStack 262 projects, they need to be able to provide the 263 version info to compare to. 264 :param _dont_call_warnings: This parameter is used just to get the 265 functionality until the actual error is to be 266 issued. When we're only after the salt version 267 checks to raise a ``RuntimeError``. 268 """ 269 if not isinstance(version, (tuple, str, salt.version.SaltStackVersion)): 270 raise RuntimeError( 271 "The 'version' argument should be passed as a tuple, string or " 272 "an instance of 'salt.version.SaltStackVersion'." 273 ) 274 elif isinstance(version, tuple): 275 version = salt.version.SaltStackVersion(*version) 276 elif isinstance(version, str): 277 version = salt.version.SaltStackVersion.from_name(version) 278 279 if stacklevel is None: 280 # Attribute the warning to the calling function, 281 # not to kwargs_warn_until() or warn_until() 282 stacklevel = 3 283 284 if _version_info_ is None: 285 _version_info_ = salt.version.__version_info__ 286 287 _version_ = salt.version.SaltStackVersion(*_version_info_) 288 289 if kwargs or _version_.info >= version.info: 290 arg_names = ", ".join("'{}'".format(key) for key in kwargs) 291 warn_until( 292 version, 293 message=( 294 "The following parameter(s) have been deprecated and " 295 "will be removed in '{}': {}.".format(version.string, arg_names) 296 ), 297 category=category, 298 stacklevel=stacklevel, 299 _version_info_=_version_.info, 300 _dont_call_warnings=_dont_call_warnings, 301 ) 302 303 304def version_cmp(pkg1, pkg2, ignore_epoch=False): 305 """ 306 Compares two version strings using salt.utils.versions.LooseVersion. This 307 is a fallback for providers which don't have a version comparison utility 308 built into them. Return -1 if version1 < version2, 0 if version1 == 309 version2, and 1 if version1 > version2. Return None if there was a problem 310 making the comparison. 311 """ 312 normalize = lambda x: str(x).split(":", 1)[-1] if ignore_epoch else str(x) 313 pkg1 = normalize(pkg1) 314 pkg2 = normalize(pkg2) 315 316 try: 317 # pylint: disable=no-member 318 if LooseVersion(pkg1) < LooseVersion(pkg2): 319 return -1 320 elif LooseVersion(pkg1) == LooseVersion(pkg2): 321 return 0 322 elif LooseVersion(pkg1) > LooseVersion(pkg2): 323 return 1 324 except Exception as exc: # pylint: disable=broad-except 325 log.exception(exc) 326 return None 327 328 329def compare(ver1="", oper="==", ver2="", cmp_func=None, ignore_epoch=False): 330 """ 331 Compares two version numbers. Accepts a custom function to perform the 332 cmp-style version comparison, otherwise uses version_cmp(). 333 """ 334 cmp_map = {"<": (-1,), "<=": (-1, 0), "==": (0,), ">=": (0, 1), ">": (1,)} 335 if oper not in ("!=",) and oper not in cmp_map: 336 log.error("Invalid operator '%s' for version comparison", oper) 337 return False 338 339 if cmp_func is None: 340 cmp_func = version_cmp 341 342 cmp_result = cmp_func(ver1, ver2, ignore_epoch=ignore_epoch) 343 if cmp_result is None: 344 return False 345 346 # Check if integer/long 347 if not isinstance(cmp_result, numbers.Integral): 348 log.error("The version comparison function did not return an integer/long.") 349 return False 350 351 if oper == "!=": 352 return cmp_result not in cmp_map["=="] 353 else: 354 # Gracefully handle cmp_result not in (-1, 0, 1). 355 if cmp_result < -1: 356 cmp_result = -1 357 elif cmp_result > 1: 358 cmp_result = 1 359 360 return cmp_result in cmp_map[oper] 361 362 363def check_boto_reqs( 364 boto_ver=None, boto3_ver=None, botocore_ver=None, check_boto=True, check_boto3=True 365): 366 """ 367 Checks for the version of various required boto libs in one central location. Most 368 boto states and modules rely on a single version of the boto, boto3, or botocore libs. 369 However, some require newer versions of any of these dependencies. This function allows 370 the module to pass in a version to override the default minimum required version. 371 372 This function is useful in centralizing checks for ``__virtual__()`` functions in the 373 various, and many, boto modules and states. 374 375 boto_ver 376 The minimum required version of the boto library. Defaults to ``2.0.0``. 377 378 boto3_ver 379 The minimum required version of the boto3 library. Defaults to ``1.2.6``. 380 381 botocore_ver 382 The minimum required version of the botocore library. Defaults to ``1.3.23``. 383 384 check_boto 385 Boolean defining whether or not to check for boto deps. This defaults to ``True`` as 386 most boto modules/states rely on boto, but some do not. 387 388 check_boto3 389 Boolean defining whether or not to check for boto3 (and therefore botocore) deps. 390 This defaults to ``True`` as most boto modules/states rely on boto3/botocore, but 391 some do not. 392 """ 393 if check_boto is True: 394 try: 395 # Late import so we can only load these for this function 396 import boto 397 398 has_boto = True 399 except ImportError: 400 has_boto = False 401 402 if boto_ver is None: 403 boto_ver = "2.0.0" 404 405 if not has_boto or version_cmp(boto.__version__, boto_ver) == -1: 406 return False, "A minimum version of boto {} is required.".format(boto_ver) 407 408 if check_boto3 is True: 409 try: 410 # Late import so we can only load these for this function 411 import boto3 412 import botocore 413 414 has_boto3 = True 415 except ImportError: 416 has_boto3 = False 417 418 # boto_s3_bucket module requires boto3 1.2.6 and botocore 1.3.23 for 419 # idempotent ACL operations via the fix in https://github.com/boto/boto3/issues/390 420 if boto3_ver is None: 421 boto3_ver = "1.2.6" 422 if botocore_ver is None: 423 botocore_ver = "1.3.23" 424 425 if not has_boto3 or version_cmp(boto3.__version__, boto3_ver) == -1: 426 return ( 427 False, 428 "A minimum version of boto3 {} is required.".format(boto3_ver), 429 ) 430 elif version_cmp(botocore.__version__, botocore_ver) == -1: 431 return ( 432 False, 433 "A minimum version of botocore {} is required".format(botocore_ver), 434 ) 435 436 return True 437