feat(poc): rewind MCP, skill, and recover-ladder docs
Initial PoC: find_protection, tagged checkpoints, FLR, mutating catalog. Lab 10.9 status enums (0=Initializing, 1=MeetingSLA). Credentials stay in gitignored config.json.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""Tagged checkpoint insert and wait-until-listed."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from zerto_rewind_mcp.client import ZertoClient, ZertoError
|
||||
from zerto_rewind_mcp.protection import FindResult
|
||||
from zerto_rewind_mcp.util import pick
|
||||
|
||||
_SAFE = re.compile(r"[^A-Za-z0-9._:-]+")
|
||||
|
||||
|
||||
def make_tag(agent: str, change_id: str, when: datetime | None = None) -> str:
|
||||
stamp = (when or datetime.now(UTC)).strftime("%Y%m%dT%H%M%SZ")
|
||||
agent_s = _SAFE.sub("-", agent.strip())[:40] or "agent"
|
||||
change_s = _SAFE.sub("-", change_id.strip())[:80] or "change"
|
||||
return f"ai:{agent_s}:{change_s}:{stamp}"
|
||||
|
||||
|
||||
def checkpoint_tag(row: dict[str, Any]) -> str:
|
||||
value = pick(row, "Tag", "tag", "CheckpointName", "checkpointName") or ""
|
||||
return str(value)
|
||||
|
||||
|
||||
def checkpoint_id(row: dict[str, Any]) -> str:
|
||||
value = pick(
|
||||
row, "CheckpointId", "checkpointId", "checkpointIdentifier", "CheckpointIdentifier"
|
||||
)
|
||||
return str(value) if value is not None else ""
|
||||
|
||||
|
||||
async def wait_for_tag(
|
||||
client: ZertoClient,
|
||||
vpg_identifier: str,
|
||||
tag: str,
|
||||
*,
|
||||
timeout_s: float = 45.0,
|
||||
interval_s: float = 1.5,
|
||||
) -> dict[str, Any]:
|
||||
deadline = asyncio.get_event_loop().time() + timeout_s
|
||||
last: list[dict[str, Any]] = []
|
||||
while asyncio.get_event_loop().time() < deadline:
|
||||
last = await client.list_checkpoints(vpg_identifier)
|
||||
for row in last:
|
||||
if checkpoint_tag(row) == tag:
|
||||
return row
|
||||
await asyncio.sleep(interval_s)
|
||||
raise ZertoError(
|
||||
f"Tagged checkpoint {tag!r} did not appear on VPG {vpg_identifier} "
|
||||
f"within {timeout_s:.0f}s. Do not mutate. "
|
||||
"If the protected site is Azure or AWS, tagged checkpoints are not supported."
|
||||
)
|
||||
|
||||
|
||||
async def tag_vpgs(
|
||||
client: ZertoClient,
|
||||
result: FindResult,
|
||||
tag: str,
|
||||
) -> dict[str, Any]:
|
||||
if result.outcome != "ok" or result.vm is None:
|
||||
return {
|
||||
"ok": False,
|
||||
"tag": tag,
|
||||
"message": result.message,
|
||||
"find": result.as_dict(),
|
||||
}
|
||||
tagged: list[dict[str, Any]] = []
|
||||
skipped = [v.as_dict() for v in result.vm.vpgs if not v.can_tag]
|
||||
errors: list[str] = []
|
||||
for vpg in result.taggable_vpgs:
|
||||
try:
|
||||
insert = await client.insert_checkpoint(vpg.vpg_identifier, tag)
|
||||
row = await wait_for_tag(client, vpg.vpg_identifier, tag)
|
||||
tagged.append(
|
||||
{
|
||||
"vpg_identifier": vpg.vpg_identifier,
|
||||
"vpg_name": vpg.vpg_name,
|
||||
"checkpoint_id": checkpoint_id(row),
|
||||
"tag": checkpoint_tag(row) or tag,
|
||||
"insert_result": insert if not isinstance(insert, dict) else "ok",
|
||||
}
|
||||
)
|
||||
except ZertoError as exc:
|
||||
errors.append(f"{vpg.vpg_name} ({vpg.vpg_identifier}): {exc}")
|
||||
ok = bool(tagged) and not errors
|
||||
message = (
|
||||
f"Tagged {len(tagged)} VPG(s) with {tag!r}."
|
||||
if ok
|
||||
else f"Checkpoint failed on {len(errors)} VPG(s). Refuse the change. " + "; ".join(errors)
|
||||
)
|
||||
if skipped and ok:
|
||||
message += " Some VPGs were skipped (syncing or not Protecting)."
|
||||
return {
|
||||
"ok": ok,
|
||||
"tag": tag,
|
||||
"vm": result.vm.as_dict(),
|
||||
"tagged": tagged,
|
||||
"skipped": skipped,
|
||||
"errors": errors,
|
||||
"message": message,
|
||||
}
|
||||
Reference in New Issue
Block a user