Skip to content

VMBackup: Fix Python 3.12+ compatibility for Azure Linux 4.0#2177

Open
jonathanbrenes wants to merge 3 commits into
Azure:masterfrom
jonathanbrenes:fix/vmbackup-azl4-compat
Open

VMBackup: Fix Python 3.12+ compatibility for Azure Linux 4.0#2177
jonathanbrenes wants to merge 3 commits into
Azure:masterfrom
jonathanbrenes:fix/vmbackup-azl4-compat

Conversation

@jonathanbrenes

@jonathanbrenes jonathanbrenes commented May 23, 2026

Copy link
Copy Markdown

Problem

The VMSnapshot (VMBackup) extension crashes on Azure Linux 4.0 (Python 3.14) due to four Python APIs that were removed in recent Python versions. The extension fails at import time, causing Azure Backup jobs to fail with error 400039 after hours of retries.

Removed APIs used by VMBackup

API Removed In Usage
distutils.version.LooseVersion Python 3.12 Version string comparisons
imp module Python 3.12 Fallback module loader for waagent
platform.dist() Python 3.8 Distro detection in telemetry
platform.linux_distribution() Python 3.8 Distro detection for patching

Changes

WaagentLib.py

  • Add try/except for LooseVersion import with a minimal shim class providing comparison operators (__lt__, __gt__, __eq__, __le__, __ge__) via regex-based version parsing.

Why a shim instead of packaging.version.Version? PEP 632 recommends the third-party packaging library as the replacement, but it is not suitable here: (1) it is not in the stdlib and may be absent on minimal VM installs, (2) it enforces strict PEP 440 parsing and rejects the loose version strings this codebase passes (e.g. kernel versions, agent versions), and (3) adding a new dependency to a VM extension that must run on every Linux distro from RHEL 7 (Python 2.7) through AZL4 (Python 3.14) is too risky. The inline shim is ~20 lines, zero-dependency, and drop-in compatible.

WAAgentUtil.py

  • Add try/except around the import imp fallback so it doesn't crash on Python 3.12+ when the primary importlib.util path fails.

HandlerUtil.py

  • Replace platform.dist() in get_dist_info() with /etc/os-release parsing (NAME + VERSION fields).
  • Fix invalid escape sequence: replace('\/', '/') was a no-op (invalid escape treated as literal); changed to replace('\\/', '/').

patch/__init__.py

  • Add /etc/os-release ID parsing to DistInfo() fallback for distros where platform.linux_distribution() is gone.
  • Add 'azurelinux' mapping in GetMyPatching()AzureLinuxPatching.

patch/AzureLinuxPatching.py (new)

  • Dedicated patching class for Azure Linux 4.0.
  • All binary paths set to /usr/bin/ (AZL4 uses merged /usr).
  • Uses dnf for package installation.

Testing

End-to-end Azure Backup on AZL4

Full backup job completed successfully (26 min, all phases: pre-snapshot, snapshot, post-snapshot).

Backward compatibility

All patches are no-ops on older Python versions (guarded by try/except with fallback to original imports). Verified across 15 distros (Python 2.7.5 – 3.14.3), including end-to-end Azure Backup on all Python 2 VMs:

Distro Python Result
RHEL 8.10 3.6.8 PASS
RHEL 9.7 3.9.25 PASS
Oracle Linux 9.7 3.9.25 PASS
SLES 15 SP6 3.6.15 PASS
SLES 16.0 3.13.13 PASS
Debian 12 3.11.2 PASS
Ubuntu 20.04 3.8.10 PASS
Ubuntu 22.04 3.10.12 PASS
Ubuntu 24.04 3.12.3 PASS
Azure Linux 4.0 3.14.3 PASS
CentOS 7.9 2.7.5 PASS
RHEL 7.9 2.7.5 PASS
Ubuntu 16.04 2.7.12 PASS
Debian 10 2.7.16 PASS
SLES 12 SP5 2.7.18 PASS

@jonathanbrenes
jonathanbrenes requested a review from a team as a code owner May 23, 2026 01:04
@jonathanbrenes

