Skip to content
Open
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
26 changes: 26 additions & 0 deletions VMBackup/main/Utils/DistroUtil.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Shared /etc/os-release parsing for VMBackup.
#
# platform.dist() / platform.linux_distribution() were removed in Python 3.8,
# so distro detection now reads /etc/os-release directly. This helper is the
# single source of truth for that parsing (previously copy-pasted in
# HandlerUtil.get_dist_info(), WaagentLib.DistInfo(), and patch/__init__.py).
Comment on lines +6 to +9

def read_os_release(path="/etc/os-release"):
"""Parse /etc/os-release into a dict of KEY -> value.

Values have surrounding double quotes stripped. Returns an empty dict if the
file is missing or unreadable (callers fall back to their defaults).
"""
data = {}
try:
with open(path, "r") as f:
for line in f:
key, sep, value = line.strip().partition("=")
if sep and key:
data[key] = value.strip('"')
except Exception:
pass
return data
8 changes: 6 additions & 2 deletions VMBackup/main/Utils/HandlerUtil.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
import Utils.Status
from Utils.EventLoggerUtil import EventLogger
from Utils.LogHelper import LoggingLevel, LoggingConstants, FileHelpers
from Utils.DistroUtil import read_os_release
from MachineIdentity import MachineIdentity
import ExtensionErrorCodeHelper
import traceback
Expand Down Expand Up @@ -627,8 +628,11 @@ def get_dist_info(self):
distinfo[0] = distinfo[0].strip()
return distinfo[0]+"-"+distinfo[1],platform.release()
else:
distinfo = platform.dist()
return distinfo[0]+"-"+distinfo[1],platform.release()
# platform.dist() removed in Python 3.8; parse /etc/os-release instead
osr = read_os_release()
if osr:
return osr.get("NAME", "Unknown") + "-" + osr.get("VERSION", "Unknown"), platform.release()
return "Unknown", "Unknown"
Comment on lines +631 to +635
except Exception as e:
errMsg = 'Failed to retrieve the distinfo with error: %s, stack trace: %s' % (str(e), traceback.format_exc())
self.log(errMsg)
Expand Down
23 changes: 17 additions & 6 deletions VMBackup/main/Utils/WAAgentUtil.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,29 @@ def searchWAAgentOld():
# Search for the old agent path if the new one is not found
agentPath = searchWAAgentOld()
if agentPath:
# Choose the loader by capability, not by exception type. Previously this
# relied on `except ImportError`, but on Python 3.3-3.4 the missing
# `importlib.util.module_from_spec` raises AttributeError (not ImportError),
# which escaped the handler and crashed the extension (broke PR #2124).
# `imp` was also removed in Python 3.12, so a blind fallback is unsafe.
try:
# For Python 3.5 and later, use importlib
import importlib.util
useImportlib = hasattr(importlib.util, 'module_from_spec')
except ImportError:
# Python 2.6 / 2.7 have no importlib.util
useImportlib = False

if useImportlib:
# Python 3.5+
spec = importlib.util.spec_from_file_location('waagent', agentPath)
waagent = importlib.util.module_from_spec(spec)
spec.loader.exec_module(waagent)
except ImportError:
# For Python 3.4 and earlier, use imp module
spec.loader.exec_module(waagent)
else:
# Python 2.6 - 3.4. 'imp' was removed in 3.12, but every version that
# reaches this branch still has 'imp', because 3.5+ always provides
# importlib.util.module_from_spec and takes the branch above.
import imp
waagent = imp.load_source('waagent', agentPath)
except Exception:
raise Exception("Can't load waagent.")
else:
raise Exception("Can't load new or old waagent. Agent path not found.")
except Exception as e:
Expand Down
101 changes: 98 additions & 3 deletions VMBackup/main/WaagentLib.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
# http://msdn.microsoft.com/en-us/library/cc227259%28PROT.13%29.aspx
#

import crypt
import random
import base64

Expand Down Expand Up @@ -56,7 +55,80 @@
import json
import datetime
import xml.sax.saxutils
from distutils.version import LooseVersion

# 'crypt' was deprecated in Python 3.11 (PEP 594) and removed in 3.13. VMBackup
# does not call gen_password_hash(), so make the import optional and fall back to
# the third-party 'legacycrypt' backend on 3.13+ when it is present.
# Note: 'legacycrypt' is NOT stdlib; hashing only works where it (or 'crypt') is
# available, which is why gen_password_hash() raises a clear error otherwise.
# We record why each backend was unavailable so the failure is diagnosable
# (surfaced in gen_password_hash()) instead of being swallowed silently.
cryptImported = False
cryptImportErrors = []

