Skip to content

09 — CPCRUD Inverse Rollback (09_cpcrud_inverse.py)

Demonstrates generating a compensating rollback template via client.cpcrud.inverse(plan, report) and applying it to restore the domain state.

Source Code (examples/09_cpcrud_inverse.py)

examples/09_cpcrud_inverse.py
"""Example 09 — CPCRUD compensating rollback template (`plan` -> `apply` -> `inverse` -> `apply`).

Run:
    uv run examples/09_cpcrud_inverse.py

Demonstrates how `client.cpcrud.inverse(plan, report)` builds a schema-valid compensating
template from the original `Plan` and `ApplyReport`:
- Created objects/services/groups/rules become `delete` operations keyed by `uid`.
- Updated objects/rules roll back to their captured `prior_state`.
- Deleted objects/rules are re-created from their captured `prior_state`.
"""

from __future__ import annotations

import asyncio
import logging
import os
import sys
from pathlib import Path
from typing import Any

from dotenv import load_dotenv

load_dotenv(".env.test", override=True)
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
logging.basicConfig(level=logging.WARNING)

sys.path.insert(0, str(Path(__file__).parent))
from _session_guard import guarded_session  # noqa: E402

from arodonata import ArodonataClient, ArodonataSettings  # noqa: E402


async def main() -> None:
    """Plan, apply, generate an inverse rollback template, and apply the rollback."""
    settings = ArodonataSettings()
    mgmt_name = settings.mgmt_names[0]
    domain = os.getenv("EXAMPLE_DOMAIN") or os.getenv("TEST_DOMAIN_A", "Domain4")

    template: dict[str, Any] = {
        "version": 1,
        "mgmt_name": mgmt_name,
        "domain": domain,
        "objects": [
            {
                "type": "host",
                "name": "cpcrud-example-inverse-host",
                "ipv4-address": "198.51.100.19",
                "comments": "Created for CPCRUD inverse rollback demo",
            }
        ],
    }

    print(f"[Step 1/4] Capturing baseline revision on {mgmt_name}:{domain}...", flush=True)
    async with ArodonataClient(settings=settings) as client, guarded_session(client, mgmt_name, domains=[domain]):
        # 1. Plan ONCE and keep the Plan instance for inverse()
        print("[Step 2/4] Planning and applying forward CPCRUD template...", flush=True)
        plan = await client.cpcrud.plan(template)
        report = await client.cpcrud.apply(plan, session_name="cpcrud-inverse-forward")
        print(f"Forward apply: success={report.success} created={report.created_count}")
        for r in report.results:
            print(f"  forward: [{r.mgmt_name}:{r.domain_name}] {r.outcome.value:9} {r.object_type} {r.name}")

        # 2. Generate compensating inverse template from (plan, report)
        print("\n[Step 3/4] Generating and validating compensating inverse template...", flush=True)
        inverse_template = client.cpcrud.inverse(plan, report)
        val_errors = client.cpcrud.validate(inverse_template)
        assert not val_errors, f"Inverse template failed schema validation: {val_errors}"

        # 3. Plan and apply the compensating template
        print("[Step 4/4] Planning and applying inverse rollback template...", flush=True)
        inv_plan = await client.cpcrud.plan(inverse_template)
        inv_report = await client.cpcrud.apply(inv_plan, session_name="cpcrud-inverse-rollback")
        print(f"\nInverse rollback: success={inv_report.success} deleted={inv_report.deleted_count}")
        for r in inv_report.results:
            print(f"  inverse: [{r.mgmt_name}:{r.domain_name}] {r.outcome.value:9} {r.object_type} {r.name}")
        print(f"\nVerifying/reverting {mgmt_name}:{domain} baseline on exit...", flush=True)


if __name__ == "__main__":
    asyncio.run(main())