Use distro library for all OS version detection (#7467)

This pull request ensures that we use distro package in all the distribution version detection. It also replaces the custom systemd /etc/os-release parsing and adds a few version fingerprints to Apache override selection.

Fixes: #7405

* Revert "Try to use platform.linux_distribution() before distro equivalent (#7403)"

This reverts commit ca3077d034.

* Use distro for all os detection code

* Address review comments

* Add changelog entry

* Added tests

* Fix tests to return a consistent os name

* Do not crash on non-linux systems

* Minor fixes to distro compatibility checks

* Make the tests OS independent

* Update certbot/util.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Skip linux specific tests on other platforms

* Test fixes

* Better test state handling

* Lower the coverage target for Windows tests
This commit is contained in:
Joona Hoikkala
2019-11-01 10:51:21 -07:00
committed by Brad Warren
co-authored by Brad Warren
parent f8ff881d23
commit fb1aafb5d2
6 changed files with 102 additions and 96 deletions
+1 -1
View File
@@ -13,6 +13,6 @@ coverage:
flags: windows flags: windows
# Fixed target instead of auto set by #7173, can # Fixed target instead of auto set by #7173, can
# be removed when flags in Codecov are added back. # be removed when flags in Codecov are added back.
target: 97.7 target: 97.4
threshold: 0.1 threshold: 0.1
base: auto base: auto
+1
View File
@@ -20,6 +20,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
staging server instead of the live server when `--dry-run` is used. staging server instead of the live server when `--dry-run` is used.
* Updated certbot-dns-google to depend on newer versions of * Updated certbot-dns-google to depend on newer versions of
google-api-python-client and oauth2client. google-api-python-client and oauth2client.
* The OS detection logic again uses distro library for Linux OSes
* certbot.plugins.common.TLSSNI01 has been deprecated and will be removed in a * certbot.plugins.common.TLSSNI01 has been deprecated and will be removed in a
future release. future release.
* CLI flags --tls-sni-01-port and --tls-sni-01-address have been removed. * CLI flags --tls-sni-01-port and --tls-sni-01-address have been removed.
@@ -16,6 +16,7 @@ from certbot_apache import override_suse
OVERRIDE_CLASSES = { OVERRIDE_CLASSES = {
"arch": override_arch.ArchConfigurator, "arch": override_arch.ArchConfigurator,
"cloudlinux": override_centos.CentOSConfigurator,
"darwin": override_darwin.DarwinConfigurator, "darwin": override_darwin.DarwinConfigurator,
"debian": override_debian.DebianConfigurator, "debian": override_debian.DebianConfigurator,
"ubuntu": override_debian.DebianConfigurator, "ubuntu": override_debian.DebianConfigurator,
@@ -23,7 +24,9 @@ OVERRIDE_CLASSES = {
"centos linux": override_centos.CentOSConfigurator, "centos linux": override_centos.CentOSConfigurator,
"fedora_old": override_centos.CentOSConfigurator, "fedora_old": override_centos.CentOSConfigurator,
"fedora": override_fedora.FedoraConfigurator, "fedora": override_fedora.FedoraConfigurator,
"linuxmint": override_debian.DebianConfigurator,
"ol": override_centos.CentOSConfigurator, "ol": override_centos.CentOSConfigurator,
"oracle": override_centos.CentOSConfigurator,
"redhatenterpriseserver": override_centos.CentOSConfigurator, "redhatenterpriseserver": override_centos.CentOSConfigurator,
"red hat enterprise linux server": override_centos.CentOSConfigurator, "red hat enterprise linux server": override_centos.CentOSConfigurator,
"rhel": override_centos.CentOSConfigurator, "rhel": override_centos.CentOSConfigurator,
@@ -32,6 +35,7 @@ OVERRIDE_CLASSES = {
"gentoo base system": override_gentoo.GentooConfigurator, "gentoo base system": override_gentoo.GentooConfigurator,
"opensuse": override_suse.OpenSUSEConfigurator, "opensuse": override_suse.OpenSUSEConfigurator,
"suse": override_suse.OpenSUSEConfigurator, "suse": override_suse.OpenSUSEConfigurator,
"sles": override_suse.OpenSUSEConfigurator,
"scientific": override_centos.CentOSConfigurator, "scientific": override_centos.CentOSConfigurator,
"scientific linux": override_centos.CentOSConfigurator, "scientific linux": override_centos.CentOSConfigurator,
} }
+64 -45
View File
@@ -1,6 +1,7 @@
"""Tests for certbot.util.""" """Tests for certbot.util."""
import argparse import argparse
import errno import errno
import sys
import unittest import unittest
import mock import mock
@@ -473,74 +474,92 @@ class IsWildcardDomainTest(unittest.TestCase):
class OsInfoTest(unittest.TestCase): class OsInfoTest(unittest.TestCase):
"""Test OS / distribution detection""" """Test OS / distribution detection"""
def test_systemd_os_release(self): @mock.patch("certbot.util.distro")
from certbot.util import (get_os_info, get_systemd_os_info, @unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux")
get_os_info_ua) def test_systemd_os_release_like(self, m_distro):
import certbot.util as cbutil
m_distro.like.return_value = "first debian third"
id_likes = cbutil.get_systemd_os_like()
self.assertEqual(len(id_likes), 3)
self.assertTrue("debian" in id_likes)
with mock.patch('certbot.compat.os.path.isfile', return_value=True): @mock.patch("certbot.util.distro")
self.assertEqual(get_os_info( @unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux")
test_util.vector_path("os-release"))[0], 'systemdos') def test_get_os_info_ua(self, m_distro):
self.assertEqual(get_os_info( import certbot.util as cbutil
test_util.vector_path("os-release"))[1], '42') with mock.patch('platform.system_alias',
self.assertEqual(get_systemd_os_info(os.devnull), ("", "")) return_value=('linux', '42', '42')):
self.assertEqual(get_os_info_ua( m_distro.name.return_value = ""
test_util.vector_path("os-release")), "SystemdOS") m_distro.linux_distribution.return_value = ("something", "1.0", "codename")
with mock.patch('certbot.compat.os.path.isfile', return_value=False): cbutil.get_python_os_info(pretty=True)
self.assertEqual(get_systemd_os_info(), ("", "")) self.assertEqual(cbutil.get_os_info_ua(),
" ".join(cbutil.get_python_os_info(pretty=True)))
def test_systemd_os_release_like(self): m_distro.name.return_value = "whatever"
from certbot.util import get_systemd_os_like self.assertEqual(cbutil.get_os_info_ua(), "whatever")
with mock.patch('certbot.compat.os.path.isfile', return_value=True): @mock.patch("certbot.util.distro")
id_likes = get_systemd_os_like(test_util.vector_path( @unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux")
"os-release")) def test_get_os_info(self, m_distro):
self.assertEqual(len(id_likes), 3) import certbot.util as cbutil
self.assertTrue("debian" in id_likes) with mock.patch("platform.system") as mock_platform:
m_distro.linux_distribution.return_value = ("name", "version", 'x')
mock_platform.return_value = "linux"
self.assertEqual(cbutil.get_os_info(), ("name", "version"))
m_distro.linux_distribution.return_value = ("something", "else")
self.assertEqual(cbutil.get_os_info(), ("something", "else"))
@mock.patch("warnings.warn")
@mock.patch("certbot.util.distro")
@unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux")
def test_get_systemd_os_info_deprecation(self, _, mock_warn):
import certbot.util as cbutil
cbutil.get_systemd_os_info()
self.assertTrue(mock_warn.called)
@mock.patch("certbot.util.subprocess.Popen") @mock.patch("certbot.util.subprocess.Popen")
def test_non_systemd_os_info(self, popen_mock): def test_non_systemd_os_info(self, popen_mock):
from certbot.util import (get_os_info, get_python_os_info, import certbot.util as cbutil
get_os_info_ua) with mock.patch('certbot.util._USE_DISTRO', False):
with mock.patch('certbot.compat.os.path.isfile', return_value=False):
with mock.patch('platform.system_alias', with mock.patch('platform.system_alias',
return_value=('NonSystemD', '42', '42')): return_value=('NonSystemD', '42', '42')):
self.assertEqual(get_os_info()[0], 'nonsystemd') self.assertEqual(cbutil.get_python_os_info()[0], 'nonsystemd')
self.assertEqual(get_os_info_ua(),
" ".join(get_python_os_info()))
with mock.patch('platform.system_alias', with mock.patch('platform.system_alias',
return_value=('darwin', '', '')): return_value=('darwin', '', '')):
comm_mock = mock.Mock() comm_mock = mock.Mock()
comm_attrs = {'communicate.return_value': comm_attrs = {'communicate.return_value':
('42.42.42', 'error')} ('42.42.42', 'error')}
comm_mock.configure_mock(**comm_attrs) comm_mock.configure_mock(**comm_attrs)
popen_mock.return_value = comm_mock popen_mock.return_value = comm_mock
self.assertEqual(get_os_info()[0], 'darwin') self.assertEqual(cbutil.get_python_os_info()[0], 'darwin')
self.assertEqual(get_os_info()[1], '42.42.42') self.assertEqual(cbutil.get_python_os_info()[1], '42.42.42')
with mock.patch('platform.system_alias',
return_value=('linux', '', '')):
with mock.patch('platform.linux_distribution',
side_effect=AttributeError,
create=True):
with mock.patch('distro.linux_distribution',
return_value=('', '', '')):
self.assertEqual(get_python_os_info(), ("linux", ""))
with mock.patch('distro.linux_distribution',
return_value=('testdist', '42', '')):
self.assertEqual(get_python_os_info(), ("testdist", "42"))
with mock.patch('platform.system_alias', with mock.patch('platform.system_alias',
return_value=('freebsd', '9.3-RC3-p1', '')): return_value=('freebsd', '9.3-RC3-p1', '')):
self.assertEqual(get_python_os_info(), ("freebsd", "9")) self.assertEqual(cbutil.get_python_os_info(), ("freebsd", "9"))
with mock.patch('platform.system_alias', with mock.patch('platform.system_alias',
return_value=('windows', '', '')): return_value=('windows', '', '')):
with mock.patch('platform.win32_ver', with mock.patch('platform.win32_ver',
return_value=('4242', '95', '2', '')): return_value=('4242', '95', '2', '')):
self.assertEqual(get_python_os_info(), self.assertEqual(cbutil.get_python_os_info(),
("windows", "95")) ("windows", "95"))
@mock.patch("certbot.util.distro")
@unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux")
def test_python_os_info_notfound(self, m_distro):
import certbot.util as cbutil
m_distro.linux_distribution.return_value = ('', '', '')
self.assertEqual(cbutil.get_python_os_info()[0], "linux")
@mock.patch("certbot.util.distro")
@unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux")
def test_python_os_info_custom(self, m_distro):
import certbot.util as cbutil
m_distro.linux_distribution.return_value = ('testdist', '42', '')
self.assertEqual(cbutil.get_python_os_info(), ("testdist", "42"))
class AtexitRegisterTest(unittest.TestCase): class AtexitRegisterTest(unittest.TestCase):
+31 -50
View File
@@ -12,9 +12,10 @@ import platform
import re import re
import socket import socket
import subprocess import subprocess
import sys
import warnings
import configargparse import configargparse
import distro
import six import six
from acme.magic_typing import Tuple, Union # pylint: disable=unused-import, no-name-in-module from acme.magic_typing import Tuple, Union # pylint: disable=unused-import, no-name-in-module
@@ -25,6 +26,12 @@ from certbot import lock
from certbot.compat import os from certbot.compat import os
from certbot.compat import filesystem from certbot.compat import filesystem
if sys.platform.startswith('linux'):
import distro
_USE_DISTRO = True
else:
_USE_DISTRO = False
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -277,77 +284,59 @@ def get_filtered_names(all_names):
logger.debug('Not suggesting name "%s"', name, exc_info=True) logger.debug('Not suggesting name "%s"', name, exc_info=True)
return filtered_names return filtered_names
def get_os_info():
def get_os_info(filepath="/etc/os-release"):
""" """
Get OS name and version Get OS name and version
:param str filepath: File path of os-release file
:returns: (os_name, os_version) :returns: (os_name, os_version)
:rtype: `tuple` of `str` :rtype: `tuple` of `str`
""" """
if os.path.isfile(filepath): return get_python_os_info(pretty=False)
# Systemd os-release parsing might be viable
os_name, os_version = get_systemd_os_info(filepath=filepath)
if os_name:
return os_name, os_version
# Fallback to platform module def get_os_info_ua():
return get_python_os_info()
def get_os_info_ua(filepath="/etc/os-release"):
""" """
Get OS name and version string for User Agent Get OS name and version string for User Agent
:param str filepath: File path of os-release file
:returns: os_ua :returns: os_ua
:rtype: `str` :rtype: `str`
""" """
if _USE_DISTRO:
os_info = distro.name(pretty=True)
if os.path.isfile(filepath): if not _USE_DISTRO or not os_info:
os_ua = get_var_from_file("PRETTY_NAME", filepath=filepath) return " ".join(get_python_os_info(pretty=True))
if not os_ua: return os_info
os_ua = get_var_from_file("NAME", filepath=filepath)
if os_ua:
return os_ua
# Fallback def get_systemd_os_info():
return " ".join(get_python_os_info())
def get_systemd_os_info(filepath="/etc/os-release"):
""" """
Parse systemd /etc/os-release for distribution information Parse systemd /etc/os-release for distribution information
:param str filepath: File path of os-release file
:returns: (os_name, os_version) :returns: (os_name, os_version)
:rtype: `tuple` of `str` :rtype: `tuple` of `str`
""" """
os_name = get_var_from_file("ID", filepath=filepath) warnings.warn(
os_version = get_var_from_file("VERSION_ID", filepath=filepath) "The get_sytemd_os_like() function is deprecated and will be removed in "
"a future release.", DeprecationWarning, stacklevel=2)
return get_os_info()[:2]
return (os_name, os_version) def get_systemd_os_like():
def get_systemd_os_like(filepath="/etc/os-release"):
""" """
Get a list of strings that indicate the distribution likeness to Get a list of strings that indicate the distribution likeness to
other distributions. other distributions.
:param str filepath: File path of os-release file
:returns: List of distribution acronyms :returns: List of distribution acronyms
:rtype: `list` of `str` :rtype: `list` of `str`
""" """
return get_var_from_file("ID_LIKE", filepath).split(" ") if _USE_DISTRO:
return distro.like().split(" ")
return []
def get_var_from_file(varname, filepath="/etc/os-release"): def get_var_from_file(varname, filepath="/etc/os-release"):
""" """
Get single value from systemd /etc/os-release Get single value from a file formatted like systemd /etc/os-release
:param str varname: Name of variable to fetch :param str varname: Name of variable to fetch
:param str filepath: File path of os-release file :param str filepath: File path of os-release file
@@ -367,7 +356,6 @@ def get_var_from_file(varname, filepath="/etc/os-release"):
return _normalize_string(line.strip()[len(var_string):]) return _normalize_string(line.strip()[len(var_string):])
return "" return ""
def _normalize_string(orig): def _normalize_string(orig):
""" """
Helper function for get_var_from_file() to remove quotes Helper function for get_var_from_file() to remove quotes
@@ -375,12 +363,13 @@ def _normalize_string(orig):
""" """
return orig.replace('"', '').replace("'", "").strip() return orig.replace('"', '').replace("'", "").strip()
def get_python_os_info(pretty=False):
def get_python_os_info():
""" """
Get Operating System type/distribution and major version Get Operating System type/distribution and major version
using python platform module using python platform module
:param bool pretty: If the returned OS name should be in longer (pretty) form
:returns: (os_name, os_version) :returns: (os_name, os_version)
:rtype: `tuple` of `str` :rtype: `tuple` of `str`
""" """
@@ -391,8 +380,8 @@ def get_python_os_info():
) )
os_type, os_ver, _ = info os_type, os_ver, _ = info
os_type = os_type.lower() os_type = os_type.lower()
if os_type.startswith('linux'): if os_type.startswith('linux') and _USE_DISTRO:
info = _get_linux_distribution() info = distro.linux_distribution(pretty)
# On arch, distro.linux_distribution() is reportedly ('','',''), # On arch, distro.linux_distribution() is reportedly ('','',''),
# so handle it defensively # so handle it defensively
if info[0]: if info[0]:
@@ -424,14 +413,6 @@ def get_python_os_info():
os_ver = '' os_ver = ''
return os_type, os_ver return os_type, os_ver
def _get_linux_distribution():
"""Gets the linux distribution name from the underlying OS"""
try:
return platform.linux_distribution()
except AttributeError:
return distro.linux_distribution()
# Just make sure we don't get pwned... Make sure that it also doesn't # Just make sure we don't get pwned... Make sure that it also doesn't
# start with a period or have two consecutive periods <- this needs to # start with a period or have two consecutive periods <- this needs to
# be done in addition to the regex # be done in addition to the regex
+1
View File
@@ -18,3 +18,4 @@ filterwarnings =
ignore:.*collections\.abc:DeprecationWarning ignore:.*collections\.abc:DeprecationWarning
ignore:The `color_scheme` argument is deprecated:DeprecationWarning:IPython.* ignore:The `color_scheme` argument is deprecated:DeprecationWarning:IPython.*
ignore:.*view_config_changes:DeprecationWarning ignore:.*view_config_changes:DeprecationWarning
ignore:.*get_systemd_os_info:DeprecationWarning