This new API offers a way to precisely detect not only the vendor OS, but also specifics such as the version and release of the currently running distro.
Probes for new distributions are quite simple to create. That and the basic usage of the API is properly documented. Signed-off-by: Cleber Rosa <[email protected]> --- client/shared/distro.py | 338 +++++++++++++++++++++++ client/shared/distro_unittest.py | 96 +++++++ documentation/source/client/distro_detection.rst | 148 ++++++++++ documentation/source/client/index.rst | 2 + 4 files changed, 584 insertions(+) create mode 100644 client/shared/distro.py create mode 100755 client/shared/distro_unittest.py create mode 100644 documentation/source/client/distro_detection.rst diff --git a/client/shared/distro.py b/client/shared/distro.py new file mode 100644 index 0000000..b4d0e38 --- /dev/null +++ b/client/shared/distro.py @@ -0,0 +1,338 @@ +""" +This module provides the client facilities to detect the Linux Distribution +it's running under. + +This is a replacement for the get_os_vendor() function from the utils module. +""" + +import os +import re + + +__all__ = ['LinuxDistro', + 'UNKNOWN_DISTRO_NAME', + 'UNKNOWN_DISTRO_VERSION', + 'UNKNOWN_DISTRO_RELEASE', + 'UNKNOWN_DISTRO_ARCH' + 'Probe', + 'register_probe', + 'detect'] + + +# pylint: disable=R0903 +class LinuxDistro(object): + ''' + Simple collection of information for a Linux Distribution + ''' + def __init__(self, name, version, release, arch): + ''' + Initializes a new Linux Distro + + :param name: a short name that precisely distinguishes this Linux + Distribution among all others. + :type name: str + :param version: the major version of the distribution. Usually this + is a single number that denotes a large development + cycle and support file. + :type version: str + :param release: the release or minor version of the distribution. + Usually this is also a single number, that is often + omitted or starts with a 0 when the major version + is initially release. It's ofter associated with a + shorter development cycle that contains incremental + a collection of improvements and fixes. + :type release: str + :param arch: the main target for this Linux Distribution. It's common + for some architectures to ship with packages for + previous and still compatible architectures, such as it's + the case with Intel/AMD 64 bit architecture that support + 32 bit code. In cases like this, this should be set to + the 64 bit architecture name. + :type arch: str + ''' + self.name = name + self.version = version + self.release = release + self.arch = arch + + + def __repr__(self): + return '<LinuxDistro: name=%s, version=%s, release=%s, arch=%s>' % ( + self.name, self.version, self.release, self.arch) + + +UNKNOWN_DISTRO_NAME = 'unknown' +UNKNOWN_DISTRO_VERSION = 0 +UNKNOWN_DISTRO_RELEASE = 0 +UNKNOWN_DISTRO_ARCH = 'unknown' + + +#: The distribution that is used when the exact one could not be found +UNKNOWN_DISTRO = LinuxDistro(UNKNOWN_DISTRO_NAME, + UNKNOWN_DISTRO_VERSION, + UNKNOWN_DISTRO_RELEASE, + UNKNOWN_DISTRO_ARCH) + + +class Probe(object): + ''' + Probes the machine and does it best to confirm it's the right distro + ''' + #: Points to a file that can determine if this machine is running a given + #: Linux Distribution. This servers a first check that enables the extra + #: checks to carry on. + CHECK_FILE = None + + #: Sets the content that should be checked on the file pointed to by + #: :attr:`CHECK_FILE_EXISTS`. Leave it set to `None` (its default) + #: to check only if the file exists, and not check its contents + CHECK_FILE_CONTAINS = None + + #: The name of the Linux Distribution to be returned if the file defined + #: by :attr:`CHECK_FILE_EXISTS` exist. + CHECK_FILE_DISTRO_NAME = None + + #: A regular expresion that will be run on the file pointed to by + #: :attr:`CHECK_FILE_EXISTS` + CHECK_VERSION_REGEX = None + + + def __init__(self): + self.score = 0 + + + def check_name_for_file(self): + ''' + Checks if this class will look for a file and return a distro + + The conditions that must be true include the file that identifies the + distro file being set (:attr:`CHECK_FILE`) and the name of the + distro to be returned (:attr:`CHECK_FILE_DISTRO_NAME`) + ''' + if self.CHECK_FILE is None: + return False + + if self.CHECK_FILE_DISTRO_NAME is None: + return False + + return True + + + def name_for_file(self): + ''' + Get the distro name if the :attr:`CHECK_FILE` is set and exists + ''' + if self.check_name_for_file(): + if os.path.exists(self.CHECK_FILE): + return self.CHECK_FILE_DISTRO_NAME + + + def check_name_for_file_contains(self): + ''' + Checks if this class will look for text on a file and return a distro + + The conditions that must be true include the file that identifies the + distro file being set (:attr:`CHECK_FILE`), the text to look for + inside the distro file (:attr:`CHECK_FILE_CONTAINS`) and the name + of the distro to be returned (:attr:`CHECK_FILE_DISTRO_NAME`) + ''' + if self.CHECK_FILE is None: + return False + + if self.CHECK_FILE_CONTAINS is None: + return False + + if self.CHECK_FILE_DISTRO_NAME is None: + return False + + return True + + + def name_for_file_contains(self): + ''' + Get the distro if the :attr:`CHECK_FILE` is set and has content + ''' + if self.check_name_for_file_contains(): + if os.path.exists(self.CHECK_FILE): + for line in open(self.CHECK_FILE).readlines(): + if self.CHECK_FILE_CONTAINS in line: + return self.CHECK_FILE_DISTRO_NAME + + + def check_version(self): + ''' + Checks if this class will look for a regex in file and return a distro + ''' + if self.CHECK_FILE is None: + return False + + if self.CHECK_VERSION_REGEX is None: + return False + + return True + + + def _get_version_match(self): + ''' + Returns the match result for the version regex on the file content + ''' + if self.check_version(): + if os.path.exists(self.CHECK_FILE): + version_file_content = open(self.CHECK_FILE).read() + else: + return None + + return self.CHECK_VERSION_REGEX.match(version_file_content) + + + def version(self): + ''' + Returns the version of the distro + ''' + version = UNKNOWN_DISTRO_VERSION + match = self._get_version_match() + if match is not None: + if match.groups() > 0: + version = match.groups()[0] + return version + + + def check_release(self): + ''' + Checks if this has the conditions met to look for the release number + ''' + return (self.check_version() and + self.CHECK_VERSION_REGEX.groups > 1) + + + def release(self): + ''' + Returns the release of the distro + ''' + release = UNKNOWN_DISTRO_RELEASE + match = self._get_version_match() + if match is not None: + if match.groups() > 1: + release = match.groups()[1] + return release + + + def get_distro(self): + ''' + Returns the :class:`LinuxDistro` this probe detected + ''' + name = None + version = UNKNOWN_DISTRO_VERSION + release = UNKNOWN_DISTRO_RELEASE + arch = UNKNOWN_DISTRO_ARCH + + distro = None + + if self.check_name_for_file(): + name = self.name_for_file() + self.score += 1 + + if self.check_name_for_file_contains(): + name = self.name_for_file_contains() + self.score += 1 + + if self.check_version(): + version = self.version() + self.score += 1 + + if self.check_release(): + release = self.release() + self.score += 1 + + # can't think of a better way to do this + arch = os.uname()[4] + + # name is the first thing that should be identified. If we don't know + # the distro name, we don't bother checking for versions + if name is not None: + distro = LinuxDistro(name, version, release, arch) + else: + distro = UNKNOWN_DISTRO + + return distro + + +class RedHatProbe(Probe): + ''' + Probe with version checks for Red Hat Enterprise Linux systems + ''' + CHECK_FILE = '/etc/redhat-release' + CHECK_FILE_CONTAINS = 'Red Hat' + CHECK_FILE_DISTRO_NAME = 'redhat' + CHECK_VERSION_REGEX = re.compile( + r'Red Hat Enterprise Linux Server release (\d{1,2})\.(\d{1,2}).*') + + +class CentosProbe(RedHatProbe): + ''' + Probe with version checks for CentOS systems + ''' + CHECK_FILE = '/etc/redhat-release' + CHECK_FILE_CONTAINS = 'CentOS' + CHECK_FILE_DISTRO_NAME = 'centos' + CHECK_VERSION_REGEX = re.compile(r'CentOS release (\d{1,2})\.(\d{1,2}).*') + + +class FedoraProbe(RedHatProbe): + ''' + Probe with version checks for Fedora systems + ''' + CHECK_FILE = '/etc/fedora-release' + CHECK_FILE_CONTAINS = 'Fedora' + CHECK_FILE_DISTRO_NAME = 'fedora' + CHECK_VERSION_REGEX = re.compile(r'Fedora release (\d{1,2}).*') + + +class DebianProbe(Probe): + ''' + Simple probe with file checks for Debian systems + ''' + CHECK_FILE = '/etc/debian-version' + CHECK_FILE_DISTRO_NAME = 'debian' + + +#: the complete list of probes that have been registered +REGISTERED_PROBES = [] + + +def register_probe(probe_class): + ''' + Register a probe to be run during autodetection + ''' + if probe_class not in REGISTERED_PROBES: + REGISTERED_PROBES.append(probe_class) + + +register_probe(RedHatProbe) +register_probe(CentosProbe) +register_probe(FedoraProbe) +register_probe(DebianProbe) + + +def detect(): + ''' + Attempts to detect the Linux Distribution running on this machine + + :returns: the detected :class:`LinuxDistro` or :data:`UNKNOWN_DISTRO` + :rtype: :class:`LinuxDistro` + ''' + results = [] + + for probe_class in REGISTERED_PROBES: + probe_instance = probe_class() + distro_result = probe_instance.get_distro() + if distro_result is not UNKNOWN_DISTRO: + results.append((distro_result, probe_instance)) + + results.sort(key=lambda t: t[1].score) + if len(results) > 0: + distro = results[-1][0] + else: + distro = UNKNOWN_DISTRO + + return distro diff --git a/client/shared/distro_unittest.py b/client/shared/distro_unittest.py new file mode 100755 index 0000000..2398156 --- /dev/null +++ b/client/shared/distro_unittest.py @@ -0,0 +1,96 @@ +#!/usr/bin/python + +import os +import re +import unittest +try: + import autotest.common as common +except ImportError: + import common + +from autotest.client.shared import distro +from autotest.client.shared.test_utils import mock + + +class Probe(unittest.TestCase): + def setUp(self): + self.god = mock.mock_god() + + + def tearDown(self): + self.god.unstub_all() + + + def test_check_name_for_file_fail(self): + class MyProbe(distro.Probe): + CHECK_FILE = '/etc/issue' + + my_probe = MyProbe() + self.assertFalse(my_probe.check_name_for_file()) + + + def test_check_name_for_file(self): + class MyProbe(distro.Probe): + CHECK_FILE = '/etc/issue' + CHECK_FILE_DISTRO_NAME = 'superdistro' + + my_probe = MyProbe() + self.assertTrue(my_probe.check_name_for_file()) + + + def test_check_name_for_file_contains_fail(self): + class MyProbe(distro.Probe): + CHECK_FILE = '/etc/issue' + CHECK_FILE_CONTAINS = 'text' + + my_probe = MyProbe() + self.assertFalse(my_probe.check_name_for_file_contains()) + + + def test_check_name_for_file_contains(self): + class MyProbe(distro.Probe): + CHECK_FILE = '/etc/issue' + CHECK_FILE_CONTAINS = 'text' + CHECK_FILE_DISTRO_NAME = 'superdistro' + + my_probe = MyProbe() + self.assertTrue(my_probe.check_name_for_file_contains()) + + + def test_check_version_fail(self): + class MyProbe(distro.Probe): + CHECK_VERSION_REGEX = re.compile(r'distro version (\d+)') + + my_probe = MyProbe() + self.assertFalse(my_probe.check_version()) + + + def test_version_returnable(self): + class MyProbe(distro.Probe): + CHECK_FILE = '/etc/distro-release' + CHECK_VERSION_REGEX = re.compile(r'distro version (\d+)') + + my_probe = MyProbe() + self.assertTrue(my_probe.check_version()) + + + def test_name_for_file(self): + distro_file = '/etc/issue' + distro_name = 'superdistro' + + self.god.stub_function(os.path, 'exists') + os.path.exists.expect_call(distro_file).and_return(True) + + class MyProbe(distro.Probe): + CHECK_FILE = distro_file + CHECK_FILE_DISTRO_NAME = distro_name + + my_probe = MyProbe() + probed_distro_name = my_probe.name_for_file() + + self.god.check_playback() + self.assertEqual(distro_name, probed_distro_name) + + +if __name__ == '__main__': + unittest.main() diff --git a/documentation/source/client/distro_detection.rst b/documentation/source/client/distro_detection.rst new file mode 100644 index 0000000..c74f101 --- /dev/null +++ b/documentation/source/client/distro_detection.rst @@ -0,0 +1,148 @@ +============================== + Linux Distribution Detection +============================== + +.. module:: autotest.client.shared.distro + +Autotest has a facility that lets tests determine quite precisely the +distribution they're running on. + +This is done through the implementation and registration of probe classes. + +Those probe classes can check for given characteristics of the running +operating system, such as the existence of a release file, its contents +or even the existence of a binary that is exclusive to a distribution +(such as package managers). + +Quickly detecting the Linux Distribution +======================================== + +The :mod:`autotest.client.shared.distro` module provides many APIs, but +the simplest one to use is the :func:`detect`. + +Its usage is quite straighforward:: + + >>> from autotest.client.shared import distro + >>> detected_distro = distro.detect() + +The returned distro can be the result of a probe validating the distribution +detection, or the not so useful :data:`UNKNOWN_DISTRO`. + +To access the relevant data on a :class:`LinuxDistro`, simply use the +attributes: + +* :attr:`name <LinuxDistro.name>` +* :attr:`version <LinuxDistro.version>` +* :attr:`release <LinuxDistro.release>` +* :attr:`arch <LinuxDistro.arch>` + +Example:: + + >>> detected_distro = distro.detect() + >>> print detected_distro.name + redhat + + +The unknown Linux Distribution +============================== + +When the detection mechanism can't precily detect the Linux Distribution, it +will still return a :class:`LinuxDistro` instance, but a special one that +contains special values for its name, version, etc. + +.. autodata:: UNKNOWN_DISTRO + +Writing a Linux Distribution Probe +================================== + +The easiest way to write a probe for your target Linux Distribution is to make +use of the features of the :class:`Probe` class. + +Even if you do plan to use the features documented here, keep in mind that all +probes should inherit from :class:`Probe` and provide a basic interface. + +Checking the Distrution name only +--------------------------------- + +The most trivial probe is one that checks the existence of a file and returns +the distribution name:: + + class RedHatProbe(Probe): + CHECK_FILE = '/etc/redhat-release' + CHECK_FILE_DISTRO_NAME = 'redhat' + +To make use of a probe, it's necessary to register it:: + + >>> from autotest.client.shared import distro + >>> distro.register_probe(RedHatProbe) + +And that's it. This is a valid example, but will give you nothing but the +distro name. + +You should usually aim for more information, such as the version numbers. + +Checking the Distribution name and version numbers +-------------------------------------------------- + +If you want to also detect the distro version numbers (and you should), then +it's possible to use the :attr:`Probe.CHECK_VERSION_REGEX` feature +of the :class:`Probe` class. + +.. autodata:: Probe.CHECK_VERSION_REGEX + +If your regex has two or more groups, that is, it will look for and save +references to two or more string, it will consider the second group to be +the :attr:`LinuxDistro.release` number. + +Probe Scores +------------ + +To increase the accuracy of the probe results, it's possible to register a +score for a probe. If a probe wants to, it can register a score for itself. + +Probes that return a score will be given priority over probes that don't. + +The score should be based on the number of checks that ran during the probe +to account for its accuracy. + +Probes should not be given a higher score because their checks look more +precise than everyone else's. + +Registering your own probes +--------------------------- + +Not only the probes that ship with autotest can be used, but your custom probe +classes can be added to the detection system. + +To do that simply call the function :func:`register_probe`: + +.. autofunction:: register_probe + +Now, remember that for things to happen smootlhy your registered probe must be +a subclass of :class:`Probe`. + + +API Reference +============= + +:class:`LinuxDistro` +-------------------- + +.. autoclass:: LinuxDistro + :members: + +:class:`Probe` +-------------- + +.. autoclass:: Probe + :members: + +:func:`register_probe` +---------------------- + +.. autofunction:: register_probe + +:func:`detect` +-------------- + +.. autofunction:: detect diff --git a/documentation/source/client/index.rst b/documentation/source/client/index.rst index 561c316..f0d8a9c 100644 --- a/documentation/source/client/index.rst +++ b/documentation/source/client/index.rst @@ -8,3 +8,5 @@ Contents: .. toctree:: :maxdepth: 2 + + distro_detection -- 1.7.11.7 _______________________________________________ Autotest-kernel mailing list [email protected] https://www.redhat.com/mailman/listinfo/autotest-kernel
