Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 81 additions & 19 deletions VMBackup/main/Utils/HandlerUtil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 17 additions & 7 deletions VMBackup/main/Utils/WAAgentUtil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
104 changes: 102 additions & 2 deletions VMBackup/main/WaagentLib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion VMBackup/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down