jonathanbrenes commented May 23, 2026

Copy link
Copy Markdown
Author

Evidence: Extension is broken on Azure Linux 4.0

Production failure

On an unpatched Azure Linux 4.0 VM (Python 3.14.3), the VMSnapshot extension cannot start at all. Every enable attempt exits with code 1, causing Azure Backup jobs to fail with error 400039 after ~9 hours of retries.

Extension log (shell.log) — 7 consecutive failures:

Thu May 21 05:04:31 PM UTC 2026- 1 returned from handle.py
Thu May 21 05:06:01 PM UTC 2026- 1 returned from handle.py
Thu May 21 08:04:37 PM UTC 2026- 1 returned from handle.py
Thu May 21 11:24:42 PM UTC 2026- 1 returned from handle.py
Fri May 22 12:49:51 AM UTC 2026- 1 returned from handle.py
Fri May 22 02:36:42 AM UTC 2026- 1 returned from handle.py
Fri May 22 03:08:32 AM UTC 2026- 1 returned from handle.py

Crash traceback — cascading ModuleNotFoundError:

File "WAAgentUtil.py", line 66
    spec.loader.exec_module(waagent)
  File "WaagentLib.py", line 59
    from distutils.version import LooseVersion
ModuleNotFoundError: No module named 'distutils'

During handling of the above exception:
  File "WAAgentUtil.py", line 69
    import imp
ModuleNotFoundError: No module named 'imp'

The import chain handle.pymounts.pyDiskUtil.pyHandlerUtil.pyWAAgentUtil.pyWaagentLib.py fails at the very first import, so no code in the extension ever executes.

All 4 removed APIs confirmed broken

Python version: 3.14.3
distutils.version.LooseVersion: FAIL - No module named 'distutils'
import imp:                     FAIL - No module named 'imp'
platform.linux_distribution():  FAIL - module 'platform' has no attribute 'linux_distribution'
platform.dist():                FAIL - module 'platform' has no attribute 'dist'

After patching — extension works

Unit tests on AZL4 VM (8/8 pass):

=== Test 1: Import chain ===
  Utils.HandlerUtil: OK
=== Test 2: DistInfo ===
  DistInfo() = ['azurelinux', '4.0']
=== Test 3: GetMyPatching ===
  Patching class = AzureLinuxPatching
=== Test 4: Binary paths ===
  cryptsetup_path = /usr/bin/cryptsetup -> EXISTS
  getenforce_path = /usr/bin/getenforce -> EXISTS
  setenforce_path = /usr/bin/setenforce -> EXISTS
=== Test 5: get_dist_info ===
  get_dist_info() = ('Azure Linux-4.0 (Cloud Variant Alpha2)', '6.18.5-...')
=== Test 7: LooseVersion shim ===
  LooseVersion: OK (comparisons work)
=== Test 8: Full handle.py import chain ===
  mounts.Mounts: OK
=== ALL TESTS PASSED ===

End-to-end backup — PASS:

Metric Unpatched Patched
Duration ~9 hours → FAILED 26 minutes
Backup size 0 MB 93 MB
Exit code 1 (all attempts) 0
Error 400039 GuestAgentSnapshotTaskStatusError None

Backward compatibility — 15 distros, all PASS

Distro Python Result
RHEL 8.10 3.6.8 PASS
RHEL 9.7 3.9.25 PASS
Oracle Linux 9.7 3.9.25 PASS
SLES 15 SP6 3.6.15 PASS
SLES 16.0 3.13.13 PASS
Debian 12 3.11.2 PASS
Ubuntu 20.04 3.8.10 PASS
Ubuntu 22.04 3.10.12 PASS
Ubuntu 24.04 3.12.3 PASS
Azure Linux 4.0 3.14.3 PASS
CentOS 7.9 2.7.5 PASS
RHEL 7.9 2.7.5 PASS
Ubuntu 16.04 2.7.12 PASS
Debian 10 2.7.16 PASS
SLES 12 SP5 2.7.18 PASS

