From a1e87b787d6fd1ab097e52518ac61dd88ce150c3 Mon Sep 17 00:00:00 2001 From: Mikhail Sandakov Date: Mon, 15 Apr 2024 08:53:03 +0300 Subject: [PATCH] Add a checker to ensure that the currently installed version of Plesk is available on autoinstall.plesk.com --- pleskdistup/actions/plesk.py | 60 ++++++++++------ pleskdistup/common/src/plesk.py | 40 +++++++++-- pleskdistup/common/src/version.py | 63 +++++++++++++++++ pleskdistup/common/tests/plesktests.py | 90 ++++++++++++++++++++++++ pleskdistup/common/tests/versiontests.py | 87 ++++++++++++++++++++++- 5 files changed, 314 insertions(+), 26 deletions(-) create mode 100644 pleskdistup/common/tests/plesktests.py diff --git a/pleskdistup/actions/plesk.py b/pleskdistup/actions/plesk.py index b4263b0..2646e4a 100644 --- a/pleskdistup/actions/plesk.py +++ b/pleskdistup/actions/plesk.py @@ -5,7 +5,7 @@ import subprocess import typing -from pleskdistup.common import action, packages, plesk, log, util +from pleskdistup.common import action, packages, plesk, log, util, version def _change_plesk_components( @@ -426,7 +426,7 @@ def _do_check(self) -> bool: class AssertMinPleskVersion(action.CheckAction): - min_version: typing.List[int] + min_version: version.PleskVersion _name: str _description: str @@ -436,24 +436,13 @@ def __init__( name: str = "check for minimal Plesk version {min_version}", description: str = "Only Plesk Obsidian {min_version} or later is supported. Please upgrade Plesk and try again.", ): - try: - vlist = min_version.split(".") - if len(vlist) not in (3, 4): - raise ValueError("Incorrect version length") - self.min_version = [int(v) for v in vlist] - if any(v < 0 for v in self.min_version): - raise ValueError("Negative number in version") - if len(self.min_version) == 3: - self.min_version.append(0) - except Exception as e: - raise ValueError("Plesk version must be in the 1.2.3[.4] format, e.g. 18.0.58 or 18.0.58.0") from e - assert len(self.min_version) == 4 + self.min_version = version.PleskVersion(min_version) self._name = name self._description = description @property def name(self) -> str: - return self._name.format(min_version=self.min_version_str) + return self._name.format(min_version=self.min_version) @name.setter def name(self, val: str) -> None: @@ -461,16 +450,12 @@ def name(self, val: str) -> None: @property def description(self) -> str: - return self._description.format(min_version=self.min_version_str) + return self._description.format(min_version=self.min_version) @description.setter def description(self, val: str) -> None: self._description = val - @property - def min_version_str(self) -> str: - return ".".join(str(v) for v in self.min_version) - def _do_check(self) -> bool: try: cur_version = [int(v) for v in plesk.get_plesk_version()] @@ -480,3 +465,38 @@ def _do_check(self) -> bool: except Exception as e: log.err(f"Checking Plesk version has failed with error: {e}") raise + + +class AssertPleskVersionIsAvailable(action.CheckAction): + _description_template: str + + def __init__(self): + self.name = "check if currently installed Plesk version is available" + self.description = "Currently installed Plesk version is outdated. Please upgrade Plesk to the latest version by 'plesk installer'" + self._description_template = "Currently installed Plesk version {current_version} is outdated. Please upgrade Plesk to the latest version by 'plesk installer'" + + def _do_check(self) -> bool: + try: + available_versions = plesk.get_available_plesk_versions() + if available_versions == []: + log.warn("Unable to retrieve available versions from autoinstall.plesk.com") + return False + current_version = plesk.get_plesk_version() + # The product list does not contain information about hotfixes, so our comparison can't be so accurate + # However, we are attempting to determine if the Plesk repository is accessible. Repositories are + # linked to the main part of the version (e.g., 18.0.50,18.0.51) and are shared between hotfix + # Therefore, we should not compare hotfix part in this case, as we are aware that the repository + # for the currently installed version is available. + current_version.hotfix = 0 + + log.debug(f"Received available versions of plesk are: {available_versions}") + log.debug(f"Plesk version installed on the host is '{current_version}'") + + if current_version not in available_versions: + self.description = self._description_template.format(current_version=current_version) + return False + + return True + except Exception as e: + log.err(f"Checking Plesk version has failed with error: {e}") + raise diff --git a/pleskdistup/common/src/plesk.py b/pleskdistup/common/src/plesk.py index 6743404..d0d072c 100644 --- a/pleskdistup/common/src/plesk.py +++ b/pleskdistup/common/src/plesk.py @@ -5,9 +5,14 @@ import re import subprocess import typing +import urllib.request +import xml.etree.ElementTree as ElementTree -from . import log, mariadb, systemd +from . import log, mariadb, systemd, version +# http://autoinstall.plesk.com/products.inf3 is an xml file with available products, +# including all versions of Plesk. +DEFAULT_AUTOINSTALL_PRODUCTS_FILE = "http://autoinstall.plesk.com/products.inf3" def send_error_report(error_message: str) -> None: log.debug(f"Error report: {error_message}") @@ -24,16 +29,41 @@ def send_error_report(error_message: str) -> None: log.debug(f"Sending error report failed: {ex}") -def get_plesk_version() -> typing.List[str]: +def get_plesk_version() -> version.PleskVersion: version_info = subprocess.check_output(["/usr/sbin/plesk", "version"], universal_newlines=True).splitlines() for line in version_info: if line.startswith("Product version"): - version = line.split()[-1] - return version.split(".") + return version.PleskVersion(line.split()[-1]) raise Exception("Unable to parse plesk version output.") +def extract_plesk_versions(products_xml: str) -> typing.List[version.PleskVersion]: + if not products_xml: + return [] + + versions = [] + root = ElementTree.fromstring(products_xml) + for product in root.findall('.//product[@id="plesk"]'): + release_key = product.get('release-key') + if release_key and '-' in release_key: + versions.append(version.PleskVersion(release_key.split("-", 1)[1])) + return versions + + +def get_available_plesk_versions( + autoinstall_products_file_url: str = DEFAULT_AUTOINSTALL_PRODUCTS_FILE +) -> typing.List[version.PleskVersion]: + try: + with urllib.request.urlopen(autoinstall_products_file_url) as response: + products_config = response.read().decode('utf-8') + return extract_plesk_versions(products_config) + + except Exception as ex: + log.warn(f"Unable to retrieve available versions of plesk from '{autoinstall_products_file_url}': {ex}") + return [] + + def get_plesk_full_version() -> typing.List[str]: return subprocess.check_output(["/usr/sbin/plesk", "version"], universal_newlines=True).splitlines() @@ -60,7 +90,7 @@ def send_conversion_status(succeed: bool, status_flag_path: str) -> None: log.warn("Conversion status flag file does not exist. Skip sending conversion status") return - plesk_version = ".".join(get_plesk_version()) + plesk_version = str(get_plesk_version()) try: log.debug(f"Trying to send status of conversion by report-update utility {results_sender_path!r}") diff --git a/pleskdistup/common/src/version.py b/pleskdistup/common/src/version.py index 44bd6c8..57905d7 100644 --- a/pleskdistup/common/src/version.py +++ b/pleskdistup/common/src/version.py @@ -1,5 +1,7 @@ # Copyright 2023-2024. WebPros International GmbH. All rights reserved. +import typing + class KernelVersion(): """Linux kernel version representation class.""" @@ -151,3 +153,64 @@ def __eq__(self, other) -> bool: def __ge__(self, other) -> bool: return not self.__lt__(other) + + +class PleskVersion: + """ + Plesk version representation class. + + Plesk version is represented as a string in format "major.minor.patch.hotfix". + Examples: + - "18.0.50" + - "18.0.51.2" + + Versions could be compared with each other, represented as a string. + Available fields are: major, minor, patch and hotfix. + """ + + major: int + minor: int + patch: int + hotfix: int + + def _extract_from_version(self, version: str) -> None: + split_version = version.split(".") + if len(split_version) not in (3, 4): + raise ValueError("Incorrect version length") + + # Version string example is "18.0.50" or "18.0.50.2" + self.major, self.minor, self.patch = map(int, split_version[:3]) + if len(split_version) > 3: + self.hotfix = int(split_version[3]) + else: + self.hotfix = 0 + + if self.major < 0 or self.minor < 0 or self.patch < 0 or self.hotfix < 0: + raise ValueError("Negative number in version") + + def __init__(self, version: str): + """Initialize a PleskVersion object.""" + self.major = 0 + self.minor = 0 + self.patch = 0 + self.hotfix = 0 + + self._extract_from_version(version) + + def _to_tuple(self) -> typing.Tuple[int, int, int, int]: + return (self.major, self.minor, self.patch, self.hotfix) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(major={self.major!r}, minor={self.minor!r}, patch={self.patch!r}, hotfix={self.hotfix!r})" + + def __str__(self) -> str: + return f"{self.major}.{self.minor}.{self.patch}.{self.hotfix}" + + def __lt__(self, other) -> bool: + return self._to_tuple() < other._to_tuple() + + def __eq__(self, other) -> bool: + return self._to_tuple() == other._to_tuple() + + def __ge__(self, other) -> bool: + return not self.__lt__(other) diff --git a/pleskdistup/common/tests/plesktests.py b/pleskdistup/common/tests/plesktests.py new file mode 100644 index 0000000..9a1d9d3 --- /dev/null +++ b/pleskdistup/common/tests/plesktests.py @@ -0,0 +1,90 @@ +# Copyright 2023-2024. WebPros International GmbH. All rights reserved. +import unittest + +from src import plesk, version + + +class TestProductsFileParser(unittest.TestCase): + + def test_simple_parce(self): + simple_data = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + expected_versions = [version.PleskVersion(ver) for ver in ["18.0.55", "18.0.56", "18.0.57", "18.0.58", "18.0.59", "18.0.60"]] + self.assertEqual(expected_versions, sorted(plesk.extract_plesk_versions(simple_data))) + + def test_empty_data(self): + self.assertEqual([], plesk.extract_plesk_versions("")) + + def test_no_plesk_prudict(self): + data = """ + + + + + + + + + + + + + +""" + + self.assertEqual([], plesk.extract_plesk_versions(data)) + + def test_only_plesk_without_release_key(self): + data = """ + + + + + + + +""" + + self.assertEqual([], plesk.extract_plesk_versions(data)) diff --git a/pleskdistup/common/tests/versiontests.py b/pleskdistup/common/tests/versiontests.py index a86ad0f..e1e5e0c 100644 --- a/pleskdistup/common/tests/versiontests.py +++ b/pleskdistup/common/tests/versiontests.py @@ -1,7 +1,7 @@ # Copyright 2023-2024. WebPros International GmbH. All rights reserved. import unittest -import src.version as version +from src import version class KernelVersionTests(unittest.TestCase): @@ -216,3 +216,88 @@ def test_compare_less_major_greater_minor_croocked(self): php1 = version.PHPVersion("PHP 6.1") php2 = version.PHPVersion("PHP 5.2") self.assertGreater(php1, php2) + + +class PleskVersionTests(unittest.TestCase): + + def test_plesk_parse_no_hotfix(self): + plesk_version = version.PleskVersion("18.0.55") + self.assertEqual(plesk_version.major, 18) + self.assertEqual(plesk_version.minor, 0) + self.assertEqual(plesk_version.patch, 55) + self.assertEqual(plesk_version.hotfix, 0) + + def test_plesk_parse_with_hotfix(self): + plesk_version = version.PleskVersion("18.0.55.2") + self.assertEqual(plesk_version.major, 18) + self.assertEqual(plesk_version.minor, 0) + self.assertEqual(plesk_version.patch, 55) + self.assertEqual(plesk_version.hotfix, 2) + + def test_plesk_parse_with_hotfix_zero(self): + plesk_version = version.PleskVersion("18.0.55.0") + self.assertEqual(plesk_version.major, 18) + self.assertEqual(plesk_version.minor, 0) + self.assertEqual(plesk_version.patch, 55) + self.assertEqual(plesk_version.hotfix, 0) + + def test_plesk_parse_not_enough_parts(self): + with self.assertRaises(ValueError): + version.PleskVersion("18.0") + + def test_plesk_parse_too_many_parts(self): + with self.assertRaises(ValueError): + version.PleskVersion("18.0.55.2.3") + + def test_plesk_parse_negative_number(self): + with self.assertRaises(ValueError): + version.PleskVersion("18.0.-55.0") + + def test_php_parse_wrong_string(self): + with self.assertRaises(ValueError): + version.PleskVersion("nothing") + + def test_compare_equal(self): + plesk_version1 = version.PleskVersion("18.0.55") + plesk_version2 = version.PleskVersion("18.0.55.0") + self.assertEqual(plesk_version1, plesk_version2) + + def test_compare_less_major(self): + plesk_version1 = version.PleskVersion("18.0.55") + plesk_version2 = version.PleskVersion("19.0.55") + self.assertLess(plesk_version1, plesk_version2) + + def test_compare_less_major_greater_minor(self): + plesk_version1 = version.PleskVersion("18.2.55") + plesk_version2 = version.PleskVersion("19.0.55") + self.assertLess(plesk_version1, plesk_version2) + + def test_compare_less_minor(self): + plesk_version1 = version.PleskVersion("18.0.55") + plesk_version2 = version.PleskVersion("18.1.55") + self.assertLess(plesk_version1, plesk_version2) + + def test_compare_less_minor_greater_patch(self): + plesk_version1 = version.PleskVersion("18.0.57") + plesk_version2 = version.PleskVersion("18.1.55") + self.assertLess(plesk_version1, plesk_version2) + + def test_compare_less_minor_greater_hotfix(self): + plesk_version1 = version.PleskVersion("18.0.57.1") + plesk_version2 = version.PleskVersion("18.1.55") + self.assertLess(plesk_version1, plesk_version2) + + def test_compare_less_patch(self): + plesk_version1 = version.PleskVersion("18.0.54") + plesk_version2 = version.PleskVersion("18.0.55") + self.assertLess(plesk_version1, plesk_version2) + + def test_compare_less_patch_greater_hotfix(self): + plesk_version1 = version.PleskVersion("18.0.54.4") + plesk_version2 = version.PleskVersion("18.0.55.1") + self.assertLess(plesk_version1, plesk_version2) + + def test_compare_less_hotfix(self): + plesk_version1 = version.PleskVersion("18.0.55.1") + plesk_version2 = version.PleskVersion("18.0.55.2") + self.assertLess(plesk_version1, plesk_version2)