commit b8528da5a8e8cf4fdeabb77022cb511043544e9f Author: Christian Heimes Date: Wed Aug 29 12:43:03 2018 +0200 Refactor os-release and platform information Move the /etc/os-release parser and platform detection code out of the private _importhook module. The ipaplatform module now contains an osinfo module that provides distribution, os, and vendor information. See: https://www.freedesktop.org/software/systemd/man/os-release.html See: https://pagure.io/freeipa/issue/7661 Signed-off-by: Christian Heimes Reviewed-By: Rob Crittenden diff --git a/ipaplatform/__init__.py b/ipaplatform/__init__.py index 06397fda1..1c22346b0 100644 --- a/ipaplatform/__init__.py +++ b/ipaplatform/__init__.py @@ -8,4 +8,4 @@ ignore. """ __import__('pkg_resources').declare_namespace(__name__) -NAME = None # initialized by IpaMetaImporter +NAME = None # initialized by ipaplatform.osinfo diff --git a/ipaplatform/_importhook.py b/ipaplatform/_importhook.py index 77c4e0d87..3f84e81fd 100644 --- a/ipaplatform/_importhook.py +++ b/ipaplatform/_importhook.py @@ -3,46 +3,14 @@ # from __future__ import absolute_import -"""Meta import hook for ipaplatform. - -Known Linux distros with /etc/os-release ----------------------------------------- - -- alpine -- centos (like rhel, fedora) -- debian -- fedora -- rhel -- ubuntu (like debian) -""" import importlib -import io -import re import sys -import warnings - - -import ipaplatform -try: - from ipaplatform.override import OVERRIDE -except ImportError: - OVERRIDE = None - -_osrelease_line = re.compile( - u"^(?!#)(?P[a-zA-Z0-9_]+)=" - u"(?P[\"\']?)(?P.+)(?P=quote)$" -) +from ipaplatform.osinfo import osinfo class IpaMetaImporter(object): - """Meta import hook and platform detector. - - The meta import hook uses /etc/os-release to auto-detects the best - matching ipaplatform provider. It is compatible with external namespace - packages, too. - """ modules = { 'ipaplatform.constants', 'ipaplatform.paths', @@ -50,80 +18,8 @@ class IpaMetaImporter(object): 'ipaplatform.tasks' } - bsd_family = ( - 'freebsd', - 'openbsd', - 'netbsd', - 'dragonfly', - 'gnukfreebsd' - ) - - def __init__(self, override=OVERRIDE): - self.override = override - self.platform_ids = self._get_platform_ids(self.override) - self.platform = self._get_platform(self.platform_ids) - - def _get_platform_ids(self, override): - platforms = [] - # allow RPM and Debian packages to override platform - if override is not None: - platforms.append(override) - - if sys.platform.startswith('linux'): - # Linux, get distribution from /etc/os-release - try: - platforms.extend(self._parse_platform()) - except Exception as e: - warnings.warn("Failed to read /etc/os-release: {}".format(e)) - elif sys.platform == 'win32': - # Windows 32 or 64bit platform - platforms.append('win32') - elif sys.platform == 'darwin': - # macOS - platforms.append('macos') - elif sys.platform.startswith(self.bsd_family): - # BSD family, look for e.g. ['freebsd10', 'freebsd'] - platforms.append(sys.platform) - simple = sys.platform.rstrip('0123456789') - if simple != sys.platform: - platforms.append(simple) - - if not platforms: - raise ValueError("Unsupported platform: {}".format(sys.platform)) - - return platforms - - def parse_osrelease(self, filename='/etc/os-release'): - release = {} - with io.open(filename, encoding='utf-8') as f: - for line in f: - mo = _osrelease_line.match(line) - if mo is not None: - release[mo.group('name')] = mo.group('value') - return release - - def _parse_platform(self, filename='/etc/os-release'): - release = self.parse_osrelease(filename) - platforms = [ - release['ID'], - ] - if "ID_LIKE" in release: - platforms.extend( - v.strip() for v in release['ID_LIKE'].split(' ') if v.strip() - ) - - return platforms - - def _get_platform(self, platform_ids): - for platform in platform_ids: - try: - importlib.import_module('ipaplatform.{}'.format(platform)) - except ImportError: - pass - else: - return platform - raise ImportError('No ipaplatform available for "{}"'.format( - ', '.join(platform_ids))) + def __init__(self, platform): + self.platform = platform def find_module(self, fullname, path=None): """Meta importer hook""" @@ -148,8 +44,7 @@ class IpaMetaImporter(object): return platform_mod -metaimporter = IpaMetaImporter() +metaimporter = IpaMetaImporter(osinfo.platform) sys.meta_path.insert(0, metaimporter) fixup_module = metaimporter.load_module -ipaplatform.NAME = metaimporter.platform diff --git a/ipaplatform/osinfo.py b/ipaplatform/osinfo.py new file mode 100644 index 000000000..a38165d01 --- /dev/null +++ b/ipaplatform/osinfo.py @@ -0,0 +1,214 @@ +# +# Copyright (C) 2018 FreeIPA Contributors see COPYING for license +# +"""Distribution information + +Known Linux distros with /etc/os-release +---------------------------------------- + +- alpine +- centos (like rhel, fedora) +- debian +- fedora +- rhel +- ubuntu (like debian) +""" +from __future__ import absolute_import + +import importlib +import io +import re +import sys +import warnings + +import six + +import ipaplatform +try: + from ipaplatform.override import OVERRIDE +except ImportError: + OVERRIDE = None + + +# pylint: disable=no-name-in-module, import-error +if six.PY3: + from collections.abc import Mapping +else: + from collections import Mapping +# pylint: enable=no-name-in-module, import-error + +_osrelease_line = re.compile( + u"^(?!#)(?P[a-zA-Z0-9_]+)=" + u"(?P[\"\']?)(?P.+)(?P=quote)$" +) + + +def _parse_osrelease(filename='/etc/os-release'): + """Parser for /etc/os-release for Linux distributions + + https://www.freedesktop.org/software/systemd/man/os-release.html + """ + release = {} + with io.open(filename, encoding='utf-8') as f: + for line in f: + mo = _osrelease_line.match(line) + if mo is not None: + release[mo.group('name')] = mo.group('value') + if 'ID_LIKE' in release: + release['ID_LIKE'] = tuple( + v.strip() + for v in release['ID_LIKE'].split(' ') + if v.strip() + ) + else: + release["ID_LIKE"] = () + # defaults + release.setdefault('NAME', 'Linux') + release.setdefault('ID', 'linux') + release.setdefault('VERSION', '') + release.setdefault('VERSION_ID', '') + return release + + +class OSInfo(Mapping): + __slots__ = ('_info', '_platform') + + bsd_family = ( + 'freebsd', + 'openbsd', + 'netbsd', + 'dragonfly', + 'gnukfreebsd' + ) + + def __init__(self): + if sys.platform.startswith('linux'): + # Linux, get distribution from /etc/os-release + info = self._handle_linux() + elif sys.platform == 'win32': + info = self._handle_win32() + elif sys.platform == 'darwin': + info = self._handle_darwin() + elif sys.platform.startswith(self.bsd_family): + info = self._handle_bsd() + else: + raise ValueError("Unsupported platform: {}".format(sys.platform)) + self._info = info + self._platform = None + + def _handle_linux(self): + """Detect Linux distribution from /etc/os-release + """ + try: + return _parse_osrelease() + except Exception as e: + warnings.warn("Failed to read /etc/os-release: {}".format(e)) + return { + 'NAME': 'Linux', + 'ID': 'linux', + } + + def _handle_win32(self): + """Windows 32 or 64bit platform + """ + return { + 'NAME': 'Windows', + 'ID': 'win32', + } + + def _handle_darwin(self): + """Handle macOS / Darwin platform + """ + return { + 'NAME': 'macOS', + 'ID': 'macos', + } + + def _handle_bsd(self): + """Handle BSD-like platforms + """ + platform = sys.platform + simple = platform.rstrip('0123456789') + id_like = [] + if simple != platform: + id_like.append(simple) + return { + 'NAME': platform, + 'ID': platform, + 'ID_LIKE': tuple(id_like), + } + + def __getitem__(self, item): + return self._info[item] + + def __iter__(self): + return iter(self._info) + + def __len__(self): + return len(self._info) + + @property + def name(self): + """OS name (user) + """ + return self._info['NAME'] + + @property + def id(self): + """Lower case OS identifier + """ + return self._info['ID'] + + @property + def id_like(self): + """Related / similar OS + """ + return self._info.get('ID_LIKE', ()) + + @property + def version(self): + """Version number and name of OS (for user) + """ + return self._info.get('VERSION') + + @property + def version_id(self): + """Version identifier + """ + return self._info.get('VERSION_ID') + + @property + def platform_ids(self): + """Ordered tuple of detected platforms (including override) + """ + platforms = [] + if OVERRIDE is not None: + # allow RPM and Debian packages to override platform + platforms.append(OVERRIDE) + if OVERRIDE != self.id: + platforms.append(self.id) + platforms.extend(self.id_like) + return tuple(platforms) + + @property + def platform(self): + if self._platform is not None: + return self._platform + for platform in self.platform_ids: + try: + importlib.import_module('ipaplatform.{}'.format(platform)) + except ImportError: + pass + else: + self._platform = platform + return platform + raise ImportError('No ipaplatform available for "{}"'.format( + ', '.join(self.platform_ids))) + + +osinfo = OSInfo() +ipaplatform.NAME = osinfo.platform + +if __name__ == '__main__': + import pprint + pprint.pprint(dict(osinfo)) diff --git a/ipatests/test_ipaplatform/test_importhook.py b/ipatests/test_ipaplatform/test_importhook.py index c7d2626d6..eeb351ba7 100644 --- a/ipatests/test_ipaplatform/test_importhook.py +++ b/ipatests/test_ipaplatform/test_importhook.py @@ -13,6 +13,7 @@ import ipaplatform.paths import ipaplatform.services import ipaplatform.tasks from ipaplatform._importhook import metaimporter +from ipaplatform.osinfo import osinfo, _parse_osrelease try: from ipaplatform.override import OVERRIDE except ImportError: @@ -26,8 +27,8 @@ DATA = os.path.join(HERE, 'data') @pytest.mark.skipif(OVERRIDE is None, reason='test requires override') def test_override(): - assert OVERRIDE == metaimporter.platform_ids[0] - assert OVERRIDE == metaimporter.platform + assert OVERRIDE == osinfo.platform_ids[0] + assert OVERRIDE == osinfo.platform @pytest.mark.parametrize('mod, name', [ @@ -46,11 +47,12 @@ def test_importhook(mod, name): assert mod.__dict__ == sys.modules[override].__dict__ -@pytest.mark.parametrize('filename, expected_platforms', [ - (os.path.join(DATA, 'os-release-centos'), ['centos', 'rhel', 'fedora']), - (os.path.join(DATA, 'os-release-fedora'), ['fedora']), - (os.path.join(DATA, 'os-release-ubuntu'), ['ubuntu', 'debian']), +@pytest.mark.parametrize('filename, id_, id_like', [ + (os.path.join(DATA, 'os-release-centos'), 'centos', ('rhel', 'fedora')), + (os.path.join(DATA, 'os-release-fedora'), 'fedora', ()), + (os.path.join(DATA, 'os-release-ubuntu'), 'ubuntu', ('debian',)), ]) -def test_parse_os_release(filename, expected_platforms): - parsed = metaimporter._parse_platform(filename) - assert parsed == expected_platforms +def test_parse_os_release(filename, id_, id_like): + parsed = _parse_osrelease(filename) + assert parsed['ID'] == id_ + assert parsed['ID_LIKE'] == id_like diff --git a/ipatests/test_ipapython/test_certdb.py b/ipatests/test_ipapython/test_certdb.py index 42edfb5a9..42d48d51a 100644 --- a/ipatests/test_ipapython/test_certdb.py +++ b/ipatests/test_ipapython/test_certdb.py @@ -5,13 +5,12 @@ import os import pytest from ipapython.certdb import NSSDatabase, TRUSTED_PEER_TRUST_FLAGS -from ipaplatform._importhook import metaimporter +from ipaplatform.osinfo import osinfo -OSRELEASE = metaimporter.parse_osrelease() CERTNICK = 'testcert' -if OSRELEASE['ID'] == 'fedora': - if int(OSRELEASE['VERSION_ID']) >= 28: +if osinfo.id == 'fedora': + if int(osinfo.version_id) >= 28: NSS_DEFAULT = 'sql' else: NSS_DEFAULT = 'dbm'