Checks
Documents the check implementations. For the operator view of CloudVision validation — credentials, workspace tracking, and submission — see CloudVision Validation.
Overview
A check is a Python routine that Infrahub runs during proposed-change validation. Unlike a transform, it produces no artifact: it reports success, informational messages, or errors, and an error blocks the proposed change.
Checks live in checks/ and are registered under check_definitions: in .infrahub.yml:
check_definitions:
- name: cv-config-validation
file_path: "./checks/cv_config_check.py"
class_name: CVConfigValidationCheck
targets: fabrics
parameters:
name: name__value
targets is a group, exactly as for generators and artifact definitions — cv-config-validation
runs once per member of the fabrics group, with the fabric name passed as the name parameter.
cv-config-validation
Class: CVConfigValidationCheck
Source: checks/cv_config_check.py
Query: checks/cv_config_check.gql (registered as cv_config_check)
Target: NetworkFabric (group fabrics)
Timeout: 600 seconds
The check deploys each device's rendered EOS configuration into a CloudVision workspace and asks CloudVision to build it, so a reviewer sees CloudVision's own verdict on the configuration before the branch merges.
class CVConfigValidationCheck(InfrahubCheck):
query = "cv_config_check"
timeout = 600
async def validate(self, data: dict[str, Any]) -> None:
parsed = CVConfigCheckQuery(**_normalize_optional_relationships(data))
...
The flow, in order:
- Resolve the target fabric from the query response. A fabric with
cloudvision_managed = falseskips everything that follows. - Read CloudVision credentials from the task-worker environment (
get_cloudvision_config()), and authenticate. - Require every confirmed device in the fabric to have a serial number and to exist — and be active — in CloudVision inventory.
- Select the devices that have a stored
AvdStructuredConfigFile, download each one, and render EOS CLI withpyavd.get_device_config(). - Deploy the configs to a deterministic workspace for this proposed change and fabric, and request a build.
- Record the workspace as a
CloudvisionWorkspaceobject and post its URL to a proposed-change thread.
Download, JSON-decode, render, connection, deployment, and build failures are all reported as errors and block the proposed change. The behavioural detail — eligibility rules, workspace naming and reuse, thread comments, and what happens when the tracking schema is absent — is documented on the CloudVision Validation page rather than duplicated here.
Optional relationships and the generated query model
_normalize_optional_relationships() runs before the generated Pydantic model parses the response.
GraphQL omits nullable relationship selections entirely rather than returning null, so a device
with no pod or no avd_artifact arrives with the key missing. The helper fills those keys with
{"node": None} so an absent relationship parses as absent membership instead of failing
validation.
Supporting modules
| Module | Holds |
|---|---|
checks/cv_helpers.py | Credential loading from the environment, deterministic workspace ID/name/description/URL derivation, proposed-change context lookup, workspace rollback |
checks/cv_workspace_lifecycle.py | Workspace threads and comments, and the two submission entry points below |
The two submission entry points in cv_workspace_lifecycle.py:
submit_linked_workspace_for_custom_webhook()— the CustomWebhook entry point. It extracts the proposed-change ID and branch from the event, ignores events that name a check other thancv-config-validation, and delegates to the shared handler.submit_linked_workspace_for_proposed_change()— the shared handler. It resolvesCloudvisionWorkspaceobjects by proposed-change ID and submits only when exactly one linked workspace exists in a submit-ready state (builtorsubmit_failed).
The manual retry path for the same handler is an invoke task:
uv run invoke submit-cv-workspace --proposed-change-id <proposed-change-id> --branch main
The webhook payload transform
cv_workspace_submission_webhook_payload (CVWorkspaceSubmissionWebhookPayload, in
transforms/cv_workspace_submission_webhook.py) renders the CustomWebhook body. It is a transform
rather than a check, but it belongs to this pipeline: it returns the check name, the proposed-change
ID, and one entry per linked workspace with its ID, status, URL, and fabric name. See
Transforms.
fabric-pool-validation
Class: FabricPoolValidationCheck
Source: checks/fabric_pool_check.py
Query: checks/fabric_pool_check.gql (registered as fabric_pool_check)
Target: NetworkFabric (group fabrics)
The check validates the role-driven pool collections the generators read — NetworkFabric.fabric_ip_pools
and NetworkPod.pod_ip_pools — so a pool mistake fails the proposed change instead of producing wrong
addressing at generation time. See Pool role resolution for how the
generators consume the same collections.
Per fabric:
- Every member of
fabric_ip_poolsmust be aCoreIPAddressPoolorCoreIPPrefixPool. - Each pool's purpose is resolved from the
IpamPrefix.rolevalues on its resources; a pool with mixed authoritative roles is an error, and two pools claiming the same role in one fabric is an error. - The required role set depends on fabric intent — the underlay and overlay routing protocols, and
whether the fabric has DCI links. A required role satisfied only through a legacy relationship
(for example
NetworkFabric.uplink_pool) is reported as information, not an error, so migration can proceed incrementally. - A required role with no pool and no Fabric Supernet to carve one from is an error. When a Fabric Supernet does exist, its remaining free space is checked against the prefixes the missing roles would need.
Per pod:
- Members of
pod_ip_poolsare type- and role-checked as above, and themlagandmlag_peeringroles must be address pools rather than prefix pools. - MLAG roles required by the pod — driven by the parent fabric's underlay protocol and whether any
rack enables MLAG — must be present through
pod_ip_poolsor a legacy pod relationship. - Pod Loopback, Loopback VTEP, and Fabric Point-to-Point prefixes must be contained by the matching fabric pool. A role the fabric leaves to its Fabric Supernet to carve on demand has nothing to contain against yet and is skipped rather than reported.
Unit coverage is in
tests/unit/test_fabric_pool_check.py.
Schema
schemas/cv/cv.yml defines CloudvisionWorkspace — Cloudvision.Workspace — the node the check
writes its workspace tracking to. See Schemas.
Running and testing a check
Run it against a fabric from the CLI:
# Variables are passed as key=value; this check takes the fabric name.
uv run infrahubctl check cv-config-validation name=Fabric-L3LS-Multi-Domain --branch <branch-name>
# List the checks the repository defines
uv run infrahubctl check --list
In the UI, a check runs automatically as part of proposed-change validation; its result appears under the proposed change's Checks tab.
Unit and integration coverage is in
tests/unit/test_cv_integration.py.
Because the check reaches CloudVision through PyAVD's CVClient, tests exercise it with that client
stubbed rather than against a live CloudVision instance.
Adding a check
-
Write the GraphQL query under
checks/and register it in thequeries:block of.infrahub.yml. -
Regenerate the matching
*_query.py— do not hand-write it:uv run infrahubctl graphql generate-return-types checks/my_check.gql -
Implement a class deriving from
InfrahubCheck, settingqueryand implementingasync def validate(self, data). Report throughself.log_info()andself.log_error(); an error fails the check. -
Register it under
check_definitions:with itstargetsgroup andparameters. -
Add tests under
tests/unit/.
Source
- Checks:
checks/ - Registration:
.infrahub.yml—check_definitions:block. - Operator documentation: CloudVision Validation.