From d9608357748588b528ec9fcc06513b53c95e9fdc Mon Sep 17 00:00:00 2001 From: Mike Stewart Date: Wed, 8 Aug 2012 18:19:42 -0700 Subject: [PATCH] Add more features to distro detection. * Use lsb_release if available on the remote system * Add support for Amazon Linux detection * Scan through /etc/redhat-release to check for centos/fedora * Detect Debian/Ubuntu family based on /etc/debian_version, as a fallback * Add mocked-up unit tests for some of the distro detection --- patchwork/files.py | 7 ++++ patchwork/info.py | 79 +++++++++++++++++++++++++++++++++++++++++++-- test/test_distro.py | 63 ++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 test/test_distro.py diff --git a/patchwork/files.py b/patchwork/files.py index cb17595..662c953 100644 --- a/patchwork/files.py +++ b/patchwork/files.py @@ -91,3 +91,10 @@ def _escape_for_regex(text): # Whereas single quotes should not be escaped regex = regex.replace(r"\'", "'") return regex + +def cat(path, runner=run): + """ + Cat a file, returning its contents as a string. + """ + with hide('stdout'): + return runner('cat "%(path)s"' % locals()).stdout diff --git a/patchwork/info.py b/patchwork/info.py index 5394f74..ebb5aa1 100644 --- a/patchwork/info.py +++ b/patchwork/info.py @@ -1,5 +1,45 @@ +from collections import namedtuple + +from fabric.api import run +from environment import has_binary from fabric.contrib.files import exists +from files import cat + + +REDHAT_FAMILY_DISTROS = "rhel fedora centos amazon".split() +DEBIAN_FAMILY_DISTROS = "ubuntu debian".split() +# Holder struct for lsb_release info. +lsb_release_info=namedtuple("lsb_release_info", + ["lsb_version", + "distributor", + "description", + "release_version", + "codename"]) + +def lsb_release(): + """ + Get Linux Standard Base distro information, if available. + http://refspecs.linuxbase.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/lsbrelease.html + + Returns None if unavailable, or else a lsb_release_info object with the + following string attributes: + lsb_version + distributor + description + release_version + codename + """ + if not has_binary('lsb_release'): + return None + def lsb_attr(flagname): + return run('lsb_release --short --%s' % flagname).stdout + result = lsb_release_info(lsb_attr('version'), + lsb_attr('id'), + lsb_attr('description'), + lsb_attr('release'), + lsb_attr('codename')) + return result def distro_name(): """ @@ -11,10 +51,23 @@ def distro_name(): * ``fedora`` * ``rhel`` * ``centos`` + * ``amazon`` * ``ubuntu`` * ``debian`` * ``other`` """ + lsb_info = lsb_release() + if lsb_info: + try: + distributor_names = { + 'Ubuntu': 'ubuntu', + 'Debian': 'debian', + 'AmazonAMI': 'amazon', + } + return distributor_names[lsb_info.distributor] + except KeyError: + pass + sentinel_files = { 'fedora': ('fedora-release',), 'centos': ('centos-release',), @@ -23,6 +76,20 @@ def distro_name(): for sentinel in sentinels: if exists('/etc/%s' % sentinel): return name + + # Redhat-like distros erratically include /etc/redhat-release + # instead of distro-specific files. + redhat_release_file = '/etc/redhat-release' + if exists(redhat_release_file): + redhat_release_content = cat(redhat_release_file) + for distro in REDHAT_FAMILY_DISTROS: + if distro in redhat_release_content.lower(): + return distro + + system_release_file = '/etc/system-release' + if exists(system_release_file) and 'Amazon Linux' in cat(system_release_file): + return 'amazon' + return "other" @@ -35,15 +102,21 @@ def distro_family(): * ``debian`` * ``redhat`` - If the system falls outside these categories, its specific family or - release name will be returned instead. + If the system falls outside these categories, its discovered distro_name + will be returned instead (possibly ``other`` if its type couldn't be + determined). """ families = { 'debian': "debian ubuntu".split(), - 'redhat': "rhel centos fedora".split() + 'redhat': "rhel centos fedora amazon".split() } distro = distro_name() for family, members in families.iteritems(): if distro in members: return family + # Even if we haven't been able to determine exact distro, + # we may be able to determine the overall family. + if distro == 'other': + if exists('/etc/debian_version'): + return 'debian' return distro diff --git a/test/test_distro.py b/test/test_distro.py new file mode 100644 index 0000000..b6b6fb1 --- /dev/null +++ b/test/test_distro.py @@ -0,0 +1,63 @@ +from patchwork import info +import unittest +from unittest import TestCase + +import mock +from patchwork.info import lsb_release_info + + +ubuntu1104_lsb_info = lsb_release_info('No LSB modules are available.', + 'Ubuntu', + 'Ubuntu 11.04', + '11.04', + 'natty') +amazon2012_03_lsb_info = lsb_release_info(':core-4.0-amd64:core-4.0-noarch:printing-4.0-amd64:printing-4.0-noarch', + 'AmazonAMI', + 'Amazon Linux AMI release 2012.03', + '2012.03', + 'n/a') + +class DistroNameDetection(TestCase): + @mock.patch('patchwork.info.lsb_release') + @mock.patch('patchwork.info.run') + def test_ubuntu_detection_via_lsb_release(self, run, lsb_release): + lsb_release.return_value = ubuntu1104_lsb_info + self.assertEqual(info.distro_name(), 'ubuntu') + lsb_release.assert_called() + self.assertFalse(run.called) + + @mock.patch('patchwork.info.lsb_release') + @mock.patch('patchwork.info.run') + def test_amazon_detection_via_lsb_release(self, run, lsb_release): + lsb_release.return_value = amazon2012_03_lsb_info + self.assertEqual(info.distro_name(), 'amazon') + lsb_release.assert_called() + self.assertFalse(run.called) + +class DistroFamilyDetection(TestCase): + @mock.patch('patchwork.info.distro_name') + @mock.patch('patchwork.info.run') + def test_debian_family(self, run, distro_name_fxn): + for d in ('ubuntu', 'debian'): + distro_name_fxn.return_value = d + self.assertEqual('debian', info.distro_family()) + self.assertFalse(run.called) + + @mock.patch('patchwork.info.distro_name') + @mock.patch('patchwork.info.run') + def test_redhat_family(self, run, distro_name_fxn): + for d in ('redhat', 'centos', 'fedora', 'amazon'): + distro_name_fxn.return_value = d + self.assertEqual('redhat', info.distro_family()) + self.assertFalse(run.called) + + @mock.patch('patchwork.info.exists') + @mock.patch('patchwork.info.distro_name') + @mock.patch('patchwork.info.run') + def test_family_inference(self, run, distro_name_fxn, file_exists): + """If debian_version exists, then it should be picked up as debian-family, + even if exact type couldn't be worked out.""" + distro_name_fxn.return_value = 'other' + file_exists.side_effect = lambda s: s == '/etc/debian_version' + self.assertEqual('debian', info.distro_family()) +