Skip to content
Closed
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
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ exclude = build
per-file-ignores =
__init__.py:F401
vm_manager_cmd.py:F841
conftest.py:E402
47 changes: 45 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,15 @@

test:
runs-on: ubuntu-24.04
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
# SonarCloud needs the full history to blame each line. Without
# it, every line of a file the pull request touches counts as new
# code, which drags pre-existing issues into the quality gate.
fetch-depth: 0

- uses: actions/setup-python@v5
with:
Expand All @@ -51,13 +58,49 @@
sudo usermod -aG libvirt "$USER"

- name: Install package with test deps
run: pip install ".[test]"
# Editable on purpose: coverage instruments the vm_manager package in
# the checkout, so the tests must import it from there. A regular
# install copies it to site-packages, the tests import that copy, and
# coverage reports 0% on every module.
run: pip install -e ".[test]"

Check warning

Code scanning / SonarCloud

Python package manager scripts should not be executed during installation

<!--SONAR_ISSUE_KEY:AZ_DyoO8HHafFaid2WiT-->Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here. <p>See more on <a href="https://sonarcloud.io/project/issues?id=seapath_vm_manager&issues=AZ_DyoO8HHafFaid2WiT&open=AZ_DyoO8HHafFaid2WiT&pullRequest=94">SonarQube Cloud</a></p>

Check warning

Code scanning / SonarCloud

Python dependencies should be locked to verified versions

<!--SONAR_ISSUE_KEY:AZ_DyoO8HHafFaid2WiU-->Using dependencies without locking resolved versions is security-sensitive. <p>See more on <a href="https://sonarcloud.io/project/issues?id=seapath_vm_manager&issues=AZ_DyoO8HHafFaid2WiU&open=AZ_DyoO8HHafFaid2WiU&pullRequest=94">SonarQube Cloud</a></p>

Check warning

Code scanning / SonarCloud

Python package manager scripts should not be executed during installation Medium

Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here. See more on SonarQube Cloud

Check warning

Code scanning / SonarCloud

Python dependencies should be locked to verified versions Medium

Using dependencies without locking resolved versions is security-sensitive. See more on SonarQube Cloud

Check warning on line 65 in .github/workflows/ci.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here.

See more on https://sonarcloud.io/project/issues?id=seapath_vm_manager&issues=AZ_Ei9CbmaZPOCQ7Eb47&open=AZ_Ei9CbmaZPOCQ7Eb47&pullRequest=95

Check warning on line 65 in .github/workflows/ci.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Using dependencies without locking resolved versions is security-sensitive.

See more on https://sonarcloud.io/project/issues?id=seapath_vm_manager&issues=AZ_Ei9CbmaZPOCQ7Eb48&open=AZ_Ei9CbmaZPOCQ7Eb48&pullRequest=95

- name: Run tests
run: sg libvirt -c "pytest tests/ -v --tb=short --ignore=tests/test_vm_manager_cluster.py --ignore=tests/test_vm_manager_cmd_cluster.py"
run: |
sg libvirt -c "pytest tests/ -v --tb=short \
--cov --cov-report=term-missing --cov-report=xml --cov-report=html \
--ignore=tests/test_vm_manager_cluster.py \
--ignore=tests/test_vm_manager_cmd_cluster.py"

- name: Add coverage to job summary
if: always()
run: |
if [ -f .coverage ]; then
echo '## Coverage' >> "$GITHUB_STEP_SUMMARY"
python -m coverage report --format=markdown >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: |
coverage.xml
htmlcov/
if-no-files-found: ignore

# Requires Automatic Analysis to be turned off in the SonarCloud
# project settings, otherwise SonarCloud rejects the CI analysis.
# Skipped when SONAR_TOKEN is unavailable, which is the case for
# pull requests opened from a fork.
- name: SonarCloud analysis
if: env.SONAR_TOKEN != ''
uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1
env:
SONAR_HOST_URL: https://sonarcloud.io

- name: Install documentation dependencies
run: pip install ".[docs]"

Check warning

Code scanning / SonarCloud

Python dependencies should be locked to verified versions Medium

Using dependencies without locking resolved versions is security-sensitive. See more on SonarQube Cloud

Check warning

Code scanning / SonarCloud

Python package manager scripts should not be executed during installation Medium

Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here. See more on SonarQube Cloud

Check warning on line 103 in .github/workflows/ci.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Using dependencies without locking resolved versions is security-sensitive.

See more on https://sonarcloud.io/project/issues?id=seapath_vm_manager&issues=AZ_Ei9CbmaZPOCQ7Eb4-&open=AZ_Ei9CbmaZPOCQ7Eb4-&pullRequest=95

Check warning on line 103 in .github/workflows/ci.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here.

See more on https://sonarcloud.io/project/issues?id=seapath_vm_manager&issues=AZ_Ei9CbmaZPOCQ7Eb49&open=AZ_Ei9CbmaZPOCQ7Eb49&pullRequest=95

- name: Build documentation
run: sphinx-build -b html docs/ docs/_build/html
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ __pycache__/
# Ignore generated documentation
/docs/_build/

