Skip to content

Commit

Permalink
Merge pull request #28 from plesk/check-plesk-version-is-not-outdated
Browse files Browse the repository at this point in the history
Add a checker to ensure that the currently installed version of Plesk is available on autoinstall.plesk.com
  • Loading branch information
SandakovMM authored Apr 17, 2024
2 parents 55062f0 + a1e87b7 commit 27f7bee
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 26 deletions.
60 changes: 40 additions & 20 deletions pleskdistup/actions/plesk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand All @@ -436,41 +436,26 @@ 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:
self._name = val

@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()]
Expand All @@ -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
40 changes: 35 additions & 5 deletions pleskdistup/common/src/plesk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand All @@ -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()

Expand All @@ -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}")
Expand Down
63 changes: 63 additions & 0 deletions pleskdistup/common/src/version.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Copyright 2023-2024. WebPros International GmbH. All rights reserved.

import typing

class KernelVersion():
"""Linux kernel version representation class."""

Expand Down Expand Up @@ -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)
90 changes: 90 additions & 0 deletions pleskdistup/common/tests/plesktests.py
Original file line number Diff line number Diff line change
@@ -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 = """
<vendors>
<vendor id="swsoft">
<products>
<product id="plesk" name="Plesk" reference="pool/PSA_18.0.60_14244/release.inf3" release-key="plesk-18.0.60"/>
<addon id="wpb-18.0.59" name="WPB 18.0.59" reference="pool/WPB_18.0.59_77/release.inf3"/>
<product id="plesk" name="Plesk" reference="pool/PSA_18.0.59_14022/release.inf3" release-key="plesk-18.0.59"/>
<product id="plesk" name="Plesk" reference="pool/PSA_18.0.58_13749/release.inf3" release-key="plesk-18.0.58"/>
<addon id="php83" name="PHP v 8.3" reference="PHP83_17/release.inf3"/>
<product id="plesk" name="Plesk" reference="pool/PSA_18.0.57_13503/release.inf3" release-key="plesk-18.0.57"/>
<product id="plesk" name="Plesk" reference="pool/PSA_18.0.56_13177/release.inf3" release-key="plesk-18.0.56"/>
<addon id="wpb-18.0.55" name="WPB 18.0.55" reference="pool/WPB_18.0.55_74/release.inf3"/>
<product id="plesk" name="Plesk" reference="pool/PSA_18.0.55_12738/release.inf3" release-key="plesk-18.0.55"/>
<addon id="wpb-18.0.51" name="WPB 18.0.51" reference="pool/WPB_18.0.51_64/release.inf3"/>
<addon id="php82" name="PHP v 8.2" reference="PHP82_17/release.inf3"/>
<addon id="php82" name="PHP v 8.2 (EOL OSes)" reference="php82.inf3"/>
<addon id="php81" name="PHP v 8.1" reference="PHP81_17/release.inf3"/>
<addon id="php81" name="PHP v 8.1 (EOL OSes)" reference="php81.inf3"/>
<addon id="php80" name="PHP v 8.0" reference="PHP80_17/release.inf3"/>
<addon id="php80" name="PHP v 8.0 (EOL OSes)" reference="php80.inf3"/>
<addon id="php72" name="PHP v 72 (EOL)" reference="php72.inf3"/>
<addon id="php74" name="PHP v 7.4" reference="PHP74_17/release.inf3"/>
<addon id="php74" name="PHP v 7.4 (EOL OSes)" reference="php74.inf3"/>
<addon id="php73" name="PHP v 73 (EOL)" reference="php73.inf3"/>
<addon id="php71" name="PHP v 71 (EOL)" reference="php71.inf3"/>
<product id="plesk" name="Plesk" reference="plesk.inf3"/>
<product id="sitebuilder" name="Sitebuilder 4.5 and earlier versions (for Plesk 9 and earlier)" reference="sitebuilder.inf3"/>
<addon id="setemplates" name="SiteEditor templates" reference="setemplates.inf3"/>
<addon id="pp-sitebuilder" name="Sitebuilder" reference="pp-sitebuilder.inf3"/>
<addon id="billing" name="Paralllels billing" reference="billing.inf3"/>
<lazy_addon id="mysql5.1" name="MySQL v 5.1" reference="mysql.inf3"/>
<lazy_addon id="apache" name="Apache with SNI support" reference="apache.inf3"/>
<lazy_addon id="nginx" name="NGINX reverse proxy server" reference="nginx.inf3"/>
<addon id="php70" name="PHP v 7.0" reference="php70.inf3"/>
<addon id="php56" name="PHP v 5.6" reference="php56.inf3"/>
<addon id="php55" name="PHP v 5.5" reference="php55.inf3"/>
<addon id="php54" name="PHP v 5.4" reference="php54.inf3"/>
<addon id="php53" name="PHP v 5.3" reference="php53.inf3"/>
<addon id="php52" name="PHP v 5.2" reference="php52.inf3"/>
<addon id="pmm" name="Panel migrator" reference="pmm.inf3"/>
</products>
</vendor>
</vendors>
"""
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 = """
<vendors>
<vendor id="swsoft">
<products>
<addon id="php70" name="PHP v 7.0" reference="php70.inf3"/>
<addon id="php56" name="PHP v 5.6" reference="php56.inf3"/>
<addon id="php55" name="PHP v 5.5" reference="php55.inf3"/>
<addon id="php54" name="PHP v 5.4" reference="php54.inf3"/>
<addon id="php53" name="PHP v 5.3" reference="php53.inf3"/>
<addon id="php52" name="PHP v 5.2" reference="php52.inf3"/>
<addon id="pmm" name="Panel migrator" reference="pmm.inf3"/>
</products>
</vendor>
</vendors>
"""

self.assertEqual([], plesk.extract_plesk_versions(data))

def test_only_plesk_without_release_key(self):
data = """
<vendors>
<vendor id="swsoft">
<products>
<product id="plesk" name="Plesk" reference="plesk.inf3"/>
</products>
</vendor>
</vendors>
"""

self.assertEqual([], plesk.extract_plesk_versions(data))
Loading

0 comments on commit 27f7bee

Please sign in to comment.