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