# Ignore coverage files
/.coverage
/coverage.xml
/htmlcov/

# Ignore sonar files
/.scannerwork/
/.sonar/
Expand Down
58 changes: 55 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@ cqfd -b flake # flake8
cqfd -b check # pylint
```

## SonarCloud

Project `seapath_vm_manager` in the `seapath` organisation, public:
https://sonarcloud.io/summary/overall?id=seapath_vm_manager

The analysis runs from the `test` job of `.github/workflows/ci.yml`, after
the tests, so that `coverage.xml` exists and can be imported. Scope and
report paths are in `sonar-project.properties`. Two things are easy to get
wrong here:

- **Automatic Analysis must stay disabled** in the project settings.
SonarCloud refuses a CI analysis while it is on, and Automatic Analysis
can never report coverage because it does not run the tests. The project
ran that way until 2026-08 and consequently had no coverage data at all.
- **`fetch-depth: 0` on the checkout is required.** Without full history
SonarCloud has no blame data, so every line of a file a pull request
touches counts as new code and pre-existing issues fail the new-code
quality gate.

Analysis needs the `SONAR_TOKEN` repository secret. The step is skipped
when it is absent, which is what happens for pull requests from forks.

## Documentation

```bash
Expand All @@ -58,15 +80,45 @@ sphinx-argparse requires `get_parser()` functions in both CLI modules.

## Tests

Tests are integration scripts requiring a real Ceph/Pacemaker cluster. Run individually:
The pytest suite lives in `tests/`:

```bash
pip install .[test]

# Everything (needs a real Ceph/Pacemaker cluster)
pytest tests/

# What CI runs: no cluster needed
pytest tests/ \
--ignore=tests/test_vm_manager_cluster.py \
--ignore=tests/test_vm_manager_cmd_cluster.py

# With coverage (branch coverage is on by default)
pytest tests/ --cov --cov-report=term-missing --cov-report=html
```

`tests/conftest.py` calls `install_ceph_stubs()` from `tests/ceph_stubs.py`
**before** importing vm_manager. vm_manager picks its backend at import time
(`__init__.py`), so without the stubs `cluster_mode` is False on any machine
without Ceph and all cluster-side tests are skipped, even the ones that need
no cluster. The stubs raise on use, so a test that reaches real Ceph code
fails loudly. Anything genuinely needing a cluster belongs in
`test_vm_manager_cluster.py` or `test_vm_manager_cmd_cluster.py`, which CI
ignores.

Coverage config is in `pyproject.toml` (`[tool.coverage.run]`). There is
deliberately no `fail_under` yet: the project is establishing a baseline
towards the OpenSSF gold criteria (90% statement, 80% branch).

Older standalone integration scripts, run by hand against a real cluster,
live in `vm_manager/helpers/tests/pacemaker/` and
`vm_manager/helpers/tests/rbd_manager/`:

```bash
python3 -m vm_manager.helpers.tests.rbd_manager.clone_rbd
python3 -m vm_manager.helpers.tests.pacemaker.add_vm
```

Test scripts are in `vm_manager/helpers/tests/pacemaker/` and `vm_manager/helpers/tests/rbd_manager/`.

## Architecture

**Entry points** (defined in `pyproject.toml [project.scripts]`):
Expand Down
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,35 @@ pytest tests/

### Run only standalone tests (no cluster required)

The cluster tests (`test_vm_manager_cluster.py`) require a running Pacemaker/Ceph
cluster. To run only the standalone libvirt tests, exclude that file:
The cluster tests (`test_vm_manager_cluster.py` and
`test_vm_manager_cmd_cluster.py`) require a running Pacemaker/Ceph cluster.
To run only the standalone tests, exclude those files:

```bash
pytest tests/ --ignore=tests/test_vm_manager_cluster.py
pytest tests/ \
--ignore=tests/test_vm_manager_cluster.py \
--ignore=tests/test_vm_manager_cmd_cluster.py
```

This is what the CI workflow runs. Tests that exercise the cluster code
paths without touching a cluster (argparse, XML building, command
construction) still run here: `tests/conftest.py` registers stub `rados`
and `rbd` modules when the real Ceph bindings are absent, so vm_manager
starts in cluster mode. The stubs raise as soon as they are used, so a test
that reaches real Ceph code fails rather than passing against a fake. See
`tests/ceph_stubs.py`.

### Measure coverage

```bash
pytest tests/ --cov --cov-report=term-missing --cov-report=html
```

Branch coverage is enabled by default (see `[tool.coverage.run]` in
`pyproject.toml`). The HTML report lands in `htmlcov/`. The CI workflow
publishes `coverage.xml` and `htmlcov/` as a build artifact and prints a
summary table in the job summary.

## Documentation