try:
from crypt import crypt
cryptImported = True
except ImportError as e:
cryptImportErrors.append("crypt (stdlib, removed in Python 3.13): %r" % e)

if cryptImported == False:
if (sys.version_info[0] == 3 and sys.version_info[1] >= 13) or (sys.version_info[0] > 3):
try:
from legacycrypt import crypt

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i remember testing it out with the legacycrypt, i have seen an error with 3.14 python version, can you confirm that it is been tested in ubuntu26

cryptImported = True
except ImportError as e:
cryptImportErrors.append("legacycrypt (non-stdlib): %r" % e)

try:
from distutils.version import LooseVersion
except ImportError:
# distutils removed in Python 3.12; minimal shim for version comparisons.
# Splits on '.', '-', '_'; numeric tokens compare as ints; pre-release
# tokens (alpha < beta < rc < release) get negative sentinels; on Py 3
# mixed-type positions are coerced to str in _cmp to avoid TypeError.
import re as _re
class LooseVersion(object):
_PRERELEASE = {
'alpha': -1000, 'a': -1000,
'beta': -100, 'b': -100,
'rc': -10, 'pre': -10,
}
def __init__(self, vstring):
self.vstring = str(vstring)
self._cmp_key = self._parse(self.vstring)
def _parse(self, s):
parts = []
for part in _re.split(r'[.\-_]', s.lower()):
try:
parts.append(int(part))
except ValueError:
parts.append(self._PRERELEASE.get(part, part))
return parts
def __str__(self):
return self.vstring
def __repr__(self):
return 'LooseVersion(%r)' % self.vstring
def _cmp(self, other):
if not isinstance(other, LooseVersion):
other = LooseVersion(other)
a, b = self._cmp_key, other._cmp_key
for i in range(max(len(a), len(b))):
ai = a[i] if i < len(a) else 0
bi = b[i] if i < len(b) else 0
if type(ai) != type(bi):
ai, bi = str(ai), str(bi)
if ai < bi:
return -1
if ai > bi:
return 1
return 0
def __lt__(self, other): return self._cmp(other) < 0
def __le__(self, other): return self._cmp(other) <= 0
def __eq__(self, other): return self._cmp(other) == 0
def __ge__(self, other): return self._cmp(other) >= 0
def __gt__(self, other): return self._cmp(other) > 0
def __ne__(self, other): return self._cmp(other) != 0

if not hasattr(subprocess, 'check_output'):
def check_output(*popenargs, **kwargs):
Expand Down Expand Up @@ -377,7 +449,14 @@ def gen_password_hash(self, password, crypt_id, salt_len):
collection = string.ascii_letters + string.digits
salt = ''.join(random.choice(collection) for _ in range(salt_len))
salt = "${0}${1}".format(crypt_id, salt)
return crypt.crypt(password, salt)

if cryptImported:
return crypt(password, salt)
raise ImportError(
"Password hashing is unavailable: the 'crypt' stdlib module was removed "
"in Python 3.13 and no 'legacycrypt' fallback is installed. Tried: "
+ ("; ".join(cryptImportErrors) if cryptImportErrors else "none")
)

def load_ata_piix(self):
return WaAgent.TryLoadAtapiix()
Expand Down Expand Up @@ -4514,6 +4593,12 @@ def GetMyDistro(dist_class_name=''):
Distro = 'oracle'
elif ('redhat'.lower() in Distro.lower()):
Distro = 'redhat'
elif ('azurelinux' in Distro.lower()):
# Azure Linux is RPM/Fedora-family for waagent's purposes; reuse the
# existing 'fedoraDistro' class. NOTE: patch-class selection instead
# maps azurelinux -> 'AzureLinux' (dedicated AzureLinuxPatching) in
# patch/__init__.py -- the two mappings differ intentionally.
Distro = 'fedora'
elif ('Kali'.lower() in Distro.lower()):
Distro = 'Kali'
elif ('FreeBSD'.lower() in Distro.lower() or 'gaia'.lower() in Distro.lower() or 'panos'.lower() in Distro.lower()):
Expand Down Expand Up @@ -4555,6 +4640,16 @@ def DistInfo(fullname=0):
return distinfo
if 'Linux' in platform.system():
distinfo = ["Default"]
# linux_distribution removed in Py 3.8; detect Azure Linux via os-release.
# Use the shared helper when importable (vendored waagent may run
# standalone, so fall through gracefully if Utils is unavailable).
try:
from Utils.DistroUtil import read_os_release
osr = read_os_release()
if osr.get("ID") == "azurelinux":
return ["azurelinux", osr.get("VERSION_ID", "")]
except Exception:
pass
if "ubuntu" in platform.version().lower():
distinfo[0] = "Ubuntu"
elif 'suse' in platform.version().lower():
Expand Down
49 changes: 49 additions & 0 deletions VMBackup/main/patch/AzureLinuxPatching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/python
#
# Copyright 2015 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.


