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
23 changes: 6 additions & 17 deletions .cqfd/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,10 @@ RUN set -x \
python3-setuptools \
python3-sphinx \
python3-pip \
openjdk-8-jre-headless \
unzip \
wget \
&& rm -rf /var/lib/apt/lists/ \
&& pip3 install --no-cache-dir sphinx-argparse

ARG sonar_version=4.7.0.2747
ARG sonar_repo=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli
RUN set -x \
&& wget -O /tmp/sonar-scanner.zip \
"${sonar_repo}/sonar-scanner-cli-${sonar_version}.zip" \
&& cd /opt \
&& unzip /tmp/sonar-scanner.zip \
&& rm -f sonar-scanner.zip
RUN ln -s "/opt/sonar-scanner-${sonar_version}" /opt/sonar-scanner
RUN echo 'sonar.host.url=http://j1.sfl.team:9000/' \
> /opt/sonar-scanner/conf/sonar-scanner.properties
COPY python-sonar.sh /usr/bin/python-sonar.sh
# Pinned so a rebuild does not pick up a new release on its own, and
# restricted to wheels so no dependency gets to run a setup script
# during the install. 0.6.0 is what the unpinned line already
# resolved to on the python3.10 of ubuntu 22.04.
&& pip3 install --no-cache-dir --only-binary :all: \
sphinx-argparse==0.6.0
4 changes: 0 additions & 4 deletions .cqfd/docker/python-sonar.sh

This file was deleted.

8 changes: 1 addition & 7 deletions .cqfdrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,11 @@
org='rte'
name='vm_manager'

flavors='check sonar check_format format flake docs'
flavors='check check_format format flake docs'

[build]
command='/usr/bin/pip install --root-user-action=ignore --prefix=. .'

[sonar]
command='/usr/bin/python-sonar.sh \
pacemaker_helper \
rbd_helper \
vm_manager'

[check]
command='pylint \
pacemaker_helper \
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,25 @@ jobs:
htmlcov/
if-no-files-found: ignore

- name: Read the project version
id: version
# The SonarCloud project compares against the previous version to
# decide what counts as new code. Every analysis so far ran without
# a version, so there was no boundary and the period fell back to
# the first analysis ever: the whole code base counted as new, and
# publishing coverage turned the main branch gate red on 2000 lines
# of pre-existing code. Feeding the scanner the version from
# pyproject.toml, the single source of truth, gives the period a
# real boundary at each release.
run: |
python - <<'PY' >> "$GITHUB_OUTPUT"
import pathlib
import tomllib

pyproject = tomllib.loads(pathlib.Path("pyproject.toml").read_text())
print("version=" + pyproject["project"]["version"])
PY

# Automatic Analysis must stay off in the SonarCloud project
# settings, otherwise SonarCloud rejects this analysis. Skipped when
# SONAR_TOKEN is unavailable, which is the case for pull requests
Expand All @@ -108,6 +127,8 @@ jobs:
uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1
env:
SONAR_HOST_URL: https://sonarcloud.io
with:
args: -Dsonar.projectVersion=${{ steps.version.outputs.version }}

- name: Install documentation dependencies
# Editable too, so this step adds the docs extra instead of
Expand Down
91 changes: 91 additions & 0 deletions tests/test_vm_manager_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright (C) 2026, RTE (http://www.rte-france.com)
# SPDX-License-Identifier: Apache-2.0

"""
Unit tests for the Flask REST API.

Every vm_manager entry point the routes call is replaced here, so nothing
touches libvirt, Ceph or Pacemaker.

In production this module is not run directly: the vmmgrapi role of
seapath/ansible serves `app` with gunicorn on a unix socket, behind an
nginx that carries the TLS, the authentication and the ACL. main() is a
debug entry point, and since the application authenticates nobody on its
own, the address it binds to is worth pinning down in a test.
"""

import pytest

from vm_manager import vm_manager_api


@pytest.fixture
def client():
"""Return a test client for the API."""
return vm_manager_api.app.test_client()


def test_main_binds_the_loopback(monkeypatch):
calls = []
monkeypatch.setattr(
vm_manager_api.app, "run", lambda **kwargs: calls.append(kwargs)
)

vm_manager_api.main()

assert calls == [{"host": "127.0.0.1"}]


def test_list_vms(client, monkeypatch):
monkeypatch.setattr(vm_manager_api.v, "list_vms", lambda: ["vm1", "vm2"])

response = client.get("/")

assert response.status_code == 200
assert response.get_json() == ["vm1", "vm2"]


def test_status(client, monkeypatch):
monkeypatch.setattr(
vm_manager_api.v, "status", lambda guest: f"{guest} is Running"
)

response = client.get("/status/guest0")

assert response.status_code == 200
assert response.get_data(as_text=True) == "guest0 is Running"


def test_stop(client, monkeypatch):
monkeypatch.setattr(
vm_manager_api.v, "stop", lambda guest: f"{guest} stopped"
)

response = client.get("/stop/guest0")

assert response.status_code == 200
assert response.get_data(as_text=True) == "guest0 stopped"


def test_start_reports_a_silent_success(client, monkeypatch):
"""A backend returning nothing is a success, not an empty answer."""
monkeypatch.setattr(vm_manager_api.v, "start", lambda guest: None)

response = client.get("/start/guest0")

assert response.status_code == 200
assert "should be OK" in response.get_data(as_text=True)


def test_start_reports_the_backend_error(client, monkeypatch):
def raise_error(guest):
raise RuntimeError(f"no such VM: {guest}")

monkeypatch.setattr(vm_manager_api.v, "start", raise_error)

response = client.get("/start/guest0")

assert response.status_code == 500
assert (
response.get_data(as_text=True) == "RuntimeError: no such VM: guest0"
)
8 changes: 7 additions & 1 deletion vm_manager/vm_manager_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ def start_vm(guest):


def main():
app.run(host="0.0.0.0")
# Loopback on purpose. In production this module is imported by the
# wsgi.py of the vmmgrapi Ansible role and served by gunicorn on a
# unix socket, behind an nginx that carries the TLS, the basic auth
# and the ACL. This entry point is for local debugging only, and
# listening on every interface would publish every route, in clear
# text and unauthenticated, around all of that.
app.run(host="127.0.0.1")


if __name__ == "__main__":
Expand Down
Loading