1# Copyright 2015,2016,2017 Nir Cohen 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15""" 16The ``distro`` package (``distro`` stands for Linux Distribution) provides 17information about the Linux distribution it runs on, such as a reliable 18machine-readable distro ID, or version information. 19 20It is the recommended replacement for Python's original 21:py:func:`platform.linux_distribution` function, but it provides much more 22functionality. An alternative implementation became necessary because Python 233.5 deprecated this function, and Python 3.8 removed it altogether. Its 24predecessor function :py:func:`platform.dist` was already deprecated since 25Python 2.6 and removed in Python 3.8. Still, there are many cases in which 26access to OS distribution information is needed. See `Python issue 1322 27<https://bugs.python.org/issue1322>`_ for more information. 28""" 29 30import argparse 31import json 32import logging 33import os 34import re 35import shlex 36import subprocess 37import sys 38import warnings 39 40__version__ = "1.6.0" 41 42# Use `if False` to avoid an ImportError on Python 2. After dropping Python 2 43# support, can use typing.TYPE_CHECKING instead. See: 44# https://docs.python.org/3/library/typing.html#typing.TYPE_CHECKING 45if False: # pragma: nocover 46 from typing import ( 47 Any, 48 Callable, 49 Dict, 50 Iterable, 51 Optional, 52 Sequence, 53 TextIO, 54 Tuple, 55 Type, 56 TypedDict, 57 Union, 58 ) 59 60 VersionDict = TypedDict( 61 "VersionDict", {"major": str, "minor": str, "build_number": str} 62 ) 63 InfoDict = TypedDict( 64 "InfoDict", 65 { 66 "id": str, 67 "version": str, 68 "version_parts": VersionDict, 69 "like": str, 70 "codename": str, 71 }, 72 ) 73 74 75_UNIXCONFDIR = os.environ.get("UNIXCONFDIR", "/etc") 76_UNIXUSRLIBDIR = os.environ.get("UNIXUSRLIBDIR", "/usr/lib") 77_OS_RELEASE_BASENAME = "os-release" 78 79#: Translation table for normalizing the "ID" attribute defined in os-release 80#: files, for use by the :func:`distro.id` method. 81#: 82#: * Key: Value as defined in the os-release file, translated to lower case, 83#: with blanks translated to underscores. 84#: 85#: * Value: Normalized value. 86NORMALIZED_OS_ID = { 87 "ol": "oracle", # Oracle Linux 88} 89 90#: Translation table for normalizing the "Distributor ID" attribute returned by 91#: the lsb_release command, for use by the :func:`distro.id` method. 92#: 93#: * Key: Value as returned by the lsb_release command, translated to lower 94#: case, with blanks translated to underscores. 95#: 96#: * Value: Normalized value. 97NORMALIZED_LSB_ID = { 98 "enterpriseenterpriseas": "oracle", # Oracle Enterprise Linux 4 99 "enterpriseenterpriseserver": "oracle", # Oracle Linux 5 100 "redhatenterpriseworkstation": "rhel", # RHEL 6, 7 Workstation 101 "redhatenterpriseserver": "rhel", # RHEL 6, 7 Server 102 "redhatenterprisecomputenode": "rhel", # RHEL 6 ComputeNode 103} 104 105#: Translation table for normalizing the distro ID derived from the file name 106#: of distro release files, for use by the :func:`distro.id` method. 107#: 108#: * Key: Value as derived from the file name of a distro release file, 109#: translated to lower case, with blanks translated to underscores. 110#: 111#: * Value: Normalized value. 112NORMALIZED_DISTRO_ID = { 113 "redhat": "rhel", # RHEL 6.x, 7.x 114} 115 116# Pattern for content of distro release file (reversed) 117_DISTRO_RELEASE_CONTENT_REVERSED_PATTERN = re.compile( 118 r"(?:[^)]*\)(.*)\()? *(?:STL )?([\d.+\-a-z]*\d) *(?:esaeler *)?(.+)" 119) 120 121# Pattern for base file name of distro release file 122_DISTRO_RELEASE_BASENAME_PATTERN = re.compile(r"(\w+)[-_](release|version)$") 123 124# Base file names to be ignored when searching for distro release file 125_DISTRO_RELEASE_IGNORE_BASENAMES = ( 126 "debian_version", 127 "lsb-release", 128 "oem-release", 129 _OS_RELEASE_BASENAME, 130 "system-release", 131 "plesk-release", 132 "iredmail-release", 133) 134 135 136def linux_distribution(full_distribution_name=True): 137 # type: (bool) -> Tuple[str, str, str] 138 """ 139 .. deprecated:: 1.6.0 140 141 :func:`distro.linux_distribution()` is deprecated. It should only be 142 used as a compatibility shim with Python's 143 :py:func:`platform.linux_distribution()`. Please use :func:`distro.id`, 144 :func:`distro.version` and :func:`distro.name` instead. 145 146 Return information about the current OS distribution as a tuple 147 ``(id_name, version, codename)`` with items as follows: 148 149 * ``id_name``: If *full_distribution_name* is false, the result of 150 :func:`distro.id`. Otherwise, the result of :func:`distro.name`. 151 152 * ``version``: The result of :func:`distro.version`. 153 154 * ``codename``: The result of :func:`distro.codename`. 155 156 The interface of this function is compatible with the original 157 :py:func:`platform.linux_distribution` function, supporting a subset of 158 its parameters. 159 160 The data it returns may not exactly be the same, because it uses more data 161 sources than the original function, and that may lead to different data if 162 the OS distribution is not consistent across multiple data sources it 163 provides (there are indeed such distributions ...). 164 165 Another reason for differences is the fact that the :func:`distro.id` 166 method normalizes the distro ID string to a reliable machine-readable value 167 for a number of popular OS distributions. 168 """ 169 warnings.warn( 170 "distro.linux_distribution() is deprecated. It should only be used as a " 171 "compatibility shim with Python's platform.linux_distribution(). Please use " 172 "distro.id(), distro.version() and distro.name() instead.", 173 DeprecationWarning, 174 stacklevel=2, 175 ) 176 return _distro.linux_distribution(full_distribution_name) 177 178 179def id(): 180 # type: () -> str 181 """ 182 Return the distro ID of the current distribution, as a 183 machine-readable string. 184 185 For a number of OS distributions, the returned distro ID value is 186 *reliable*, in the sense that it is documented and that it does not change 187 across releases of the distribution. 188 189 This package maintains the following reliable distro ID values: 190 191 ============== ========================================= 192 Distro ID Distribution 193 ============== ========================================= 194 "ubuntu" Ubuntu 195 "debian" Debian 196 "rhel" RedHat Enterprise Linux 197 "centos" CentOS 198 "fedora" Fedora 199 "sles" SUSE Linux Enterprise Server 200 "opensuse" openSUSE 201 "amazon" Amazon Linux 202 "arch" Arch Linux 203 "cloudlinux" CloudLinux OS 204 "exherbo" Exherbo Linux 205 "gentoo" GenToo Linux 206 "ibm_powerkvm" IBM PowerKVM 207 "kvmibm" KVM for IBM z Systems 208 "linuxmint" Linux Mint 209 "mageia" Mageia 210 "mandriva" Mandriva Linux 211 "parallels" Parallels 212 "pidora" Pidora 213 "raspbian" Raspbian 214 "oracle" Oracle Linux (and Oracle Enterprise Linux) 215 "scientific" Scientific Linux 216 "slackware" Slackware 217 "xenserver" XenServer 218 "openbsd" OpenBSD 219 "netbsd" NetBSD 220 "freebsd" FreeBSD 221 "midnightbsd" MidnightBSD 222 ============== ========================================= 223 224 If you have a need to get distros for reliable IDs added into this set, 225 or if you find that the :func:`distro.id` function returns a different 226 distro ID for one of the listed distros, please create an issue in the 227 `distro issue tracker`_. 228 229 **Lookup hierarchy and transformations:** 230 231 First, the ID is obtained from the following sources, in the specified 232 order. The first available and non-empty value is used: 233 234 * the value of the "ID" attribute of the os-release file, 235 236 * the value of the "Distributor ID" attribute returned by the lsb_release 237 command, 238 239 * the first part of the file name of the distro release file, 240 241 The so determined ID value then passes the following transformations, 242 before it is returned by this method: 243 244 * it is translated to lower case, 245 246 * blanks (which should not be there anyway) are translated to underscores, 247 248 * a normalization of the ID is performed, based upon 249 `normalization tables`_. The purpose of this normalization is to ensure 250 that the ID is as reliable as possible, even across incompatible changes 251 in the OS distributions. A common reason for an incompatible change is 252 the addition of an os-release file, or the addition of the lsb_release 253 command, with ID values that differ from what was previously determined 254 from the distro release file name. 255 """ 256 return _distro.id() 257 258 259def name(pretty=False): 260 # type: (bool) -> str 261 """ 262 Return the name of the current OS distribution, as a human-readable 263 string. 264 265 If *pretty* is false, the name is returned without version or codename. 266 (e.g. "CentOS Linux") 267 268 If *pretty* is true, the version and codename are appended. 269 (e.g. "CentOS Linux 7.1.1503 (Core)") 270 271 **Lookup hierarchy:** 272 273 The name is obtained from the following sources, in the specified order. 274 The first available and non-empty value is used: 275 276 * If *pretty* is false: 277 278 - the value of the "NAME" attribute of the os-release file, 279 280 - the value of the "Distributor ID" attribute returned by the lsb_release 281 command, 282 283 - the value of the "<name>" field of the distro release file. 284 285 * If *pretty* is true: 286 287 - the value of the "PRETTY_NAME" attribute of the os-release file, 288 289 - the value of the "Description" attribute returned by the lsb_release 290 command, 291 292 - the value of the "<name>" field of the distro release file, appended 293 with the value of the pretty version ("<version_id>" and "<codename>" 294 fields) of the distro release file, if available. 295 """ 296 return _distro.name(pretty) 297 298 299def version(pretty=False, best=False): 300 # type: (bool, bool) -> str 301 """ 302 Return the version of the current OS distribution, as a human-readable 303 string. 304 305 If *pretty* is false, the version is returned without codename (e.g. 306 "7.0"). 307 308 If *pretty* is true, the codename in parenthesis is appended, if the 309 codename is non-empty (e.g. "7.0 (Maipo)"). 310 311 Some distributions provide version numbers with different precisions in 312 the different sources of distribution information. Examining the different 313 sources in a fixed priority order does not always yield the most precise 314 version (e.g. for Debian 8.2, or CentOS 7.1). 315 316 The *best* parameter can be used to control the approach for the returned 317 version: 318 319 If *best* is false, the first non-empty version number in priority order of 320 the examined sources is returned. 321 322 If *best* is true, the most precise version number out of all examined 323 sources is returned. 324 325 **Lookup hierarchy:** 326 327 In all cases, the version number is obtained from the following sources. 328 If *best* is false, this order represents the priority order: 329 330 * the value of the "VERSION_ID" attribute of the os-release file, 331 * the value of the "Release" attribute returned by the lsb_release 332 command, 333 * the version number parsed from the "<version_id>" field of the first line 334 of the distro release file, 335 * the version number parsed from the "PRETTY_NAME" attribute of the 336 os-release file, if it follows the format of the distro release files. 337 * the version number parsed from the "Description" attribute returned by 338 the lsb_release command, if it follows the format of the distro release 339 files. 340 """ 341 return _distro.version(pretty, best) 342 343 344def version_parts(best=False): 345 # type: (bool) -> Tuple[str, str, str] 346 """ 347 Return the version of the current OS distribution as a tuple 348 ``(major, minor, build_number)`` with items as follows: 349 350 * ``major``: The result of :func:`distro.major_version`. 351 352 * ``minor``: The result of :func:`distro.minor_version`. 353 354 * ``build_number``: The result of :func:`distro.build_number`. 355 356 For a description of the *best* parameter, see the :func:`distro.version` 357 method. 358 """ 359 return _distro.version_parts(best) 360 361 362def major_version(best=False): 363 # type: (bool) -> str 364 """ 365 Return the major version of the current OS distribution, as a string, 366 if provided. 367 Otherwise, the empty string is returned. The major version is the first 368 part of the dot-separated version string. 369 370 For a description of the *best* parameter, see the :func:`distro.version` 371 method. 372 """ 373 return _distro.major_version(best) 374 375 376def minor_version(best=False): 377 # type: (bool) -> str 378 """ 379 Return the minor version of the current OS distribution, as a string, 380 if provided. 381 Otherwise, the empty string is returned. The minor version is the second 382 part of the dot-separated version string. 383 384 For a description of the *best* parameter, see the :func:`distro.version` 385 method. 386 """ 387 return _distro.minor_version(best) 388 389 390def build_number(best=False): 391 # type: (bool) -> str 392 """ 393 Return the build number of the current OS distribution, as a string, 394 if provided. 395 Otherwise, the empty string is returned. The build number is the third part 396 of the dot-separated version string. 397 398 For a description of the *best* parameter, see the :func:`distro.version` 399 method. 400 """ 401 return _distro.build_number(best) 402 403 404def like(): 405 # type: () -> str 406 """ 407 Return a space-separated list of distro IDs of distributions that are 408 closely related to the current OS distribution in regards to packaging 409 and programming interfaces, for example distributions the current 410 distribution is a derivative from. 411 412 **Lookup hierarchy:** 413 414 This information item is only provided by the os-release file. 415 For details, see the description of the "ID_LIKE" attribute in the 416 `os-release man page 417 <http://www.freedesktop.org/software/systemd/man/os-release.html>`_. 418 """ 419 return _distro.like() 420 421 422def codename(): 423 # type: () -> str 424 """ 425 Return the codename for the release of the current OS distribution, 426 as a string. 427 428 If the distribution does not have a codename, an empty string is returned. 429 430 Note that the returned codename is not always really a codename. For 431 example, openSUSE returns "x86_64". This function does not handle such 432 cases in any special way and just returns the string it finds, if any. 433 434 **Lookup hierarchy:** 435 436 * the codename within the "VERSION" attribute of the os-release file, if 437 provided, 438 439 * the value of the "Codename" attribute returned by the lsb_release 440 command, 441 442 * the value of the "<codename>" field of the distro release file. 443 """ 444 return _distro.codename() 445 446 447def info(pretty=False, best=False): 448 # type: (bool, bool) -> InfoDict 449 """ 450 Return certain machine-readable information items about the current OS 451 distribution in a dictionary, as shown in the following example: 452 453 .. sourcecode:: python 454 455 { 456 'id': 'rhel', 457 'version': '7.0', 458 'version_parts': { 459 'major': '7', 460 'minor': '0', 461 'build_number': '' 462 }, 463 'like': 'fedora', 464 'codename': 'Maipo' 465 } 466 467 The dictionary structure and keys are always the same, regardless of which 468 information items are available in the underlying data sources. The values 469 for the various keys are as follows: 470 471 * ``id``: The result of :func:`distro.id`. 472 473 * ``version``: The result of :func:`distro.version`. 474 475 * ``version_parts -> major``: The result of :func:`distro.major_version`. 476 477 * ``version_parts -> minor``: The result of :func:`distro.minor_version`. 478 479 * ``version_parts -> build_number``: The result of 480 :func:`distro.build_number`. 481 482 * ``like``: The result of :func:`distro.like`. 483 484 * ``codename``: The result of :func:`distro.codename`. 485 486 For a description of the *pretty* and *best* parameters, see the 487 :func:`distro.version` method. 488 """ 489 return _distro.info(pretty, best) 490 491 492def os_release_info(): 493 # type: () -> Dict[str, str] 494 """ 495 Return a dictionary containing key-value pairs for the information items 496 from the os-release file data source of the current OS distribution. 497 498 See `os-release file`_ for details about these information items. 499 """ 500 return _distro.os_release_info() 501 502 503def lsb_release_info(): 504 # type: () -> Dict[str, str] 505 """ 506 Return a dictionary containing key-value pairs for the information items 507 from the lsb_release command data source of the current OS distribution. 508 509 See `lsb_release command output`_ for details about these information 510 items. 511 """ 512 return _distro.lsb_release_info() 513 514 515def distro_release_info(): 516 # type: () -> Dict[str, str] 517 """ 518 Return a dictionary containing key-value pairs for the information items 519 from the distro release file data source of the current OS distribution. 520 521 See `distro release file`_ for details about these information items. 522 """ 523 return _distro.distro_release_info() 524 525 526def uname_info(): 527 # type: () -> Dict[str, str] 528 """ 529 Return a dictionary containing key-value pairs for the information items 530 from the distro release file data source of the current OS distribution. 531 """ 532 return _distro.uname_info() 533 534 535def os_release_attr(attribute): 536 # type: (str) -> str 537 """ 538 Return a single named information item from the os-release file data source 539 of the current OS distribution. 540 541 Parameters: 542 543 * ``attribute`` (string): Key of the information item. 544 545 Returns: 546 547 * (string): Value of the information item, if the item exists. 548 The empty string, if the item does not exist. 549 550 See `os-release file`_ for details about these information items. 551 """ 552 return _distro.os_release_attr(attribute) 553 554 555def lsb_release_attr(attribute): 556 # type: (str) -> str 557 """ 558 Return a single named information item from the lsb_release command output 559 data source of the current OS distribution. 560 561 Parameters: 562 563 * ``attribute`` (string): Key of the information item. 564 565 Returns: 566 567 * (string): Value of the information item, if the item exists. 568 The empty string, if the item does not exist. 569 570 See `lsb_release command output`_ for details about these information 571 items. 572 """ 573 return _distro.lsb_release_attr(attribute) 574 575 576def distro_release_attr(attribute): 577 # type: (str) -> str 578 """ 579 Return a single named information item from the distro release file 580 data source of the current OS distribution. 581 582 Parameters: 583 584 * ``attribute`` (string): Key of the information item. 585 586 Returns: 587 588 * (string): Value of the information item, if the item exists. 589 The empty string, if the item does not exist. 590 591 See `distro release file`_ for details about these information items. 592 """ 593 return _distro.distro_release_attr(attribute) 594 595 596def uname_attr(attribute): 597 # type: (str) -> str 598 """ 599 Return a single named information item from the distro release file 600 data source of the current OS distribution. 601 602 Parameters: 603 604 * ``attribute`` (string): Key of the information item. 605 606 Returns: 607 608 * (string): Value of the information item, if the item exists. 609 The empty string, if the item does not exist. 610 """ 611 return _distro.uname_attr(attribute) 612 613 614try: 615 from functools import cached_property 616except ImportError: 617 # Python < 3.8 618 class cached_property(object): # type: ignore 619 """A version of @property which caches the value. On access, it calls the 620 underlying function and sets the value in `__dict__` so future accesses 621 will not re-call the property. 622 """ 623 624 def __init__(self, f): 625 # type: (Callable[[Any], Any]) -> None 626 self._fname = f.__name__ 627 self._f = f 628 629 def __get__(self, obj, owner): 630 # type: (Any, Type[Any]) -> Any 631 assert obj is not None, "call {} on an instance".format(self._fname) 632 ret = obj.__dict__[self._fname] = self._f(obj) 633 return ret 634 635 636class LinuxDistribution(object): 637 """ 638 Provides information about a OS distribution. 639 640 This package creates a private module-global instance of this class with 641 default initialization arguments, that is used by the 642 `consolidated accessor functions`_ and `single source accessor functions`_. 643 By using default initialization arguments, that module-global instance 644 returns data about the current OS distribution (i.e. the distro this 645 package runs on). 646 647 Normally, it is not necessary to create additional instances of this class. 648 However, in situations where control is needed over the exact data sources 649 that are used, instances of this class can be created with a specific 650 distro release file, or a specific os-release file, or without invoking the 651 lsb_release command. 652 """ 653 654 def __init__( 655 self, 656 include_lsb=True, 657 os_release_file="", 658 distro_release_file="", 659 include_uname=True, 660 root_dir=None, 661 ): 662 # type: (bool, str, str, bool, Optional[str]) -> None 663 """ 664 The initialization method of this class gathers information from the 665 available data sources, and stores that in private instance attributes. 666 Subsequent access to the information items uses these private instance 667 attributes, so that the data sources are read only once. 668 669 Parameters: 670 671 * ``include_lsb`` (bool): Controls whether the 672 `lsb_release command output`_ is included as a data source. 673 674 If the lsb_release command is not available in the program execution 675 path, the data source for the lsb_release command will be empty. 676 677 * ``os_release_file`` (string): The path name of the 678 `os-release file`_ that is to be used as a data source. 679 680 An empty string (the default) will cause the default path name to 681 be used (see `os-release file`_ for details). 682 683 If the specified or defaulted os-release file does not exist, the 684 data source for the os-release file will be empty. 685 686 * ``distro_release_file`` (string): The path name of the 687 `distro release file`_ that is to be used as a data source. 688 689 An empty string (the default) will cause a default search algorithm 690 to be used (see `distro release file`_ for details). 691 692 If the specified distro release file does not exist, or if no default 693 distro release file can be found, the data source for the distro 694 release file will be empty. 695 696 * ``include_uname`` (bool): Controls whether uname command output is 697 included as a data source. If the uname command is not available in 698 the program execution path the data source for the uname command will 699 be empty. 700 701 * ``root_dir`` (string): The absolute path to the root directory to use 702 to find distro-related information files. 703 704 Public instance attributes: 705 706 * ``os_release_file`` (string): The path name of the 707 `os-release file`_ that is actually used as a data source. The 708 empty string if no distro release file is used as a data source. 709 710 * ``distro_release_file`` (string): The path name of the 711 `distro release file`_ that is actually used as a data source. The 712 empty string if no distro release file is used as a data source. 713 714 * ``include_lsb`` (bool): The result of the ``include_lsb`` parameter. 715 This controls whether the lsb information will be loaded. 716 717 * ``include_uname`` (bool): The result of the ``include_uname`` 718 parameter. This controls whether the uname information will 719 be loaded. 720 721 Raises: 722 723 * :py:exc:`IOError`: Some I/O issue with an os-release file or distro 724 release file. 725 726 * :py:exc:`subprocess.CalledProcessError`: The lsb_release command had 727 some issue (other than not being available in the program execution 728 path). 729 730 * :py:exc:`UnicodeError`: A data source has unexpected characters or 731 uses an unexpected encoding. 732 """ 733 self.root_dir = root_dir 734 self.etc_dir = os.path.join(root_dir, "etc") if root_dir else _UNIXCONFDIR 735 self.usr_lib_dir = ( 736 os.path.join(root_dir, "usr/lib") if root_dir else _UNIXUSRLIBDIR 737 ) 738 739 if os_release_file: 740 self.os_release_file = os_release_file 741 else: 742 etc_dir_os_release_file = os.path.join(self.etc_dir, _OS_RELEASE_BASENAME) 743 usr_lib_os_release_file = os.path.join( 744 self.usr_lib_dir, _OS_RELEASE_BASENAME 745 ) 746 747 # NOTE: The idea is to respect order **and** have it set 748 # at all times for API backwards compatibility. 749 if os.path.isfile(etc_dir_os_release_file) or not os.path.isfile( 750 usr_lib_os_release_file 751 ): 752 self.os_release_file = etc_dir_os_release_file 753 else: 754 self.os_release_file = usr_lib_os_release_file 755 756 self.distro_release_file = distro_release_file or "" # updated later 757 self.include_lsb = include_lsb 758 self.include_uname = include_uname 759 760 def __repr__(self): 761 # type: () -> str 762 """Return repr of all info""" 763 return ( 764 "LinuxDistribution(" 765 "os_release_file={self.os_release_file!r}, " 766 "distro_release_file={self.distro_release_file!r}, " 767 "include_lsb={self.include_lsb!r}, " 768 "include_uname={self.include_uname!r}, " 769 "_os_release_info={self._os_release_info!r}, " 770 "_lsb_release_info={self._lsb_release_info!r}, " 771 "_distro_release_info={self._distro_release_info!r}, " 772 "_uname_info={self._uname_info!r})".format(self=self) 773 ) 774 775 def linux_distribution(self, full_distribution_name=True): 776 # type: (bool) -> Tuple[str, str, str] 777 """ 778 Return information about the OS distribution that is compatible 779 with Python's :func:`platform.linux_distribution`, supporting a subset 780 of its parameters. 781 782 For details, see :func:`distro.linux_distribution`. 783 """ 784 return ( 785 self.name() if full_distribution_name else self.id(), 786 self.version(), 787 self.codename(), 788 ) 789 790 def id(self): 791 # type: () -> str 792 """Return the distro ID of the OS distribution, as a string. 793 794 For details, see :func:`distro.id`. 795 """ 796 797 def normalize(distro_id, table): 798 # type: (str, Dict[str, str]) -> str 799 distro_id = distro_id.lower().replace(" ", "_") 800 return table.get(distro_id, distro_id) 801 802 distro_id = self.os_release_attr("id") 803 if distro_id: 804 return normalize(distro_id, NORMALIZED_OS_ID) 805 806 distro_id = self.lsb_release_attr("distributor_id") 807 if distro_id: 808 return normalize(distro_id, NORMALIZED_LSB_ID) 809 810 distro_id = self.distro_release_attr("id") 811 if distro_id: 812 return normalize(distro_id, NORMALIZED_DISTRO_ID) 813 814 distro_id = self.uname_attr("id") 815 if distro_id: 816 return normalize(distro_id, NORMALIZED_DISTRO_ID) 817 818 return "" 819 820 def name(self, pretty=False): 821 # type: (bool) -> str 822 """ 823 Return the name of the OS distribution, as a string. 824 825 For details, see :func:`distro.name`. 826 """ 827 name = ( 828 self.os_release_attr("name") 829 or self.lsb_release_attr("distributor_id") 830 or self.distro_release_attr("name") 831 or self.uname_attr("name") 832 ) 833 if pretty: 834 name = self.os_release_attr("pretty_name") or self.lsb_release_attr( 835 "description" 836 ) 837 if not name: 838 name = self.distro_release_attr("name") or self.uname_attr("name") 839 version = self.version(pretty=True) 840 if version: 841 name = name + " " + version 842 return name or "" 843 844 def version(self, pretty=False, best=False): 845 # type: (bool, bool) -> str 846 """ 847 Return the version of the OS distribution, as a string. 848 849 For details, see :func:`distro.version`. 850 """ 851 versions = [ 852 self.os_release_attr("version_id"), 853 self.lsb_release_attr("release"), 854 self.distro_release_attr("version_id"), 855 self._parse_distro_release_content(self.os_release_attr("pretty_name")).get( 856 "version_id", "" 857 ), 858 self._parse_distro_release_content( 859 self.lsb_release_attr("description") 860 ).get("version_id", ""), 861 self.uname_attr("release"), 862 ] 863 version = "" 864 if best: 865 # This algorithm uses the last version in priority order that has 866 # the best precision. If the versions are not in conflict, that 867 # does not matter; otherwise, using the last one instead of the 868 # first one might be considered a surprise. 869 for v in versions: 870 if v.count(".") > version.count(".") or version == "": 871 version = v 872 else: 873 for v in versions: 874 if v != "": 875 version = v 876 break 877 if pretty and version and self.codename(): 878 version = "{0} ({1})".format(version, self.codename()) 879 return version 880 881 def version_parts(self, best=False): 882 # type: (bool) -> Tuple[str, str, str] 883 """ 884 Return the version of the OS distribution, as a tuple of version 885 numbers. 886 887 For details, see :func:`distro.version_parts`. 888 """ 889 version_str = self.version(best=best) 890 if version_str: 891 version_regex = re.compile(r"(\d+)\.?(\d+)?\.?(\d+)?") 892 matches = version_regex.match(version_str) 893 if matches: 894 major, minor, build_number = matches.groups() 895 return major, minor or "", build_number or "" 896 return "", "", "" 897 898 def major_version(self, best=False): 899 # type: (bool) -> str 900 """ 901 Return the major version number of the current distribution. 902 903 For details, see :func:`distro.major_version`. 904 """ 905 return self.version_parts(best)[0] 906 907 def minor_version(self, best=False): 908 # type: (bool) -> str 909 """ 910 Return the minor version number of the current distribution. 911 912 For details, see :func:`distro.minor_version`. 913 """ 914 return self.version_parts(best)[1] 915 916 def build_number(self, best=False): 917 # type: (bool) -> str 918 """ 919 Return the build number of the current distribution. 920 921 For details, see :func:`distro.build_number`. 922 """ 923 return self.version_parts(best)[2] 924 925 def like(self): 926 # type: () -> str 927 """ 928 Return the IDs of distributions that are like the OS distribution. 929 930 For details, see :func:`distro.like`. 931 """ 932 return self.os_release_attr("id_like") or "" 933 934 def codename(self): 935 # type: () -> str 936 """ 937 Return the codename of the OS distribution. 938 939 For details, see :func:`distro.codename`. 940 """ 941 try: 942 # Handle os_release specially since distros might purposefully set 943 # this to empty string to have no codename 944 return self._os_release_info["codename"] 945 except KeyError: 946 return ( 947 self.lsb_release_attr("codename") 948 or self.distro_release_attr("codename") 949 or "" 950 ) 951 952 def info(self, pretty=False, best=False): 953 # type: (bool, bool) -> InfoDict 954 """ 955 Return certain machine-readable information about the OS 956 distribution. 957 958 For details, see :func:`distro.info`. 959 """ 960 return dict( 961 id=self.id(), 962 version=self.version(pretty, best), 963 version_parts=dict( 964 major=self.major_version(best), 965 minor=self.minor_version(best), 966 build_number=self.build_number(best), 967 ), 968 like=self.like(), 969 codename=self.codename(), 970 ) 971 972 def os_release_info(self): 973 # type: () -> Dict[str, str] 974 """ 975 Return a dictionary containing key-value pairs for the information 976 items from the os-release file data source of the OS distribution. 977 978 For details, see :func:`distro.os_release_info`. 979 """ 980 return self._os_release_info 981 982 def lsb_release_info(self): 983 # type: () -> Dict[str, str] 984 """ 985 Return a dictionary containing key-value pairs for the information 986 items from the lsb_release command data source of the OS 987 distribution. 988 989 For details, see :func:`distro.lsb_release_info`. 990 """ 991 return self._lsb_release_info 992 993 def distro_release_info(self): 994 # type: () -> Dict[str, str] 995 """ 996 Return a dictionary containing key-value pairs for the information 997 items from the distro release file data source of the OS 998 distribution. 999 1000 For details, see :func:`distro.distro_release_info`. 1001 """ 1002 return self._distro_release_info 1003 1004 def uname_info(self): 1005 # type: () -> Dict[str, str] 1006 """ 1007 Return a dictionary containing key-value pairs for the information 1008 items from the uname command data source of the OS distribution. 1009 1010 For details, see :func:`distro.uname_info`. 1011 """ 1012 return self._uname_info 1013 1014 def os_release_attr(self, attribute): 1015 # type: (str) -> str 1016 """ 1017 Return a single named information item from the os-release file data 1018 source of the OS distribution. 1019 1020 For details, see :func:`distro.os_release_attr`. 1021 """ 1022 return self._os_release_info.get(attribute, "") 1023 1024 def lsb_release_attr(self, attribute): 1025 # type: (str) -> str 1026 """ 1027 Return a single named information item from the lsb_release command 1028 output data source of the OS distribution. 1029 1030 For details, see :func:`distro.lsb_release_attr`. 1031 """ 1032 return self._lsb_release_info.get(attribute, "") 1033 1034 def distro_release_attr(self, attribute): 1035 # type: (str) -> str 1036 """ 1037 Return a single named information item from the distro release file 1038 data source of the OS distribution. 1039 1040 For details, see :func:`distro.distro_release_attr`. 1041 """ 1042 return self._distro_release_info.get(attribute, "") 1043 1044 def uname_attr(self, attribute): 1045 # type: (str) -> str 1046 """ 1047 Return a single named information item from the uname command 1048 output data source of the OS distribution. 1049 1050 For details, see :func:`distro.uname_attr`. 1051 """ 1052 return self._uname_info.get(attribute, "") 1053 1054 @cached_property 1055 def _os_release_info(self): 1056 # type: () -> Dict[str, str] 1057 """ 1058 Get the information items from the specified os-release file. 1059 1060 Returns: 1061 A dictionary containing all information items. 1062 """ 1063 if os.path.isfile(self.os_release_file): 1064 with open(self.os_release_file) as release_file: 1065 return self._parse_os_release_content(release_file) 1066 return {} 1067 1068 @staticmethod 1069 def _parse_os_release_content(lines): 1070 # type: (TextIO) -> Dict[str, str] 1071 """ 1072 Parse the lines of an os-release file. 1073 1074 Parameters: 1075 1076 * lines: Iterable through the lines in the os-release file. 1077 Each line must be a unicode string or a UTF-8 encoded byte 1078 string. 1079 1080 Returns: 1081 A dictionary containing all information items. 1082 """ 1083 props = {} 1084 lexer = shlex.shlex(lines, posix=True) 1085 lexer.whitespace_split = True 1086 1087 # The shlex module defines its `wordchars` variable using literals, 1088 # making it dependent on the encoding of the Python source file. 1089 # In Python 2.6 and 2.7, the shlex source file is encoded in 1090 # 'iso-8859-1', and the `wordchars` variable is defined as a byte 1091 # string. This causes a UnicodeDecodeError to be raised when the 1092 # parsed content is a unicode object. The following fix resolves that 1093 # (... but it should be fixed in shlex...): 1094 if sys.version_info[0] == 2 and isinstance(lexer.wordchars, bytes): 1095 lexer.wordchars = lexer.wordchars.decode("iso-8859-1") 1096 1097 tokens = list(lexer) 1098 for token in tokens: 1099 # At this point, all shell-like parsing has been done (i.e. 1100 # comments processed, quotes and backslash escape sequences 1101 # processed, multi-line values assembled, trailing newlines 1102 # stripped, etc.), so the tokens are now either: 1103 # * variable assignments: var=value 1104 # * commands or their arguments (not allowed in os-release) 1105 if "=" in token: 1106 k, v = token.split("=", 1) 1107 props[k.lower()] = v 1108 else: 1109 # Ignore any tokens that are not variable assignments 1110 pass 1111 1112 if "version_codename" in props: 1113 # os-release added a version_codename field. Use that in 1114 # preference to anything else Note that some distros purposefully 1115 # do not have code names. They should be setting 1116 # version_codename="" 1117 props["codename"] = props["version_codename"] 1118 elif "ubuntu_codename" in props: 1119 # Same as above but a non-standard field name used on older Ubuntus 1120 props["codename"] = props["ubuntu_codename"] 1121 elif "version" in props: 1122 # If there is no version_codename, parse it from the version 1123 match = re.search(r"(\(\D+\))|,(\s+)?\D+", props["version"]) 1124 if match: 1125 codename = match.group() 1126 codename = codename.strip("()") 1127 codename = codename.strip(",") 1128 codename = codename.strip() 1129 # codename appears within paranthese. 1130 props["codename"] = codename 1131 1132 return props 1133 1134 @cached_property 1135 def _lsb_release_info(self): 1136 # type: () -> Dict[str, str] 1137 """ 1138 Get the information items from the lsb_release command output. 1139 1140 Returns: 1141 A dictionary containing all information items. 1142 """ 1143 if not self.include_lsb: 1144 return {} 1145 with open(os.devnull, "wb") as devnull: 1146 try: 1147 cmd = ("lsb_release", "-a") 1148 stdout = subprocess.check_output(cmd, stderr=devnull) 1149 # Command not found or lsb_release returned error 1150 except (OSError, subprocess.CalledProcessError): 1151 return {} 1152 content = self._to_str(stdout).splitlines() 1153 return self._parse_lsb_release_content(content) 1154 1155 @staticmethod 1156 def _parse_lsb_release_content(lines): 1157 # type: (Iterable[str]) -> Dict[str, str] 1158 """ 1159 Parse the output of the lsb_release command. 1160 1161 Parameters: 1162 1163 * lines: Iterable through the lines of the lsb_release output. 1164 Each line must be a unicode string or a UTF-8 encoded byte 1165 string. 1166 1167 Returns: 1168 A dictionary containing all information items. 1169 """ 1170 props = {} 1171 for line in lines: 1172 kv = line.strip("\n").split(":", 1) 1173 if len(kv) != 2: 1174 # Ignore lines without colon. 1175 continue 1176 k, v = kv 1177 props.update({k.replace(" ", "_").lower(): v.strip()}) 1178 return props 1179 1180 @cached_property 1181 def _uname_info(self): 1182 # type: () -> Dict[str, str] 1183 with open(os.devnull, "wb") as devnull: 1184 try: 1185 cmd = ("uname", "-rs") 1186 stdout = subprocess.check_output(cmd, stderr=devnull) 1187 except OSError: 1188 return {} 1189 content = self._to_str(stdout).splitlines() 1190 return self._parse_uname_content(content) 1191 1192 @staticmethod 1193 def _parse_uname_content(lines): 1194 # type: (Sequence[str]) -> Dict[str, str] 1195 props = {} 1196 match = re.search(r"^([^\s]+)\s+([\d\.]+)", lines[0].strip()) 1197 if match: 1198 name, version = match.groups() 1199 1200 # This is to prevent the Linux kernel version from 1201 # appearing as the 'best' version on otherwise 1202 # identifiable distributions. 1203 if name == "Linux": 1204 return {} 1205 props["id"] = name.lower() 1206 props["name"] = name 1207 props["release"] = version 1208 return props 1209 1210 @staticmethod 1211 def _to_str(text): 1212 # type: (Union[bytes, str]) -> str 1213 encoding = sys.getfilesystemencoding() 1214 encoding = "utf-8" if encoding == "ascii" else encoding 1215 1216 if sys.version_info[0] >= 3: 1217 if isinstance(text, bytes): 1218 return text.decode(encoding) 1219 else: 1220 if isinstance(text, unicode): # noqa 1221 return text.encode(encoding) 1222 1223 return text 1224 1225 @cached_property 1226 def _distro_release_info(self): 1227 # type: () -> Dict[str, str] 1228 """ 1229 Get the information items from the specified distro release file. 1230 1231 Returns: 1232 A dictionary containing all information items. 1233 """ 1234 if self.distro_release_file: 1235 # If it was specified, we use it and parse what we can, even if 1236 # its file name or content does not match the expected pattern. 1237 distro_info = self._parse_distro_release_file(self.distro_release_file) 1238 basename = os.path.basename(self.distro_release_file) 1239 # The file name pattern for user-specified distro release files 1240 # is somewhat more tolerant (compared to when searching for the 1241 # file), because we want to use what was specified as best as 1242 # possible. 1243 match = _DISTRO_RELEASE_BASENAME_PATTERN.match(basename) 1244 if "name" in distro_info and "cloudlinux" in distro_info["name"].lower(): 1245 distro_info["id"] = "cloudlinux" 1246 elif match: 1247 distro_info["id"] = match.group(1) 1248 return distro_info 1249 else: 1250 try: 1251 basenames = os.listdir(self.etc_dir) 1252 # We sort for repeatability in cases where there are multiple 1253 # distro specific files; e.g. CentOS, Oracle, Enterprise all 1254 # containing `redhat-release` on top of their own. 1255 basenames.sort() 1256 except OSError: 1257 # This may occur when /etc is not readable but we can't be 1258 # sure about the *-release files. Check common entries of 1259 # /etc for information. If they turn out to not be there the 1260 # error is handled in `_parse_distro_release_file()`. 1261 basenames = [ 1262 "SuSE-release", 1263 "arch-release", 1264 "base-release", 1265 "centos-release", 1266 "fedora-release", 1267 "gentoo-release", 1268 "mageia-release", 1269 "mandrake-release", 1270 "mandriva-release", 1271 "mandrivalinux-release", 1272 "manjaro-release", 1273 "oracle-release", 1274 "redhat-release", 1275 "sl-release", 1276 "slackware-version", 1277 ] 1278 for basename in basenames: 1279 if basename in _DISTRO_RELEASE_IGNORE_BASENAMES: 1280 continue 1281 match = _DISTRO_RELEASE_BASENAME_PATTERN.match(basename) 1282 if match: 1283 filepath = os.path.join(self.etc_dir, basename) 1284 distro_info = self._parse_distro_release_file(filepath) 1285 if "name" in distro_info: 1286 # The name is always present if the pattern matches 1287 self.distro_release_file = filepath 1288 distro_info["id"] = match.group(1) 1289 if "cloudlinux" in distro_info["name"].lower(): 1290 distro_info["id"] = "cloudlinux" 1291 return distro_info 1292 return {} 1293 1294 def _parse_distro_release_file(self, filepath): 1295 # type: (str) -> Dict[str, str] 1296 """ 1297 Parse a distro release file. 1298 1299 Parameters: 1300 1301 * filepath: Path name of the distro release file. 1302 1303 Returns: 1304 A dictionary containing all information items. 1305 """ 1306 try: 1307 with open(filepath) as fp: 1308 # Only parse the first line. For instance, on SLES there 1309 # are multiple lines. We don't want them... 1310 return self._parse_distro_release_content(fp.readline()) 1311 except (OSError, IOError): 1312 # Ignore not being able to read a specific, seemingly version 1313 # related file. 1314 # See https://github.com/python-distro/distro/issues/162 1315 return {} 1316 1317 @staticmethod 1318 def _parse_distro_release_content(line): 1319 # type: (str) -> Dict[str, str] 1320 """ 1321 Parse a line from a distro release file. 1322 1323 Parameters: 1324 * line: Line from the distro release file. Must be a unicode string 1325 or a UTF-8 encoded byte string. 1326 1327 Returns: 1328 A dictionary containing all information items. 1329 """ 1330 matches = _DISTRO_RELEASE_CONTENT_REVERSED_PATTERN.match(line.strip()[::-1]) 1331 distro_info = {} 1332 if matches: 1333 # regexp ensures non-None 1334 distro_info["name"] = matches.group(3)[::-1] 1335 if matches.group(2): 1336 distro_info["version_id"] = matches.group(2)[::-1] 1337 if matches.group(1): 1338 distro_info["codename"] = matches.group(1)[::-1] 1339 elif line: 1340 distro_info["name"] = line.strip() 1341 return distro_info 1342 1343 1344_distro = LinuxDistribution() 1345 1346 1347def main(): 1348 # type: () -> None 1349 logger = logging.getLogger(__name__) 1350 logger.setLevel(logging.DEBUG) 1351 logger.addHandler(logging.StreamHandler(sys.stdout)) 1352 1353 parser = argparse.ArgumentParser(description="OS distro info tool") 1354 parser.add_argument( 1355 "--json", "-j", help="Output in machine readable format", action="store_true" 1356 ) 1357 1358 parser.add_argument( 1359 "--root-dir", 1360 "-r", 1361 type=str, 1362 dest="root_dir", 1363 help="Path to the root filesystem directory (defaults to /)", 1364 ) 1365 1366 args = parser.parse_args() 1367 1368 if args.root_dir: 1369 dist = LinuxDistribution( 1370 include_lsb=False, include_uname=False, root_dir=args.root_dir 1371 ) 1372 else: 1373 dist = _distro 1374 1375 if args.json: 1376 logger.info(json.dumps(dist.info(), indent=4, sort_keys=True)) 1377 else: 1378 logger.info("Name: %s", dist.name(pretty=True)) 1379 distribution_version = dist.version(pretty=True) 1380 logger.info("Version: %s", distribution_version) 1381 distribution_codename = dist.codename() 1382 logger.info("Codename: %s", distribution_codename) 1383 1384 1385if __name__ == "__main__": 1386 main() 1387