diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index faf74394d..ffbedc90b 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -78,6 +78,8 @@ CLI_DEFAULTS = { "centos linux": CLI_DEFAULTS_CENTOS, "fedora": CLI_DEFAULTS_CENTOS, "red hat enterprise linux server": CLI_DEFAULTS_CENTOS, + "rhel": CLI_DEFAULTS_CENTOS, + "amazon": CLI_DEFAULTS_CENTOS, "gentoo base system": CLI_DEFAULTS_GENTOO, "darwin": CLI_DEFAULTS_DARWIN, } diff --git a/certbot/client.py b/certbot/client.py index 3d17e2295..8c178408c 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -53,7 +53,7 @@ def _determine_user_agent(config): if config.user_agent is None: ua = "CertbotACMEClient/{0} ({1}) Authenticator/{2} Installer/{3}" - ua = ua.format(certbot.__version__, " ".join(util.get_os_info()), + ua = ua.format(certbot.__version__, util.get_os_info_ua(), config.authenticator, config.installer) else: ua = config.user_agent diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 7b99fd339..671da16f0 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -171,13 +171,13 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: self._call_no_clientmock(args) - os_ver = " ".join(util.get_os_info()) + os_ver = util.get_os_info_ua() ua = acme_net.call_args[1]["user_agent"] self.assertTrue(os_ver in ua) import platform plat = platform.platform() if "linux" in plat.lower(): - self.assertTrue(platform.linux_distribution()[0] in ua) + self.assertTrue(util.get_os_info_ua() in ua) with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: ua = "bandersnatch" diff --git a/certbot/tests/testdata/os-release b/certbot/tests/testdata/os-release new file mode 100644 index 000000000..cd5297acf --- /dev/null +++ b/certbot/tests/testdata/os-release @@ -0,0 +1,7 @@ +NAME="SystemdOS" +VERSION="42.42.42 LTS, Unreal" +ID=systemdos +ID_LIKE=debian +VERSION_ID="42" +HOME_URL="http://www.example.com/" +SUPPORT_URL="http://help.example.com/" diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index ada64edb2..8e1b330ed 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -10,8 +10,8 @@ import unittest import mock import six -import certbot from certbot import errors +from certbot.tests import test_util class RunScriptTest(unittest.TestCase): @@ -340,31 +340,66 @@ class EnforceDomainSanityTest(unittest.TestCase): u"eichh\u00f6rnchen.example.com") -class GetStrictVersionTest(unittest.TestCase): - """Tests for certbot.util.get_strict_version.""" +class OsInfoTest(unittest.TestCase): + """Test OS / distribution detection""" - @classmethod - def _call(cls, *args, **kwargs): - from certbot.util import get_strict_version - return get_strict_version(*args, **kwargs) + def test_systemd_os_release(self): + from certbot.util import (get_os_info, get_systemd_os_info, + get_os_info_ua) - def test_two_dev_versions(self): - self.assertTrue( - self._call("0.0.0.dev20151006") < self._call("0.0.0.dev20151008")) + with mock.patch('os.path.isfile', return_value=True): + self.assertEqual(get_os_info( + test_util.vector_path("os-release"))[0], 'systemdos') + self.assertEqual(get_os_info( + test_util.vector_path("os-release"))[1], '42') + self.assertEqual(get_systemd_os_info("/dev/null"), ("", "")) + self.assertEqual(get_os_info_ua( + test_util.vector_path("os-release")), + "SystemdOS") + with mock.patch('os.path.isfile', return_value=False): + self.assertEqual(get_systemd_os_info(), ("", "")) - def test_one_dev_one_release_version(self): - self.assertTrue(self._call("1.0.0.dev0") < self._call("1.0.0")) - self.assertTrue(self._call("1.0.0") < self._call("1.0.1.dev0")) + @mock.patch("certbot.util.subprocess.Popen") + def test_non_systemd_os_info(self, popen_mock): + from certbot.util import (get_os_info, get_python_os_info, + get_os_info_ua) + with mock.patch('os.path.isfile', return_value=False): + with mock.patch('platform.system_alias', + return_value=('NonSystemD', '42', '42')): + self.assertEqual(get_os_info()[0], 'nonsystemd') + self.assertEqual(get_os_info_ua(), + " ".join(get_python_os_info())) - def test_two_release_versions(self): - self.assertTrue(self._call("0.0.0") < self._call("0.0.1")) - self.assertTrue(self._call("0.0.0") < self._call("0.1.0")) - self.assertTrue(self._call("0.0.0") < self._call("1.0.0")) + with mock.patch('platform.system_alias', + return_value=('darwin', '', '')): + comm_mock = mock.Mock() + comm_attrs = {'communicate.return_value': + ('42.42.42', 'error')} + comm_mock.configure_mock(**comm_attrs) # pylint: disable=star-args + popen_mock.return_value = comm_mock + self.assertEqual(get_os_info()[0], 'darwin') + self.assertEqual(get_os_info()[1], '42.42.42') - def test_current_version(self): - current_version = self._call(certbot.__version__) - self.assertTrue(self._call("0.6.0") < current_version) - self.assertTrue(current_version < self._call("99.99.99")) + with mock.patch('platform.system_alias', + return_value=('linux', '', '')): + with mock.patch('platform.linux_distribution', + return_value=('', '', '')): + self.assertEqual(get_python_os_info(), ("linux", "")) + + with mock.patch('platform.linux_distribution', + return_value=('testdist', '42', '')): + self.assertEqual(get_python_os_info(), ("testdist", "42")) + + with mock.patch('platform.system_alias', + return_value=('freebsd', '9.3-RC3-p1', '')): + self.assertEqual(get_python_os_info(), ("freebsd", "9")) + + with mock.patch('platform.system_alias', + return_value=('windows', '', '')): + with mock.patch('platform.win32_ver', + return_value=('4242', '95', '2', '')): + self.assertEqual(get_python_os_info(), + ("windows", "95")) if __name__ == "__main__": diff --git a/certbot/util.py b/certbot/util.py index 020050f6d..8507f80d6 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -213,9 +213,95 @@ def safely_remove(path): raise -def get_os_info(): +def get_os_info(filepath="/etc/os-release"): + """ + Get OS name and version + + :param str filepath: File path of os-release file + :returns: (os_name, os_version) + :rtype: `tuple` of `str` + """ + + if os.path.isfile(filepath): + # 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 + return get_python_os_info() + + +def get_os_info_ua(filepath="/etc/os-release"): + """ + Get OS name and version string for User Agent + + :param str filepath: File path of os-release file + :returns: os_ua + :rtype: `str` + """ + + if os.path.isfile(filepath): + os_ua = _get_systemd_os_release_var("PRETTY_NAME", filepath=filepath) + if not os_ua: + os_ua = _get_systemd_os_release_var("NAME", filepath=filepath) + if os_ua: + return os_ua + + # Fallback + return " ".join(get_python_os_info()) + + +def get_systemd_os_info(filepath="/etc/os-release"): + """ + Parse systemd /etc/os-release for distribution information + + :param str filepath: File path of os-release file + :returns: (os_name, os_version) + :rtype: `tuple` of `str` + """ + + os_name = _get_systemd_os_release_var("ID", filepath=filepath) + os_version = _get_systemd_os_release_var("VERSION_ID", filepath=filepath) + + return (os_name, os_version) + + +def _get_systemd_os_release_var(varname, filepath="/etc/os-release"): + """ + Get single value from systemd /etc/os-release + + :param str varname: Name of variable to fetch + :param str filepath: File path of os-release file + :returns: requested value + :rtype: `str` + """ + + var_string = varname+"=" + if not os.path.isfile(filepath): + return "" + with open(filepath, 'r') as fh: + contents = fh.readlines() + + for line in contents: + if line.strip().startswith(var_string): + # Return the value of var, normalized + return _normalize_string(line.strip()[len(var_string):]) + return "" + + +def _normalize_string(orig): + """ + Helper function for _get_systemd_os_release_var() to remove quotes + and whitespaces + """ + return orig.replace('"', '').replace("'", "").strip() + + +def get_python_os_info(): """ Get Operating System type/distribution and major version + using python platform module :returns: (os_name, os_version) :rtype: `tuple` of `str` @@ -240,7 +326,6 @@ def get_os_info(): ["sw_vers", "-productVersion"], stdout=subprocess.PIPE ).communicate()[0] - os_ver = os_ver.partition(".")[0] elif os_type.startswith('freebsd'): # eg "9.3-RC3-p1" os_ver = os_ver.partition("-")[0] @@ -317,7 +402,7 @@ def enforce_domain_sanity(domain): domain = domain.encode('ascii').lower() except UnicodeError: error_fmt = (u"Internationalized domain names " - "are not presently supported: {0}") + "are not presently supported: {0}") if isinstance(domain, six.text_type): raise errors.ConfigurationError(error_fmt.format(domain)) else: @@ -344,7 +429,8 @@ def enforce_domain_sanity(domain): # first and last char is not "-" fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?