All patches are guarded by try/except — on older Python versions where the deprecated APIs still exist, the original code path runs unchanged (zero behavior change). Python 2 VMs were validated with full end-to-end Azure Backup (baseline + patched).

The VMSnapshot extension crashes on Azure Linux 4.0 (Python 3.14) due to
four Python APIs removed in recent versions. The extension fails at import
time, causing Azure Backup jobs to fail with error 400039 after hours of
retries.

Root cause — removed Python APIs used by VMBackup:
  - distutils.version.LooseVersion (removed in 3.12)
  - imp module (removed in 3.12)
  - platform.dist() (removed in 3.8)
  - platform.linux_distribution() (removed in 3.8)

Changes:

WaagentLib.py:
  - Add try/except for LooseVersion import with a minimal shim class
    that provides comparison operators (__lt__, __gt__, __eq__, __le__,
    __ge__) using regex-based version parsing.
    Note: Python never added a stdlib replacement for LooseVersion.
    PEP 632 recommends the third-party `packaging` library, but it is
    not suitable here: (1) it is not in the stdlib and may be absent on
    minimal installs, (2) it enforces strict PEP 440 parsing and rejects
    the loose version strings this codebase passes, and (3) adding a
    dependency to a VM extension that must run on every Linux distro from
    RHEL 7 (Python 2.7) to AZL4 (Python 3.14) is fragile. The inline
    shim is zero-dependency and drop-in compatible.

WAAgentUtil.py:
  - Add try/except around the `import imp` fallback so it does not crash
    on Python 3.12+ when the primary importlib path fails

HandlerUtil.py:
  - Replace platform.dist() in get_dist_info() with /etc/os-release
    parsing (NAME + VERSION fields)
  - Fix invalid escape sequence: replace('\/', '/') was a no-op
    (invalid escape treated as literal char); changed to
    replace('\\/', '/') which correctly replaces \/ with /

patch/__init__.py:
  - Add /etc/os-release ID parsing to DistInfo() fallback
  - Add 'azurelinux' mapping in GetMyPatching() -> AzureLinuxPatching

patch/AzureLinuxPatching.py (new):
  - Dedicated patching class for Azure Linux 4.0
  - All binary paths set to /usr/bin/ (AZL4 merged /usr)
  - Uses dnf for package installation

Tested:
  - End-to-end Azure Backup on AZL4 VM: PASS (26 min, all phases)
  - Backward compat across 10 distros (Python 3.6-3.14): all PASS
    RHEL 8/9, Oracle Linux 9, SLES 15/16, Debian 12,
    Ubuntu 20.04/22.04/24.04, Azure Linux 4.0
@jonathanbrenes
jonathanbrenes force-pushed the fix/vmbackup-azl4-compat branch from 986e482 to 037ab48 Compare May 28, 2026 03:41
vupadhyay-ms pushed a commit to vupadhyay-ms/azure-linux-extensions that referenced this pull request Jul 13, 2026
Combine the strengths of both fallback shims discussed in review:

- Keep pre-release precedence (alpha < beta < rc < release) via negative
  sentinels and the [.\-_] separator split (broader than a digits-only
  tokenizer for kernel-style strings).
- Add explicit str() coercion when tuple positions have mismatched types
  so Py 3 does not raise TypeError comparing int vs str (e.g. '1.0.5' vs
  '1.0.foo'). This matches the safe pattern from PR Azure#2177.
- Add __ne__ so Py 2.7 does not fall back to identity comparison; Py 3
  auto-derives it but Py 2.7 does not.
- Add __repr__ for cleaner logs and debugging.
- Fold six near-identical rich-comparison methods into a single _cmp()
  helper and one-line delegates, halving the surface area.

No behavior change for the 4 in-tree call sites (all pass numeric plugin
version strings such as '1.0.9231.0'); shim now degrades safely on
arbitrary inputs instead of raising.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5519adfb-3554-4ee6-84ff-61ec7bab7e1a
…seVersion shim