import subprocess
from patch.AbstractPatching import AbstractPatching
from common import *


class AzureLinuxPatching(AbstractPatching):
def __init__(self,logger,distro_info):
super(AzureLinuxPatching,self).__init__(distro_info)
self.logger = logger
self.base64_path = '/usr/bin/base64'
self.bash_path = '/usr/bin/bash'
self.blkid_path = '/usr/bin/blkid'
self.cat_path = '/usr/bin/cat'
self.cryptsetup_path = '/usr/bin/cryptsetup'
self.dd_path = '/usr/bin/dd'
self.e2fsck_path = '/usr/bin/e2fsck'
self.echo_path = '/usr/bin/echo'
self.getenforce_path = '/usr/bin/getenforce'
self.setenforce_path = '/usr/bin/setenforce'
self.lsblk_path = '/usr/bin/lsblk'
self.usr_flag = 1
self.lsscsi_path = '/usr/bin/lsscsi'
self.mkdir_path = '/usr/bin/mkdir'
self.mount_path = '/usr/bin/mount'
self.openssl_path = '/usr/bin/openssl'
self.resize2fs_path = '/usr/bin/resize2fs'
self.umount_path = '/usr/bin/umount'

def install_extras(self):
common_extras = ['cryptsetup','lsscsi']
for extra in common_extras:
self.logger.log("installation for " + extra + ' result is ' + str(subprocess.call(['dnf', 'install','-y', extra])))
13 changes: 13 additions & 0 deletions VMBackup/main/patch/__init__.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
from patch.DefaultPatching import DefaultPatching
from patch.FreeBSDPatching import FreeBSDPatching
from patch.NSBSDPatching import NSBSDPatching
from patch.AzureLinuxPatching import AzureLinuxPatching

from Utils.DistroUtil import read_os_release

# Define the function in case waagent(<2.0.4) doesn't have DistInfo()
def DistInfo():
Expand Down Expand Up @@ -60,6 +63,10 @@ def DistInfo():
return distinfo
if 'Linux' in platform.system():
distinfo = ["Default"]
# On Python 3.8+ linux_distribution is removed; detect via /etc/os-release
osr = read_os_release()
if osr.get("ID") == "azurelinux":
return ["azurelinux", osr.get("VERSION_ID", "")]
if "ubuntu" in platform.version().lower():
distinfo[0] = "Ubuntu"
elif 'suse' in platform.version().lower():
Expand Down Expand Up @@ -114,6 +121,12 @@ def GetMyPatching(logger):
Distro = 'oracle'
elif ('redhat'.lower() in Distro.lower()):
Distro = 'redhat'
elif ('azurelinux' in Distro.lower()):
# Select the dedicated AzureLinuxPatching class (see
# patch/AzureLinuxPatching.py). NOTE: WaagentLib.GetMyDistro maps
# azurelinux -> 'fedora' for waagent's distro classes; the two
# mappings differ intentionally.
Distro = 'AzureLinux'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the AzureLinux distro string is set to two different values in two code paths:

  • WaagentLib.GetMyDistro (line 4598) maps azurelinux'fedora'
  • here in GetMyPatching maps azurelinux'AzureLinux'

Is that intentional (patcher class name vs distro-string consumers), or should they match? Either way a one-line comment explaining the divergence would help future readers — right now it looks like a bug even if it isn't.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's intentional, and I added comments at both sites saying so. I also validated it on a real Azure Linux 4.0 VM (Python 3.14.3) with a small script — 8/8 checks pass: patch.DistInfo()/WaagentLib.DistInfo() = ['azurelinux','4.0'], GetMyPatching()AzureLinuxPatching, GetMyDistro()fedoraDistro. So the patch layer uses the dedicated class while the waagent distro layer reuses the Fedora family, exactly as intended.

elif ("Kali".lower() in Distro.lower()):
Distro = 'Kali'
elif ('FreeBSD'.lower() in Distro.lower() or 'gaia'.lower() in Distro.lower() or 'panos'.lower() in Distro.lower()):
Expand Down