diff --git a/src/core/src/core_logic/VersionComparator.py b/src/core/src/core_logic/VersionComparator.py new file mode 100644 index 000000000..68779c91e --- /dev/null +++ b/src/core/src/core_logic/VersionComparator.py @@ -0,0 +1,82 @@ +# Copyright 2024 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.7+ +import re + + +class VersionComparator(object): + + def compare_version_nums(self, version_a, version_b): + # type (str, str) -> int + """ Compare two versions with handling numeric and string parts, return -1 (less), +1 (greater), 0 (equal) """ + + parse_version_a = self.__parse_version(version_a) + parse_version_b = self.__parse_version(version_b) + + for v1, v2 in zip(parse_version_a, parse_version_b): + for sub_v1, sub_v2 in zip(v1, v2): + if sub_v1 < sub_v2: + return -1 # less + elif sub_v1 > sub_v2: + return 1 # greater + + # If equal 27.13.4 vs 27.13.4, return 0 + return (len(parse_version_a) > len(parse_version_b)) - (len(parse_version_a) < len(parse_version_b)) + + def extract_version_nums(self, path): + # type (str) -> str + """ + Extract the version part from a given path. + Input: /var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-1.2.5/config + Return: "1.2.5" + """ + match = re.search(r'([\d]+\.[\d]+\.[\d]+)', path) + return match.group(1) if match else str() + + def sort_versions_desc_order(self, paths): + # type (list[str]) -> list[str] + """ + Sort paths based on version numbers extracted from paths. + Input: + ["Microsoft.CPlat.Core.LinuxPatchExtension-1.21.1001", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.100", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.21.100"] + Return: + ["Microsoft.CPlat.Core.LinuxPatchExtension-1.21.1001", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.21.100", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.100"] + """ + return sorted(paths, key=self.__version_key, reverse=True) + + def __version_key(self, path): + # type (str) -> (int) + """ Extract version number from input and return int tuple. + Input: "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.100" + Return: (1.6.100) + """ + version_numbers = self.extract_version_nums(path) + return tuple(map(int, version_numbers.split('.'))) if version_numbers else (0, 0, 0) + + def __split_version_components(self, version): + # type (str) -> [any] + """ Split a version into numeric and non-numeric into components list: 27.13.4~18.04.1 -> [27][14][4]""" + return [int(x) if x.isdigit() else x for x in re.split(r'(\d+)', version) if x] + + def __parse_version(self, version_components): + # type (str) -> [[any]] + """ Parse the split version list into list [27][14][4] -> [[27], [14], [4]]""" + return [self.__split_version_components(x) for x in version_components.split(".")] + + diff --git a/src/core/src/package_managers/UbuntuProClient.py b/src/core/src/package_managers/UbuntuProClient.py index 0fe82346a..5c8b3189e 100644 --- a/src/core/src/package_managers/UbuntuProClient.py +++ b/src/core/src/package_managers/UbuntuProClient.py @@ -16,6 +16,8 @@ """This is the Ubuntu Pro Client implementation""" import json + +from core.src.core_logic.VersionComparator import VersionComparator from core.src.bootstrap.Constants import Constants @@ -27,6 +29,7 @@ def __init__(self, env_layer, composite_logger): self.ubuntu_pro_client_security_status_cmd = 'pro security-status --format=json' self.security_esm_criteria_strings = ["esm-infra", "esm-apps"] self.is_ubuntu_pro_client_attached = False + self.version_comparator = VersionComparator() def install_or_update_pro(self): """install/update pro(ubuntu-advantage-tools) to the latest version""" @@ -50,10 +53,17 @@ def is_pro_working(self): is_minimum_ubuntu_pro_version_installed = False try: from uaclient.api.u.pro.version.v1 import version - from distutils.version import LooseVersion # Importing this module here as there is conflict between "distutils.version" and "uaclient.api.u.pro.version.v1.version when 'LooseVersion' is called." version_result = version() ubuntu_pro_client_version = version_result.installed_version - is_minimum_ubuntu_pro_version_installed = LooseVersion(ubuntu_pro_client_version) >= LooseVersion(Constants.UbuntuProClientSettings.MINIMUM_CLIENT_VERSION) + + # extract version from pro_client_verison 27.13.4~18.04.1 -> 27.13.4 + extracted_ubuntu_pro_client_version = self.version_comparator.extract_version_nums(ubuntu_pro_client_version) + + self.composite_logger.log_debug("Ubuntu Pro Client current version: [ClientVersion={0}]".format(str(extracted_ubuntu_pro_client_version))) + + # use custom comparator output 0 (equal), -1 (less), +1 (greater) + is_minimum_ubuntu_pro_version_installed = self.version_comparator.compare_version_nums(extracted_ubuntu_pro_client_version, Constants.UbuntuProClientSettings.MINIMUM_CLIENT_VERSION) >= 0 + if ubuntu_pro_client_version is not None and is_minimum_ubuntu_pro_version_installed: is_ubuntu_pro_client_working = True self.is_ubuntu_pro_client_attached = self.log_ubuntu_pro_client_attached() diff --git a/src/core/tests/Test_VersionComparator.py b/src/core/tests/Test_VersionComparator.py new file mode 100644 index 000000000..ff8b79724 --- /dev/null +++ b/src/core/tests/Test_VersionComparator.py @@ -0,0 +1,77 @@ +# Copyright 2024 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.7+ + +import unittest + +from core.src.core_logic.VersionComparator import VersionComparator + + +class TestVersionComparator(unittest.TestCase): + + def setUp(self): + self.version_comparator = VersionComparator() + + def test_linux_version_comparator(self): + # Test extract version logic + self.assertEqual(self.version_comparator.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25"), "1.2.25") + self.assertEqual(self.version_comparator.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25-abc"), "1.2.25") + self.assertEqual(self.version_comparator.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25+abc.123"), "1.2.25") + self.assertEqual(self.version_comparator.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25-abc+def.123"), "1.2.25") + self.assertEqual(self.version_comparator.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.21.1001"), "1.21.1001") + self.assertEqual(self.version_comparator.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.6.100"), "1.6.100") + self.assertEqual(self.version_comparator.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.6.99"), "1.6.99") + self.assertEqual(self.version_comparator.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.6."), "") + self.assertEqual(self.version_comparator.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-a.b.c"), "") + + expected_extracted_version = "27.13.4" + test_extracted_v1 = self.version_comparator.extract_version_nums("27.13.4~18.04.1") + test_extracted_v2 = self.version_comparator.extract_version_nums("27.13.4+18.04.1") + test_extracted_v3 = self.version_comparator.extract_version_nums("27.13.4-18.04.1") + + self.assertEqual(test_extracted_v1, expected_extracted_version) + self.assertEqual(test_extracted_v2, expected_extracted_version) + self.assertEqual(test_extracted_v3, expected_extracted_version) + + # Test compare versions logic + self.assertEqual(self.version_comparator.compare_version_nums(test_extracted_v1, "27.13.4"), 0) # equal + self.assertEqual(self.version_comparator.compare_version_nums(test_extracted_v2, "27.13.3"), 1) # greater + self.assertEqual(self.version_comparator.compare_version_nums(test_extracted_v3, "27.13.5"), -1) # less + + # Test sort versions logic + unsorted_path_versions = [ + "Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25-abc+def.123", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.21.1001", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.100", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.99", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.21.100", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25-abc", + ] + + expected_sorted_path_versions = [ + "Microsoft.CPlat.Core.LinuxPatchExtension-1.21.1001", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.21.100", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.100", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.99", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25-abc+def.123", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25-abc" + ] + + # valid versions + self.assertEqual(self.version_comparator.sort_versions_desc_order(unsorted_path_versions), expected_sorted_path_versions) + +if __name__ == '__main__': + unittest.main() + diff --git a/src/extension/src/ActionHandler.py b/src/extension/src/ActionHandler.py index c27918311..26e211f30 100644 --- a/src/extension/src/ActionHandler.py +++ b/src/extension/src/ActionHandler.py @@ -19,11 +19,12 @@ import os import shutil import time -from distutils.version import LooseVersion from extension.src.Constants import Constants from extension.src.EnableCommandHandler import EnableCommandHandler from extension.src.InstallCommandHandler import InstallCommandHandler +from extension.src.Utility import Utility +from extension.src.VersionComparatorHandler import VersionComparatorHandler from extension.src.local_loggers.StdOutFileMirror import StdOutFileMirror @@ -48,6 +49,7 @@ def __init__(self, logger, env_layer, telemetry_writer, utility, runtime_context self.file_logger = None self.operation_id_substitute_for_all_actions_in_telemetry = str((datetime.datetime.utcnow()).strftime(Constants.UTC_DATETIME_FORMAT)) self.seq_no = self.ext_config_settings_handler.get_seq_no_from_env_var() + self.version_comparator_handler = VersionComparatorHandler() def determine_operation(self, command): switcher = { @@ -224,8 +226,13 @@ def update(self): # identify the version preceding current self.logger.log("Fetching the extension version preceding current from all available versions...") - paths_to_all_versions.sort(reverse=True, key=LooseVersion) - preceding_version_path = paths_to_all_versions[1] + + # use custom sort logic to sort path based on version numbers + sorted_versions = self.version_comparator_handler.sort_versions_desc_order(paths_to_all_versions) + self.logger.log_debug("List of extension versions in descending order: [SortedVersion={0}]".format(sorted_versions)) + + preceding_version_path = sorted_versions[1] + if preceding_version_path is None or preceding_version_path == "" or not os.path.exists(preceding_version_path): error_msg = "Could not find path where preceding extension version artifacts are stored. Hence, cannot copy the required artifacts to the latest version. "\ "[Preceding extension version path={0}]".format(str(preceding_version_path)) diff --git a/src/extension/src/EnvLayer.py b/src/extension/src/EnvLayer.py index de85ffd60..bcae0e2b5 100644 --- a/src/extension/src/EnvLayer.py +++ b/src/extension/src/EnvLayer.py @@ -150,12 +150,12 @@ def is_tty_required_in_sudoers(self): if self.require_tty_setting not in str(setting): continue - if re.match('.*!' + self.require_tty_setting, setting): - setting_substr_without_requiretty = re.search('(.*)!' + self.require_tty_setting, setting).group(1).strip() + if re.match(r'.*!' + self.require_tty_setting, setting): + setting_substr_without_requiretty = re.search(r'(.*)!' + self.require_tty_setting, setting).group(1).strip() if self.is_tty_defaults_set(setting_substr_without_requiretty): tty_set_to_required = False else: - setting_substr_without_requiretty = re.search('(.*)' + self.require_tty_setting, setting).group(1).strip() + setting_substr_without_requiretty = re.search(r'(.*)' + self.require_tty_setting, setting).group(1).strip() if self.is_tty_defaults_set(setting_substr_without_requiretty): tty_set_to_required = True diff --git a/src/extension/src/VersionComparatorHandler.py b/src/extension/src/VersionComparatorHandler.py new file mode 100644 index 000000000..fd1190874 --- /dev/null +++ b/src/extension/src/VersionComparatorHandler.py @@ -0,0 +1,54 @@ +# Copyright 2024 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.7+ + +import re + + +class VersionComparatorHandler(object): + + def extract_version_nums(self, path): + # type (str) -> (str) + """ + Extract the version part from a given path. + Input: /var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-1.2.5/config + Return: "1.2.5" + """ + match = re.search(r'([\d]+\.[\d]+\.[\d]+)', path) + return match.group(1) if match else "" + + def sort_versions_desc_order(self, paths): + # type (list[str]) -> list[str] + """ + Sort paths based on version numbers extracted from paths. + Input: + ["Microsoft.CPlat.Core.LinuxPatchExtension-1.21.1001", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.100", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.21.100"] + Return: + ["Microsoft.CPlat.Core.LinuxPatchExtension-1.21.1001", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.21.100", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.100"] + """ + return sorted(paths, key=self.__version_key,reverse=True) + + def __version_key(self, path): + # type (str) -> (int) + """ Extract version number from input and return int tuple. + Input: "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.100" + Return: (1.6.100) + """ + version_numbers = self.extract_version_nums(path) + return tuple(map(int, version_numbers.split('.'))) if version_numbers else (0, 0, 0) diff --git a/src/extension/src/file_handlers/ExtConfigSettingsHandler.py b/src/extension/src/file_handlers/ExtConfigSettingsHandler.py index ea31bc2a6..ab0d421fd 100644 --- a/src/extension/src/file_handlers/ExtConfigSettingsHandler.py +++ b/src/extension/src/file_handlers/ExtConfigSettingsHandler.py @@ -89,7 +89,7 @@ def __get_seq_no_from_config_settings(self): for subdir, dirs, files in os.walk(self.config_folder): for file in files: try: - if re.match('^\d+' + self.file_ext + '$', file): + if re.match(r'^\d+' + self.file_ext + '$', file): cur_seq_no = int(os.path.basename(file).split('.')[0]) file_modified_time = os.path.getmtime(os.path.join(self.config_folder, file)) self.logger.log("Sequence number being considered and the corresponding file modified time. [Sequence No={0}] [Modified={1}]".format(str(cur_seq_no), str(file_modified_time))) diff --git a/src/extension/tests/Test_VersionComparatorHandler.py b/src/extension/tests/Test_VersionComparatorHandler.py new file mode 100644 index 000000000..450231d13 --- /dev/null +++ b/src/extension/tests/Test_VersionComparatorHandler.py @@ -0,0 +1,59 @@ +# Copyright 2024 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.7+ + +import unittest +from extension.src.VersionComparatorHandler import VersionComparatorHandler + + +class TestVersionComparatorHandler(unittest.TestCase): + + def setUp(self): + self.version_comparator_handler = VersionComparatorHandler() + + def test_linux_version_comparator_handler(self): + # Test extract version logic + self.assertEqual(self.version_comparator_handler.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25"), "1.2.25") + self.assertEqual(self.version_comparator_handler.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25-abc"), "1.2.25") + self.assertEqual(self.version_comparator_handler.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25+abc.123"), "1.2.25") + self.assertEqual(self.version_comparator_handler.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25-abc+def.123"), "1.2.25") + self.assertEqual(self.version_comparator_handler.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.21.1001"), "1.21.1001") + self.assertEqual(self.version_comparator_handler.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.6.100"), "1.6.100") + self.assertEqual(self.version_comparator_handler.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.6.99"), "1.6.99") + self.assertEqual(self.version_comparator_handler.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-1.6."), "") + self.assertEqual(self.version_comparator_handler.extract_version_nums("Microsoft.CPlat.Core.LinuxPatchExtension-a.b.c"), "") + + # Test sort versions logic + unsorted_path_versions = [ + "Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25-abc+def.123", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.21.1001", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.100", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.99", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.21.100", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25-abc", + ] + + expected_sorted_path_versions = [ + "Microsoft.CPlat.Core.LinuxPatchExtension-1.21.1001", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.21.100", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.100", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.6.99", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25-abc+def.123", + "Microsoft.CPlat.Core.LinuxPatchExtension-1.2.25-abc" + ] + + # valid versions + self.assertEqual(self.version_comparator_handler.sort_versions_desc_order(unsorted_path_versions), expected_sorted_path_versions) + diff --git a/src/tools/Package-All.py b/src/tools/Package-All.py index 20bd80fef..b7729dac1 100644 --- a/src/tools/Package-All.py +++ b/src/tools/Package-All.py @@ -30,8 +30,7 @@ # imports in VERY_FIRST_IMPORTS, order should be kept VERY_FIRST_IMPORTS = [ 'from __future__ import print_function\n', - 'from abc import ABCMeta, abstractmethod\n', - 'from distutils.version import LooseVersion\n'] + 'from abc import ABCMeta, abstractmethod\n'] GLOBAL_IMPORTS = set()