- WaagentLib.py: guard 'import crypt' with legacycrypt/passlib fallbacks so the
  extension no longer crashes at import on Python 3.13+ (e.g. Ubuntu 26.04);
  gen_password_hash() uses whichever backend resolved, else raises a clear error.
- Harden the distutils LooseVersion fallback shim: split on '.', '-', '_';
  numeric tokens compare as ints; pre-release tokens (alpha<beta<rc<release)
  use negative sentinels; mixed-type positions coerce to str to avoid TypeError.
  Adopts the review-hardened shim (matches vupadhyay-ms a8b9ff6).
@jonathanbrenes

Copy link
Copy Markdown
Author

Update: crypt removal handling (Python 3.13+ / Ubuntu 26.04) + hardened LooseVersion shim

Pushed one commit on top of the existing Python 3.12 compatibility work.

1. crypt removal (Python 3.13+, e.g. Ubuntu 26.04)
crypt was removed from the stdlib in Python 3.13, which crashed the extension at import. WaagentLib.py now guards the import with fallbacks:

  • from crypt import crypt (Python < 3.13)
  • from legacycrypt import crypt (Python 3.13+)
  • from passlib.hash import sha512_crypt

gen_password_hash() uses whichever backend resolved, otherwise raises a clear ImportError instead of crashing at import.

2. Hardened LooseVersion fallback shim
Adopted the review-hardened shim (aligns with vupadhyay-ms a8b9ff6):

  • Splits on ., -, _ (handles kernel-style version strings)
  • Numeric tokens compare as ints; pre-release tokens (alpha < beta < rc < release) use negative sentinels
  • Mixed-type positions are coerced to str in _cmp() to avoid TypeError on Python 3
  • __ne__ (for Py 2.7) and __repr__ retained; six comparators delegate to one _cmp() helper

Validation

  • Shim unit-checked (pre-release ordering + mixed-type safety), all pass.
  • Patched code executed under native Python on distro VMs from 2.7.5 through 3.12.3 (import guard, hashing, version compare, distro/patch-class selection all pass).
  • Scope: VMBackup only.

@jonathanbrenes

Copy link
Copy Markdown
Author

Tested distros / Python versions (patched code executed on-VM under each native Python — import guard, password hashing, version compare, and distro/patch-class selection all PASS):

Distro Python Result
CentOS 7.9 2.7.5 PASS
RHEL 7.9 2.7.5 PASS
SLES 12 SP5 3.4.10 (+ 2.7.18) PASS
Ubuntu 16.04 3.5.2 PASS
RHEL 8.10 3.6.8 PASS
SLES 15 SP6 3.6.15 PASS
Ubuntu 20.04 3.8.10 PASS
Oracle Linux 9.7 3.9.25 PASS
RHEL 9.7 3.9.25 PASS
Ubuntu 22.04 3.10.12 PASS
Debian 12 3.11.2 PASS
Ubuntu 24.04 3.12.3 PASS

Not yet re-validated on-VM (Azure run-command guest handler got stuck — infra, not code): Ubuntu 26.04 (Python 3.13+, the crypt-removed case), Azure Linux 4.0 (Python 3.14), SLES 16. The crypt fallback and shim were verified locally for these; the AZL4 end-to-end backup path was validated in the original change.

@vupadhyay-ms vupadhyay-ms left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rest all looks good to me.

Comment thread VMBackup/main/WaagentLib.py Outdated
passLibImported = False

try:
from crypt import crypt as crypt

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: from crypt import crypt as crypt — the as crypt is a no-op. from crypt import crypt does the same thing.

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.

1. nit — redundant as crypt alias
Changed from crypt import crypt as cryptfrom crypt import crypt.

Comment thread VMBackup/main/WaagentLib.py Outdated
try:
from legacycrypt import crypt
cryptImported = True
except ImportError:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

non-blocking: All three fallback branches (crypt, legacycrypt, passlib) use except ImportError: pass, which swallows the reason silently. If none of the three are available and gen_password_hash is later hit, debugging becomes archaeology. Could we log once at import time (or on first call) which sources were tried and failed?

