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
64 changes: 64 additions & 0 deletions VMBackup/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# VMBackup Extension

Linux guest extension used by the **Azure Backup** service to take
application-consistent backups of Linux VMs running in Azure.

> **Note:** This extension is intended to be installed and managed by Azure
> Backup. Installing it manually outside that context is not supported.

## Deployment

The extension is deployed automatically as part of the first scheduled backup
after a VM is configured for backup. To configure a VM for backup, see:

- [Azure Portal](https://docs.microsoft.com/azure/backup/quick-backup-vm-portal)
- [Azure PowerShell](https://docs.microsoft.com/azure/backup/quick-backup-vm-powershell)
- [Azure CLI](https://docs.microsoft.com/azure/backup/quick-backup-vm-cli)

## Repository layout

```
VMBackup/
├── main/ Production source. Runs inside the target Linux VM.
├── test/ Unit tests and test helpers (not shipped in the zip).
├── references/ Vendored dependencies.
└── README.md This file.
```

## Running unit tests

Tests live under `VMBackup/test/` and are written as `unittest.TestCase`
subclasses. The test runner requires **Python 3.6 or newer**. The
extension's own runtime Python compatibility is unchanged and continues to
be validated by manual VM deployment.

Run all commands from the `VMBackup/` directory.

### Option A — `unittest` (no install required)

```sh
python -m unittest discover -s test -p "test_*.py" -v
```

### Option B — `pytest` (nicer output, optional)

```sh
pip install -r test/requirements.txt
pytest test/ -v
```

Both runners discover the same test files; pick whichever you prefer.

## Adding tests

Place a test for `main/<Pkg>/<Module>.py` at
`test/unit/<pkg>/test_<module>.py` (lowercase package directory and file name).

```
main/Utils/WAAgentUtil.py → test/unit/utils/test_waagent_util.py
```

If a test needs filesystem staging, mocks, or other module-specific
scaffolding, add a fixtures module under `test/helpers/` (e.g.
`test/helpers/waagent_fixtures.py`) rather than putting that logic into
`test/helpers/tools.py`. Keep `tools.py` for genuinely shared utilities.
7 changes: 0 additions & 7 deletions VMBackup/README.txt

This file was deleted.

6 changes: 3 additions & 3 deletions VMBackup/main/Utils/WAAgentUtil.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@
# it as a submodule of current module
#
def searchWAAgent():
agentPath = os.path.join(os.getcwd(), "main/WaagentLib.py")
agentPath = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, "WaagentLib.py")
if(os.path.isfile(agentPath)):
return agentPath
user_paths = os.environ['PYTHONPATH'].split(os.pathsep)
user_paths = os.environ.get('PYTHONPATH', '').split(os.pathsep)
for user_path in user_paths:
agentPath = os.path.join(user_path, 'waagent')
if(os.path.isfile(agentPath)):
Expand All @@ -55,7 +55,7 @@ def searchWAAgentOld():
agentPath = '/usr/sbin/waagent'
if(os.path.isfile(agentPath)):
return agentPath
user_paths = os.environ['PYTHONPATH'].split(os.pathsep)
user_paths = os.environ.get('PYTHONPATH', '').split(os.pathsep)
for user_path in user_paths:
agentPath = os.path.join(user_path, 'waagent')
if(os.path.isfile(agentPath)):
Expand Down
3 changes: 2 additions & 1 deletion VMBackup/main/handle.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,8 @@ def thread_for_log_upload():
backup_logger.commit(para_parser.logsBlobUri)

def start_daemon():
args = [os.path.join(os.getcwd(), "main/handle.sh"), "daemon"]
handle_sh_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "handle.sh")
args = [handle_sh_path, "daemon"]
#This process will start a new background process by calling
# handle.py -daemon
#to run the script and will exit itself immediatelly.
Expand Down
Empty file added VMBackup/test/__init__.py
Empty file.
Empty file.
26 changes: 26 additions & 0 deletions VMBackup/test/helpers/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Generic helpers shared by VMBackup unit tests.

This module is intentionally small and dependency-free (stdlib only). Anything
that's specific to one production module belongs in a per-feature fixtures
module under this directory, not here.
"""

import os
import sys


# Repo paths. test/helpers/tools.py -> ../.. = VMBackup/
VMBACKUP_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
MAIN_DIR = os.path.join(VMBACKUP_DIR, "main")
UTILS_DIR = os.path.join(MAIN_DIR, "Utils")


def purge_modules(*names):
"""Drop the named modules from `sys.modules`.

Useful when a test needs to force a fresh import of a module whose
module-level (import-time) code is part of the behavior under test.
"""
for name in names:
sys.modules.pop(name, None)
94 changes: 94 additions & 0 deletions VMBackup/test/helpers/waagent_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
Test fixtures for `main/Utils/WAAgentUtil.py`.

`WAAgentUtil.py` runs filesystem-dependent code at import time to locate and
load a `waagent` module. To exercise that behavior in tests, we need to
stage a fresh, minimal copy of the extension tree on disk and import the
module against it.
"""

import importlib
import os
import shutil
import sys

from test.helpers import tools


# Modules that get cached in `sys.modules` once WAAgentUtil is imported.
# Tests must drop these before re-importing so module-level code re-runs.
_CACHED_MODULES = ("waagent", "WAAgentUtil", "Utils", "Utils.WAAgentUtil")

# Minimal stand-in for `main/WaagentLib.py`. The real file is large; tests
# only need the symbols WAAgentUtil's module-level code touches.
_BUNDLED_LIB_STUB = '''
"""Test stub for WaagentLib.py."""
SOURCE = "bundled"

def RunGetOutput(cmd, chk_err=True):
return 0, ""

def AddExtensionEvent(*args, **kwargs):
pass

class WALAEventOperation:
HeartBeat = "HeartBeat"
Provision = "Provision"
Install = "Install"
UnInstall = "UnInstall"
Disable = "Disable"
Enable = "Enable"
Download = "Download"
Upgrade = "Upgrade"
Update = "Update"
'''


def stage_extension_tree(dest_dir, with_bundled_lib=True, bundled_lib_source=None):
"""Lay out a minimal extension tree under `dest_dir` and return the path
to the staged `main/` directory.

Layout produced::

dest_dir/
main/
__init__.py
WaagentLib.py (only if with_bundled_lib=True)
Utils/
__init__.py
WAAgentUtil.py (copied verbatim from the repo)

`bundled_lib_source`, when given, overrides the contents written to
`WaagentLib.py`.
"""
main_dir = os.path.join(dest_dir, "main")
utils_dir = os.path.join(main_dir, "Utils")
os.makedirs(utils_dir, exist_ok=True)
open(os.path.join(main_dir, "__init__.py"), "w").close()
open(os.path.join(utils_dir, "__init__.py"), "w").close()
shutil.copy(
os.path.join(tools.UTILS_DIR, "WAAgentUtil.py"),
os.path.join(utils_dir, "WAAgentUtil.py"),
)
if with_bundled_lib:
with open(os.path.join(main_dir, "WaagentLib.py"), "w") as f:
f.write(bundled_lib_source if bundled_lib_source is not None else _BUNDLED_LIB_STUB)
return main_dir


def import_waagent_util_from(main_dir):
"""Force a fresh import of `Utils.WAAgentUtil` against a staged tree."""
purge_module_cache()
sys.path.insert(0, main_dir)
try:
return importlib.import_module("Utils.WAAgentUtil")
finally:
try:
sys.path.remove(main_dir)
except ValueError:
pass


def purge_module_cache():
"""Drop any cached WAAgentUtil-related modules from `sys.modules`."""
tools.purge_modules(*_CACHED_MODULES)
14 changes: 14 additions & 0 deletions VMBackup/test/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Optional dependencies for running VMBackup unit tests.
#
# The tests themselves are written as unittest.TestCase subclasses and require
# nothing beyond the Python standard library. You can run them with:
#
# python -m unittest discover -s test -p "test_*.py"
#
# Installing pytest is optional. It produces nicer failure output and supports
# parallel execution, but it is not required.
#
# pip install -r test/requirements.txt
# pytest test/
#
pytest>=6.0
Empty file.
Empty file.
69 changes: 69 additions & 0 deletions VMBackup/test/unit/utils/test_waagent_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Tests for `main/Utils/WAAgentUtil.py` — module-level loading behavior."""

import os
import shutil
import sys
import tempfile
import unittest

# Make `from test.helpers import ...` resolvable when running from VMBackup root.
_HERE = os.path.dirname(os.path.abspath(__file__))
_VMBACKUP_DIR = os.path.abspath(os.path.join(_HERE, os.pardir, os.pardir, os.pardir))
if _VMBACKUP_DIR not in sys.path:
sys.path.insert(0, _VMBACKUP_DIR)

from test.helpers import waagent_fixtures # noqa: E402


class WAAgentUtilLoadTests(unittest.TestCase):
"""Verifies that WAAgentUtil reliably locates and loads the bundled
`WaagentLib.py` regardless of process CWD or `PYTHONPATH` state."""

def setUp(self):
self._original_cwd = os.getcwd()
self._tmp_root = tempfile.mkdtemp(prefix="vmbackup-test-ext-")
self._unrelated_cwd = tempfile.mkdtemp(prefix="vmbackup-test-cwd-")
# Exercise the unset case — see test_pythonpath_unset_does_not_raise.
self._saved_pythonpath = os.environ.pop("PYTHONPATH", None)

def tearDown(self):
os.chdir(self._original_cwd)
shutil.rmtree(self._tmp_root, ignore_errors=True)
shutil.rmtree(self._unrelated_cwd, ignore_errors=True)
if self._saved_pythonpath is not None:
os.environ["PYTHONPATH"] = self._saved_pythonpath
waagent_fixtures.purge_module_cache()

def test_loads_bundled_lib_when_cwd_is_extension_root(self):
"""Happy path: CWD == extension root."""
main_dir = waagent_fixtures.stage_extension_tree(self._tmp_root)
os.chdir(self._tmp_root)

mod = waagent_fixtures.import_waagent_util_from(main_dir)

self.assertEqual(getattr(mod.waagent, "SOURCE", None), "bundled")
self.assertEqual(mod.GetPathUsed(), 1)

def test_loads_bundled_lib_when_cwd_is_not_extension_root(self):
"""The bundled lib must be located via `__file__`, not `os.getcwd()`."""
main_dir = waagent_fixtures.stage_extension_tree(self._tmp_root)
os.chdir(self._unrelated_cwd)
self.assertNotEqual(os.getcwd(), self._tmp_root)

mod = waagent_fixtures.import_waagent_util_from(main_dir)

self.assertEqual(getattr(mod.waagent, "SOURCE", None), "bundled")
self.assertEqual(mod.GetPathUsed(), 1)

def test_pythonpath_unset_does_not_raise(self):
"""Module load must tolerate `PYTHONPATH` being absent from the env."""
self.assertNotIn("PYTHONPATH", os.environ)
main_dir = waagent_fixtures.stage_extension_tree(self._tmp_root)

mod = waagent_fixtures.import_waagent_util_from(main_dir)

self.assertEqual(getattr(mod.waagent, "SOURCE", None), "bundled")


if __name__ == "__main__":
unittest.main()