fix(flr): partition-rooted FLR paths + agent/intent in checkpoint names (#2)
This commit was merged in pull request #2.
This commit is contained in:
@@ -11,14 +11,54 @@ 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._:-]+")
|
||||
_CTRL = re.compile(r"[\x00-\x1f\x7f]+")
|
||||
_WS = re.compile(r"\s+")
|
||||
|
||||
# CheckpointName is the only field POST /v1/vpgs/{id}/checkpoints accepts
|
||||
# (VpgInsertTagCheckpointDataApi has exactly one property). Who the agent is
|
||||
# and what it is about to do therefore have to live inside the name itself.
|
||||
# Measured on 10.x: names of at least 400 chars are accepted, and spaces,
|
||||
# slashes, parentheses, '=' and '|' all survive. We still cap at TAG_MAX_LEN
|
||||
# so the name stays readable in the Zerto UI checkpoint list.
|
||||
TAG_MAX_LEN = 250
|
||||
|
||||
|
||||
def make_tag(agent: str, change_id: str, when: datetime | None = None) -> str:
|
||||
def _clean(value: Any, limit: int) -> str:
|
||||
"""One field of a checkpoint name: single-line, no separator collisions."""
|
||||
text = _CTRL.sub(" ", str(value or ""))
|
||||
# Zerto appends "; Used for File Level Restore" to its own tags, and we use
|
||||
# " | " as our field separator. Keep both out of user-supplied text.
|
||||
text = text.replace(";", ",").replace("|", "/")
|
||||
return _WS.sub(" ", text).strip()[:limit]
|
||||
|
||||
|
||||
def make_tag(
|
||||
agent: str,
|
||||
change_id: str,
|
||||
when: datetime | None = None,
|
||||
*,
|
||||
action: str | None = None,
|
||||
vm_name: str | None = None,
|
||||
) -> str:
|
||||
"""Build the checkpoint name.
|
||||
|
||||
Says which agent, what it is about to do, to which VM, under what change id:
|
||||
|
||||
ai:claude | edit /etc/app.conf | vm=jp-ubuntu | change=chg-99 | 20260921T150405Z
|
||||
|
||||
action is free text from the caller describing the pending mutation.
|
||||
"""
|
||||
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}"
|
||||
parts = [f"ai:{_clean(agent, 40) or 'agent'}"]
|
||||
cleaned_action = _clean(action, 120)
|
||||
if cleaned_action:
|
||||
parts.append(cleaned_action)
|
||||
cleaned_vm = _clean(vm_name, 60)
|
||||
if cleaned_vm:
|
||||
parts.append(f"vm={cleaned_vm}")
|
||||
parts.append(f"change={_clean(change_id, 80) or 'change'}")
|
||||
parts.append(stamp)
|
||||
return " | ".join(parts)[:TAG_MAX_LEN]
|
||||
|
||||
|
||||
def checkpoint_tag(row: dict[str, Any]) -> str:
|
||||
@@ -68,6 +108,10 @@ async def tag_vpgs(
|
||||
"message": result.message,
|
||||
"find": result.as_dict(),
|
||||
}
|
||||
# Insert then wait, one VPG at a time. Measured on 10.x: tagged checkpoint
|
||||
# inserts fired back to back at the same VPG are silently dropped -- the POST
|
||||
# returns 200 and queues a task, but only the first checkpoint ever appears.
|
||||
# Do not turn this loop into an asyncio.gather().
|
||||
tagged: list[dict[str, Any]] = []
|
||||
skipped = [v.as_dict() for v in result.vm.vpgs if not v.can_tag]
|
||||
errors: list[str] = []
|
||||
|
||||
Reference in New Issue
Block a user