Also worth a comment somewhere that legacycrypt and passlib are not stdlib — same reasoning we've discussed elsewhere for skipping packaging.version. Not a blocker since VMBackup doesn't call gen_password_hash today, but a note prevents someone from wiring this into a real code path assuming it works on every VM.

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.

Each backend's failure reason is now captured in a cryptImportErrors list and surfaced in gen_password_hash()'s error message (deterministic "on first call" reporting — there's no reliable logger at module import in the vendored waagent). Also added a comment noting legacycrypt is not stdlib and that hashing is only available where a backend is present.

Comment thread VMBackup/main/WaagentLib.py Outdated
if cryptImported:
return crypt(password, salt)
elif passLibImported:
return sha512_crypt.hash(password)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

blocking (potential defect): the caller constructed salt = "${crypt_id}${salt}" at line 451, but the passlib branch doesn't pass it in:

elif passLibImported:
    return sha512_crypt.hash(password)   # <-- caller's salt is dropped

passlib.hash.sha512_crypt.hash(password) generates its own random salt with default rounds, so if this branch ever runs the customer gets a different hash format than the crypt and legacycrypt branches produce. That's a silent semantic drift under the customer.

Suggest either:

sha512_crypt.using(salt=<parsed_salt_chars>, rounds=<rounds>).hash(password)

(parsing the salt back out of the $id$salt string), or just dropping the passlib branch entirely if we're not willing to preserve the exact hash format.

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.

Took the "drop it" option — the passlib branch and passLibImported flag are removed; hashing is now cryptlegacycrypt, else a clear ImportError. This eliminates the format drift and the non-stdlib dependency. Updated test_waagentlib_password_hash.py accordingly (removed the obsolete passlib test).

Comment thread VMBackup/main/Utils/WAAgentUtil.py Outdated
@@ -66,10 +66,11 @@ def searchWAAgentOld():
spec.loader.exec_module(waagent)
except ImportError:

@vupadhyay-ms vupadhyay-ms Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this is exactly the shape that broke PR #2124except ImportError doesn't catch AttributeError, which is what Py versions have started raising when importlib.util.module_from_spec isn't available. A capability check is safer and immune to future exception-type churn:

import importlib.util
if hasattr(importlib.util, 'module_from_spec'):
    spec = importlib.util.spec_from_file_location('waagent', agentPath)
    waagent = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(waagent)
else:
    import imp
    waagent = imp.load_source('waagent', agentPath)

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.

Replaced the exception-based branch with hasattr(importlib.util, 'module_from_spec'). One refinement over the snippet: I also guarded the import importlib.util itself, since Python 2.7 has no importlib.util (a bare import raises ImportError there). Covers all three cases — importlib.util missing (2.6/2.7), module_from_spec missing (3.3–3.4), present (3.5+).

Comment thread VMBackup/main/Utils/HandlerUtil.py Outdated
date_string = r'\/Date(' + str((int)(time_span)) + r')\/'
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
stat_rept = stat_rept.replace('\\/', '/') # To fix the datetime format of CreationTime to be consumed by C# DateTimeOffset

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

blocking (behavior change, not a pure fix): the original .replace('\\\/', '\/') was a no-op — \/ is an invalid escape sequence and Python treated it literally, so the replacement was \/\/ (identity). The new code .replace('\\/', '/') actually strips the backslashes, so the C# DateTimeOffset consumer downstream now sees /Date(...)/ instead of \/Date(...)\/. That's a real payload change.

Could we get a sign-off from the C# side (the CreationTime consumer for this status file), or split this into its own PR so it can be audited independently? Right now it rides along inside a Python-3 compatibility PR, which makes it easy to miss during review.

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.

Good catch prompting a closer look. I tested a regression script that reproduces the exact do_status_report sequence and proved the two forms emit byte-identical output — the replace() runs before the \/Date(...)\/ token is substituted, and json.dumps doesn't emit \/ for normal content, so nothing changes on the wire. To keep this PR a pure compatibility change, I've reverted the line to the master original (verified: no diff vs master). The test stays in to guard the behavior.

Comment thread VMBackup/main/Utils/HandlerUtil.py Outdated
return distinfo[0]+"-"+distinfo[1],platform.release()
# platform.dist() removed in Python 3.8; parse /etc/os-release instead
try:
with open("/etc/os-release", "r") as f:

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 exact same /etc/os-release parsing block appears in three places:

  • here (get_dist_info)
  • WaagentLib.py DistInfo() around line 4641
  • patch/__init__.py DistInfo() around line 64

All three do the same open + partition('=') + strip('"') loop. Would you consider extracting a single read_os_release() helper (e.g., in Utils/) so future distro additions only need one change? Copy-paste tends to drift over time.

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.

Extracted a single Utils/DistroUtil.read_os_release() helper and wired all three sites (HandlerUtil.get_dist_info, patch.DistInfo, WaagentLib.DistInfo). WaagentLib uses a guarded import so the vendored waagent still runs standalone.

elif ('redhat'.lower() in Distro.lower()):
Distro = 'redhat'
elif ('azurelinux' in Distro.lower()):
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.

- WaagentLib.py: drop redundant 'as crypt'; capture per-backend import failure
  reasons (cryptImportErrors) and surface them in gen_password_hash(); remove the
  passlib branch (it self-generated a salt, dropping the caller's -> different
  hash format) so hashing is crypt -> legacycrypt else a clear ImportError.
- WAAgentUtil.py: select the module loader by capability
  (hasattr(importlib.util, 'module_from_spec')) instead of exception type;
  guard 'import importlib.util' for Python 2.7 which lacks it.
- Utils/DistroUtil.py (new): single shared read_os_release() helper.
- HandlerUtil.py, patch/__init__.py, WaagentLib.py: use the shared helper for
  /etc/os-release parsing (WaagentLib via a guarded import to stay standalone).
- HandlerUtil.py: revert the CreationTime escape line to the master original
  (proven byte-identical output; keeps this a pure compatibility change).
- Document the intentional azurelinux mapping divergence (GetMyDistro -> 'fedora'
  vs GetMyPatching -> 'AzureLinux') with comments at both sites.
@vupadhyay-ms

vupadhyay-ms commented Jul 21, 2026

Copy link
Copy Markdown

LGTM

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the VMBackup (VMSnapshot) extension to run on Azure Linux 4.0 / Python 3.12+ by removing reliance on stdlib APIs that were deprecated/removed in newer Python versions and by adding Azure Linux–specific distro/patching handling.

Changes:

  • Added compatibility shims/guards for removed Python APIs (distutils.version.LooseVersion, crypt, imp, platform.dist() / platform.linux_distribution()).
  • Introduced shared /etc/os-release parsing to replace deprecated platform distro APIs.
  • Added an Azure Linux–specific patching implementation and mapping.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
VMBackup/main/WaagentLib.py Makes crypt and LooseVersion imports resilient on newer Python; adds Azure Linux distro detection/mapping.
VMBackup/main/Utils/WAAgentUtil.py Avoids crashing on Python versions where importlib.util capabilities differ; avoids unsafe imp fallback on 3.12+.
VMBackup/main/Utils/HandlerUtil.py Replaces removed platform.dist() usage with /etc/os-release parsing for telemetry.
VMBackup/main/Utils/DistroUtil.py New shared helper for parsing /etc/os-release.
VMBackup/main/patch/AzureLinuxPatching.py New Azure Linux 4.0 patching class using merged /usr paths and dnf.
VMBackup/main/patch/init.py Adds Azure Linux distro detection via /etc/os-release and maps to AzureLinuxPatching.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +631 to +635
# 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 +6 to +9
# 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).

@arisettisanjana arisettisanjana left a comment

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.

have tested the latest changes with py3.14 version and it is working fine, LGTM

Comment thread VMBackup/main/WaagentLib.py Outdated

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.

ModuleNotFoundError: No module named 'crypt'
when running with python3.14

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants