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 <cheimes@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
This commit is contained in:
Christian Heimes 2018-08-29 12:43:03 +02:00 committed by Tibor Dudlák
parent 8af6accfa5
commit b8528da5a8
No known key found for this signature in database
GPG Key ID: 12B8BD343576CDF5
5 changed files with 233 additions and 123 deletions

View File

@ -8,4 +8,4 @@ ignore.
"""
__import__('pkg_resources').declare_namespace(__name__)
NAME = None # initialized by IpaMetaImporter
NAME = None # initialized by ipaplatform.osinfo

View File

@ -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<name>[a-zA-Z0-9_]+)="
u"(?P<quote>[\"\']?)(?P<value>.+)(?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

214
ipaplatform/osinfo.py Normal file
View File

@ -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<name>[a-zA-Z0-9_]+)="
u"(?P<quote>[\"\']?)(?P<value>.+)(?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))

View File

@ -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

View File

@ -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'