The HTML documentation is generated with Sphinx.
Expand Down
17 changes: 16 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,24 @@ dependencies = [
readme = "README.md"

[project.optional-dependencies]
test = ["pytest>=7.0"]
test = ["pytest>=7.0", "pytest-cov>=4.0", "coverage[toml]>=7.0"]
docs = ["sphinx>=4.0", "sphinx-argparse"]

[tool.coverage.run]
branch = true
source = ["vm_manager"]
omit = [
# Standalone integration scripts shipped with the package, driven by
# hand against a real cluster. They are tests, not product code.
"vm_manager/helpers/tests/*",
]

[tool.coverage.report]
show_missing = true
# No exclusions and no fail_under yet: this is a baseline measurement, and
# the OpenSSF gold criteria (90% statement, 80% branch) need an honest
# starting number before thresholds are worth enforcing.

[tool.setuptools]
packages = ["vm_manager", "vm_manager.helpers", "vm_manager.helpers.tests.pacemaker", "vm_manager.helpers.tests.rbd_manager"]

Expand Down
20 changes: 20 additions & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright (C) 2026 Savoir-faire Linux Inc.
# SPDX-License-Identifier: Apache-2.0

sonar.projectKey=seapath_vm_manager
sonar.organization=seapath

# Keep the whole repository in scope, as Automatic Analysis did, so the
# workflow, Dockerfile and XML analysers keep reporting. Only build output
# is excluded.
sonar.sources=.
sonar.exclusions=tests/**, docs/_build/**, htmlcov/**
sonar.tests=tests

sonar.python.version=3.8, 3.9, 3.10, 3.11, 3.12

# Written by the "Run tests" step of .github/workflows/ci.yml. Automatic
# Analysis could never provide this: it does not run the build or the
# tests, which is why SonarCloud has never had coverage data for this
# project.
sonar.python.coverage.reportPaths=coverage.xml
91 changes: 91 additions & 0 deletions tests/ceph_stubs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright (C) 2026 Savoir-faire Linux Inc.
# SPDX-License-Identifier: Apache-2.0

"""
Import-time stubs for the Ceph Python bindings.

vm_manager picks its backend when it is first imported (see
vm_manager/__init__.py): if ``rados`` and ``rbd`` are importable it exposes
the cluster API, otherwise it falls back to the libvirt-only API. Those
bindings ship with Ceph itself and are not installable from PyPI, so on a
plain CI runner ``cluster_mode`` is False and every cluster-side test is
skipped, including the ones that only exercise argparse or pure helper
functions and need no cluster at all.

Installing these stubs before vm_manager is imported makes ``cluster_mode``
True so those tests run. The stubs only satisfy the import: instantiating
one raises, so a test that reaches real Ceph code fails loudly instead of
quietly passing against a fake.

On a machine with Ceph installed the genuine bindings are found and nothing
is stubbed.
"""

import sys
import types


class _CephStub:
"""Placeholder for a Ceph binding class, unusable on purpose."""

def __init__(self, *args, **kwargs):
raise RuntimeError(
"{} is a test stub: this test reached real Ceph code, which "
"needs a live cluster. Mock the RbdManager method under test, "
"or move the test to tests/test_vm_manager_cluster.py.".format(
type(self).__name__
)
)


class Rados(_CephStub):
"""Stub for :class:`rados.Rados`."""


class RBD(_CephStub):
"""Stub for :class:`rbd.RBD`."""


class Group(_CephStub):
"""Stub for :class:`rbd.Group`."""


class Image(_CephStub):
"""Stub for :class:`rbd.Image`."""


def _make_module(name, attrs):
"""Build a stub module exposing ``attrs``.

:param name: the module name to register in :data:`sys.modules`
:param attrs: a dict of attribute name to value
:return: the newly created module
"""
module = types.ModuleType(name)
module.__doc__ = "Test stub for the Ceph '{}' bindings.".format(name)
for attr_name, value in attrs.items():
setattr(module, attr_name, value)
return module


def install_ceph_stubs():
"""Register stub ``rados`` and ``rbd`` modules if the real ones are absent.

Must be called before vm_manager is imported for the first time.

:return: True if stubs were installed, False if the real Ceph bindings
are available and were left alone
"""
try:
import rados # noqa: F401
import rbd # noqa: F401
except ModuleNotFoundError:
pass
else:
return False

sys.modules["rados"] = _make_module("rados", {"Rados": Rados})
sys.modules["rbd"] = _make_module(
"rbd", {"RBD": RBD, "Group": Group, "Image": Image}
)
return True
18 changes: 14 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
# Copyright (C) 2025, RTE (http://www.rte-france.com)
# Copyright (C) 2026 Savoir-faire Linux Inc.
# SPDX-License-Identifier: Apache-2.0

import os
import secrets
# vm_manager selects its backend when it is first imported, so the Ceph
# stubs have to be installed before any vm_manager import below. That is
# what forces the unusual import order here (see the E402 exemption for
# conftest.py in .flake8) and why this call sits at module level rather
# than in a fixture.
from ceph_stubs import install_ceph_stubs

import pytest
install_ceph_stubs()

from vm_manager.helpers.libvirt import LibVirtManager
import os # noqa: E402
import secrets # noqa: E402

import pytest # noqa: E402

from vm_manager.helpers.libvirt import LibVirtManager # noqa: E402


@pytest.fixture
Expand Down
Loading
Loading