E2E Proxmox Service Matrix¶
The proxbox-api E2E suite runs every Docker-backed cell against a dedicated
proxmox-sdk mock container. The mock is split by Proxmox service
(pve, pbs, pdm) and CI fans the suite across all three so a single push
exercises every supported service stub in parallel.
For the surrounding workflow layout — CI jobs, dependency modes, the staged TestPyPI/PyPI publication lanes, and the Docker image variants — see CI and E2E Workflows.
Why a Service Axis¶
proxmox-sdk publishes three service-specific image tags from
emersonfelipesp/proxmox-sdk:
| Service | Image tag | Coverage |
|---|---|---|
pve |
emersonfelipesp/proxmox-sdk:latest-pve |
Full Proxmox VE OpenAPI surface (646 endpoints). Drives the historical sync pipeline. |
pbs |
emersonfelipesp/proxmox-sdk:latest-pbs |
Proxmox Backup Server stub. /health (generic) + / (service identifier) only; PVE-shaped routes are intentionally absent. |
pdm |
emersonfelipesp/proxmox-sdk:latest-pdm |
Proxmox Datacenter Manager stub. Same shape as PBS today. |
Running the same backend, the same NetBox container, and the same fixtures against all three tags is the cheapest way to catch a regression that would break a real PBS or PDM connection — even before the upstream OpenAPI surface is fully generated.
Matrix Shape¶
ci.yml¶
The CI matrix is generated dynamically by the setup job. The generator emits
the cross-product of:
| Axis | Source | Default |
|---|---|---|
base (transport bundle) |
Hard-coded list in setup.gen |
7 transport combinations covering http_manage, https_nginx, https_granian, and IPv6 dual-stack |
netbox_proxbox_mode |
INPUT_NETBOX_PROXBOX_MODE (workflow input) and the event type |
dev for push/PR, [dev, pypi] on release |
netbox_version |
.github/netbox-versions.json |
Currently three certified NetBox tags |
proxmox_service |
Hard-coded ["pve", "pbs", "pdm"] |
All three services on every run |
The full cross-product is therefore 7 (transport) × 1–2 (mode) × 3 (NetBox) × 3 (service) = 63–126 cells.
Each cell uses fail-fast: false, so an unrelated cell failure does not abort
the rest of the run.
The image tag is rendered into the runner environment from the matrix and used
both for docker pull and for the live proxmox-e2e-mock container:
env:
PROXMOX_OPENAPI_IMAGE: emersonfelipesp/proxmox-sdk:latest-${{ matrix.proxmox_service }}
PROXMOX_SERVICE: ${{ matrix.proxmox_service }}
publish-testpypi.yml¶
The pre- and post-publish E2E jobs pin the same axis statically with
proxmox_service: [pve, pbs, pdm] so the published artifacts (TestPyPI dist,
Docker Hub image) are validated against every service stub before the release
is finalized.
Architecture¶
Every cell stands up the same physical layout on its runner. The only difference
between cells is which proxmox-sdk tag is loaded into proxmox-e2e-mock.
flowchart LR
A[GitHub Actions Runner]
subgraph D[Docker network: proxbox-e2e]
NB[NetBox container<br>netbox-proxbox installed]
NGINX[Optional HTTPS nginx]
API[proxbox-api container<br>raw / nginx / granian target]
PM[proxmox-sdk mock<br>tag latest-pve / latest-pbs / latest-pdm]
PG[(PostgreSQL)]
RD[(Redis)]
end
A --> NB
A --> API
A --> PM
NB --> PG
NB --> RD
API -->|Proxmox-shaped reads| PM
API -->|NetBox REST writes| NB
NGINX --> NB
Fixture Layer¶
The Python fixture layer lives in proxbox_api/e2e/fixtures/proxmox_sdk_mock.py
and is the single place that decides "is this run PVE-shaped, or a service stub
that has no VM/cluster data?".
| Helper | Responsibility |
|---|---|
_resolve_proxmox_service(service="pve") |
Env-aware resolver. Reads PROXMOX_SERVICE when the caller did not pass an explicit service, lower-cases the value, and falls back to pve. |
_empty_cluster(name, service) |
Returns a service-labeled cluster with no nodes/VMs. Used for PBS and PDM cells. |
create_minimal_cluster(prefix, service="pve") |
One node, two VMs (QEMU + LXC) on PVE; an empty shell on non-PVE. |
create_multi_cluster(prefix, service="pve") |
Two PVE clusters with multiple nodes/VMs; a single empty shell on non-PVE. |
create_cluster_with_backups(prefix, service="pve") |
PVE cluster + backup metadata; on non-PVE returns the empty cluster and an empty backup list. |
create_custom_cluster(name, nodes_spec, vms_spec, prefix, service="pve") |
PVE custom topology; an empty shell on non-PVE. |
Tests do not need to know which service is loaded — they ask for a fixture, and the fixture either returns realistic PVE state or a labeled empty cluster. What changes between services is the test selection, not the fixture surface.
Skip Policy¶
tests/e2e/conftest.py exposes two session-scoped fixtures that own service
routing:
@pytest.fixture(scope="session")
def proxmox_service() -> str:
return (os.environ.get("PROXMOX_SERVICE", "pve").strip().lower() or "pve")
@pytest.fixture(scope="session")
def requires_pve_schema(proxmox_service: str) -> None:
if proxmox_service != "pve":
pytest.skip(f"requires PVE schema; service={proxmox_service}")
Test modules that drive the full PVE sync pipeline declare the fixture requirement at module level:
This applies today in:
tests/e2e/test_backups_sync.pytests/e2e/test_devices_sync.pytests/e2e/test_vm_sync.py
When PROXMOX_SERVICE is pbs or pdm those entire modules are auto-skipped
with a visible reason in CI logs. The minimal_cluster / multi_cluster /
mock_proxmox_session fixtures still build their labeled empty cluster, so no
fixture import explodes when a stub is loaded.
Service Smoke Test¶
A dedicated module verifies that the right service tag is actually mounted.
tests/e2e/test_proxmox_mock_health.py runs on PBS and PDM cells only. It
asserts /health is reachable (generic readiness probe) and that the mock's
root / payload identifies the loaded service stub:
@pytest.mark.asyncio(loop_scope="session")
@pytest.mark.mock_http
async def test_pbs_pdm_mock_root_reports_loaded_service(proxmox_service: str):
if proxmox_service == "pve":
pytest.skip("PBS/PDM service smoke only")
base_url = os.environ.get(
"PROXMOX_MOCK_PUBLISHED_URL", "http://localhost:8006"
).rstrip("/")
async with httpx.AsyncClient(base_url=base_url, timeout=10.0) as client:
health = await client.get("/health")
root = await client.get("/")
assert health.status_code == 200
assert root.status_code == 200
assert proxmox_service in root.text.lower()
This is the test that would catch the wrong image tag being pulled, or a
service stub regressing the service field in its root payload. (/health
itself is intentionally generic across all proxmox-sdk mock variants.)
Pytest Markers and CI Wiring¶
Two markers gate the Docker-backed E2E layer:
mock_http— runs against the realproxmox-sdkcontainer on theproxbox-e2eDocker network. Every cell runs this layer.mock_backend— runs against the in-processMockBackend. Only used as a separate pass.
The CI workflow runs them in two steps inside each cell:
- name: Run E2E tests (Docker proxmox mock)
env:
PROXMOX_SERVICE: ${{ matrix.proxmox_service }}
run: uv run pytest tests/e2e/ -m "mock_http" --tb=short -v
- name: Run E2E tests with in-process MockBackend
if: github.ref == 'refs/heads/main' && matrix.proxmox_service == 'pve'
env:
PROXMOX_SERVICE: ${{ matrix.proxmox_service }}
run: uv run pytest tests/e2e/ -m "mock_backend" --tb=short -v
The mock_backend step is gated to main and to pve. The in-process
backend reuses the PVE-shaped fixtures and is not meant to validate the service
container itself, so running it for pbs / pdm would add no signal.
What Each Cell Verifies¶
| Verification | pve |
pbs |
pdm |
|---|---|---|---|
Stack readiness (NetBox API, proxbox-api API, proxmox-sdk /openapi.json) |
yes | yes | yes |
Mock root / reports the loaded service identifier |
(smoke skipped) | yes | yes |
auth/register-key + netbox/endpoint + netbox/status smoke |
yes | yes | yes |
extras/custom-fields/create smoke |
yes | yes | yes |
test_backups_sync.py (requires_pve_schema) |
yes | skip | skip |
test_devices_sync.py (requires_pve_schema) |
yes | skip | skip |
test_vm_sync.py (requires_pve_schema) |
yes | skip | skip |
In-process mock_backend pass (main branch only) |
yes | skip | skip |
PVE cells therefore continue to certify the full sync pipeline, while PBS and PDM cells certify that the plugin, the backend, NetBox, and the mock can coexist and reach each other under those service tags.
Running a Single Cell Locally¶
# Pick the service you want to reproduce
export PROXMOX_SERVICE=pbs
# Pull the matching mock and start it on the standard port
docker pull "emersonfelipesp/proxmox-sdk:latest-${PROXMOX_SERVICE}"
docker run -d --name proxmox-e2e-mock -p 8006:8000 \
-e PROXMOX_API_MODE=mock \
"emersonfelipesp/proxmox-sdk:latest-${PROXMOX_SERVICE}" \
sh -c 'exec uvicorn ${APP_MODULE} --host 0.0.0.0 --port 8000'
# Run the E2E suite the way CI runs it
export PROXBOX_E2E_NETBOX_URL=http://127.0.0.1:8000
export PROXBOX_E2E_NETBOX_TOKEN=<your local NetBox token>
export PROXMOX_MOCK_PUBLISHED_URL=http://localhost:8006
uv run pytest tests/e2e/ -m "mock_http" --tb=short -v
On pbs or pdm the requires_pve_schema-gated modules report
SKIPPED [requires PVE schema; service=...] and the
test_proxmox_mock_health.py smoke verifies the running container actually
exposes the requested service tag.
When To Edit This Page¶
- A new
proxmox_servicevalue is added (or one is removed) — update the image tag table and the per-cell verification table. - A new fixture helper gains a
serviceparameter — list it under Fixture Layer. - A new test module either requires or stops requiring PVE schema — update Skip Policy.
- The pytest marker gating changes — update Pytest Markers and CI Wiring.
Keep docs/pt-BR/development/e2e-proxmox-service-matrix.md in sync with this
file when content changes.