diff --git a/VMBackup/main/Utils/HandlerUtil.py b/VMBackup/main/Utils/HandlerUtil.py index 14915ba97..0637b1fd0 100755 --- a/VMBackup/main/Utils/HandlerUtil.py +++ b/VMBackup/main/Utils/HandlerUtil.py @@ -81,6 +81,13 @@ import Utils.Status from Utils.EventLoggerUtil import EventLogger from Utils.LogHelper import LoggingLevel, LoggingConstants, FileHelpers + +# Handle the deprecation of platform.dist() in Python 3.8+ +try: + import distro + HAS_DISTRO = True +except ImportError: + HAS_DISTRO = False from MachineIdentity import MachineIdentity import ExtensionErrorCodeHelper import traceback @@ -552,28 +559,74 @@ def get_dist_info(self): if 'NS-BSD' in platform.system(): release = re.sub('\\-.*$', '', str(platform.release())) return "NS-BSD", release - if 'linux_distribution' in dir(platform): - distinfo = list(platform.linux_distribution(full_distribution_name=0)) - # remove trailing whitespace in distro name - if(distinfo[0] == ''): - osfile= open("/etc/os-release", "r") + + # Try modern approach first (Python 3.8+ compatible) + if HAS_DISTRO: + try: + distro_name = distro.name() + distro_version = distro.version() + if distro_name and distro_version: + return distro_name + "-" + distro_version, platform.release() + except Exception as e: + self.log('Warning: distro package failed with error: %s' % str(e)) + + # Fallback to linux_distribution (deprecated in Python 3.5, removed in Python 3.8) + if hasattr(platform, 'linux_distribution'): + try: + distinfo = list(platform.linux_distribution(full_distribution_name=0)) + # remove trailing whitespace in distro name + if(distinfo[0] == ''): + osfile= open("/etc/os-release", "r") + for line in osfile: + lists=str(line).split("=") + if(lists[0]== "NAME"): + distroname = lists[1].split("\"") + if(lists[0]=="VERSION"): + distroversion = lists[1].split("\"") + osfile.close() + return distroname[1]+"-"+distroversion[1],platform.release() + distinfo[0] = distinfo[0].strip() + return distinfo[0]+"-"+distinfo[1],platform.release() + except Exception as e: + self.log('Warning: platform.linux_distribution failed with error: %s' % str(e)) + + # Fallback to platform.dist() (deprecated in Python 3.5, removed in Python 3.8+) + if hasattr(platform, 'dist'): + try: + distinfo = platform.dist() + return distinfo[0]+"-"+distinfo[1],platform.release() + except Exception as e: + self.log('Warning: platform.dist failed with error: %s' % str(e)) + + # Final fallback: try to parse /etc/os-release manually + try: + distroname = None + distroversion = None + with open("/etc/os-release", "r") as osfile: for line in osfile: - lists=str(line).split("=") - if(lists[0]== "NAME"): - distroname = lists[1].split("\"") - if(lists[0]=="VERSION"): - distroversion = lists[1].split("\"") - osfile.close() - return distroname[1]+"-"+distroversion[1],platform.release() - distinfo[0] = distinfo[0].strip() - return distinfo[0]+"-"+distinfo[1],platform.release() - else: - distinfo = platform.dist() - return distinfo[0]+"-"+distinfo[1],platform.release() + lists = str(line.strip()).split("=", 1) + if len(lists) >= 2: + key = lists[0].strip() + value = lists[1].strip().strip('"') + if key == "NAME": + distroname = value + elif key == "VERSION" or key == "VERSION_ID": + distroversion = value + + if distroname and distroversion: + return distroname + "-" + distroversion, platform.release() + elif distroname: + return distroname + "-Unknown", platform.release() + except Exception as e: + self.log('Warning: Failed to parse /etc/os-release with error: %s' % str(e)) + + # If all else fails, return unknown + return "Unknown", "Unknown" + except Exception as e: errMsg = 'Failed to retrieve the distinfo with error: %s, stack trace: %s' % (str(e), traceback.format_exc()) self.log(errMsg) - return "Unkonwn","Unkonwn" + return "Unknown","Unknown" def substat_new_entry(self,sub_status,code,name,status,formattedmessage): sub_status_obj = Utils.Status.SubstatusObj(code,name,status,formattedmessage) @@ -655,9 +708,18 @@ def do_status_report(self, operation, status, status_code, message, taskId = Non date_place_holder = 'e2794170-c93d-4178-a8da-9bc7fd91ecc0' stat_rept.timestampUTC = date_place_holder date_string = r'\/Date(' + str((int)(time_span)) + r')\/' + # Convert TopLevelStatus object to JSON array string + # Before: stat_rept is TopLevelStatus object with timestampUTC="e2794170-c93d-4178-a8da-9bc7fd91ecc0" + # After: stat_rept = '[{"version":"1.0","timestampUTC":"e2794170-c93d-4178-a8da-9bc7fd91ecc0","status":{"name":"VMSnapshotLinux",...}}]' stat_rept = "[" + json.dumps(stat_rept, cls = ComplexEncoder) + "]" - stat_rept = stat_rept.replace('\\\/', '\/') # To fix the datetime format of CreationTime to be consumed by C# DateTimeOffset + # Replace placeholder GUID with actual Microsoft JSON date format first + # Before: "timestampUTC":"e2794170-c93d-4178-a8da-9bc7fd91ecc0" + # After: "timestampUTC":"\/Date(time_span)\/" stat_rept = stat_rept.replace(date_place_holder,date_string) + # Now remove JSON-escaped forward slashes to get clean date format for C# DateTimeOffset + # Before: "timestampUTC":"\/Date(time_span)\/" + # After: "timestampUTC":"/Date(time_span)/" + stat_rept = stat_rept.replace(r'\/', '/') # To fix the datetime format of CreationTime to be consumed by C# DateTimeOffset # Add Status as sub-status for Status to be written on Status-File sub_stat = self.substat_new_entry(sub_stat,'0',stat_rept,'success',None) diff --git a/VMBackup/main/Utils/WAAgentUtil.py b/VMBackup/main/Utils/WAAgentUtil.py index fdab9bbf9..f85520ecd 100644 --- a/VMBackup/main/Utils/WAAgentUtil.py +++ b/VMBackup/main/Utils/WAAgentUtil.py @@ -17,10 +17,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +try: + # For Python 3.5 and later, use importlib + import importlib.util + has_importlib_util = True +except ImportError: + has_importlib_util = False + try: import imp as imp + has_imp = True except ImportError: - import importlib as imp + has_imp = False + +if not has_importlib_util and not has_imp: + raise ImportError("Neither importlib.util nor imp module is available") + import os import os.path @@ -58,18 +70,16 @@ def searchWAAgentOld(): # Search for the old agent path if the new one is not found agentPath = searchWAAgentOld() if agentPath: - try: + if has_importlib_util: # For Python 3.5 and later, use importlib - import importlib.util spec = importlib.util.spec_from_file_location('waagent', agentPath) waagent = importlib.util.module_from_spec(spec) spec.loader.exec_module(waagent) - except ImportError: + elif has_imp: # For Python 3.4 and earlier, use imp module - import imp waagent = imp.load_source('waagent', agentPath) - except Exception: - raise Exception("Can't load waagent.") + else: + raise Exception("No suitable import mechanism available.") else: raise Exception("Can't load new or old waagent. Agent path not found.") except Exception as e: diff --git a/VMBackup/main/WaagentLib.py b/VMBackup/main/WaagentLib.py index cfda355f1..68a010815 100644 --- a/VMBackup/main/WaagentLib.py +++ b/VMBackup/main/WaagentLib.py @@ -23,7 +23,16 @@ # http://msdn.microsoft.com/en-us/library/cc227259%28PROT.13%29.aspx # -import crypt +# TODO: Many classes, methods, and imports in this file might not be needed by VM Backup Extension +# and should be removed to reduce file size and eliminate unnecessary dependencies. +# Future cleanup should analyze actual VMBackup usage and remove unused code. + +# Note: crypt module deprecated in Python 3.13+, but gen_password_hash() is not used by VMBackup +try: + import crypt +except ImportError: + # Python 3.13+ removed crypt module, but VMBackup doesn't use password functions + crypt = None import random import base64 @@ -56,7 +65,96 @@ import json import datetime import xml.sax.saxutils -from distutils.version import LooseVersion +try: + from packaging.version import Version as LooseVersion +except ImportError: + try: + from distutils.version import LooseVersion + except ImportError: + # Fallback for environments without packaging or distutils + class LooseVersion: + """ + Custom version comparison class that implements semantic versioning. + + Examples of version comparisons that work correctly: + - LooseVersion("10.0") > LooseVersion("2.0") # True (10 > 2, not string "10.0" < "2.0") + - LooseVersion("1.10") > LooseVersion("1.2") # True (10 > 2 in minor version) + - LooseVersion("2.1.3") > LooseVersion("2.1") # True (2.1.3 > 2.1.0) + - LooseVersion("1.0-alpha") < LooseVersion("1.0") # True (pre-release < release) + - LooseVersion("1.0-beta") > LooseVersion("1.0-alpha") # True (beta > alpha) + - LooseVersion("1.0-rc") > LooseVersion("1.0-beta") # True (rc > beta) + + How parsing works: + - "2.1.3" → (2, 1, 3) + - "1.0-alpha" → (1, 0, -1000) # alpha = -1000 for correct precedence + - "1.0-beta" → (1, 0, -100) # beta = -100 + - "1.0-rc" → (1, 0, -10) # rc = -10 + - "1.0" → (1, 0) # release version (no negative suffix) + + Tuple comparison ensures: (1, 0, -1000) < (1, 0, -100) < (1, 0, -10) < (1, 0) + """ + def __init__(self, version_string): + self.version = str(version_string) + # Parse version into comparable parts + self._parsed = self._parse_version(self.version) + + def _parse_version(self, version_str): + """ + Parse version string into comparable tuple of integers and strings. + + Parsing examples: + - "2.1.3" → splits to ["2", "1", "3"] → converts to (2, 1, 3) + - "1.0-alpha" → splits to ["1", "0", "alpha"] → converts to (1, 0, -1000) + - "1.10.5-beta2" → splits to ["1", "10", "5", "beta2"] → converts to (1, 10, 5, "beta2") + """ + import re + # Split by dots, hyphens, and underscores + parts = re.split(r'[.\-_]', version_str.lower()) + parsed = [] + for part in parts: + # Try to convert to int, otherwise keep as string + try: + parsed.append(int(part)) + except ValueError: + # Handle pre-release identifiers with negative values for correct precedence + # This ensures: alpha < beta < rc < release + if part in ('alpha', 'a'): + parsed.append(-1000) # Lowest precedence + elif part in ('beta', 'b'): + parsed.append(-100) # Medium precedence + elif part in ('rc', 'pre'): + parsed.append(-10) # High precedence (but still < release) + else: + parsed.append(part) # Keep as string for mixed alphanumeric + return tuple(parsed) + + def __str__(self): + return self.version + + def __eq__(self, other): + if isinstance(other, LooseVersion): + return self._parsed == other._parsed + return self._parsed == LooseVersion(other)._parsed + + def __lt__(self, other): + if isinstance(other, LooseVersion): + return self._parsed < other._parsed + return self._parsed < LooseVersion(other)._parsed + + def __le__(self, other): + if isinstance(other, LooseVersion): + return self._parsed <= other._parsed + return self._parsed <= LooseVersion(other)._parsed + + def __gt__(self, other): + if isinstance(other, LooseVersion): + return self._parsed > other._parsed + return self._parsed > LooseVersion(other)._parsed + + def __ge__(self, other): + if isinstance(other, LooseVersion): + return self._parsed >= other._parsed + return self._parsed >= LooseVersion(other)._parsed if not hasattr(subprocess, 'check_output'): def check_output(*popenargs, **kwargs): @@ -374,6 +472,8 @@ def chpasswd(self, username, password, crypt_id=6, salt_len=10): return "Failed to set password for {0}: {1}".format(username, output) def gen_password_hash(self, password, crypt_id, salt_len): + if crypt is None: + raise ImportError("crypt module not available (Python 3.13+). This function is not used by VMBackup.") collection = string.ascii_letters + string.digits salt = ''.join(random.choice(collection) for _ in range(salt_len)) salt = "${0}${1}".format(crypt_id, salt) diff --git a/VMBackup/setup.py b/VMBackup/setup.py index 404c6d2f5..bd047d685 100755 --- a/VMBackup/setup.py +++ b/VMBackup/setup.py @@ -28,7 +28,10 @@ # To upload: # python setup.py sdist upload -from distutils.core import setup +try: + from setuptools import setup +except ImportError: + from distutils.core import setup import os import shutil import tempfile