fix(flr): 10.9 session list + download path #1

Merged
claude merged 1 commits from lab/flr-wait-and-download into main 2026-09-21 12:18:14 -04:00
4 changed files with 69 additions and 21 deletions
Showing only changes of commit f9893b4956 - Show all commits
+1
View File
@@ -13,5 +13,6 @@ config.json
catalog.json
*.log
.mcp.json
recovered/
.claude/*
!.claude/gitea-ship.json
+14 -10
View File
@@ -216,16 +216,20 @@ class ZertoClient:
)
async def fetch_download(self, token: str) -> bytes:
response = await self.request("GET", f"/v1/downloads/{token}")
if response.status_code >= 400:
# workflow also shows GET /v1/flrs/{token}
response = await self.request("GET", f"/v1/flrs/{token}")
if response.status_code >= 400:
raise ZertoError(
f"FLR download HTTP {response.status_code}: {response.text[:300]}",
status_code=response.status_code,
)
return response.content
token = str(token).strip().strip('"')
if token.startswith("v1/") or token.startswith("/v1/"):
paths = [token if token.startswith("/") else f"/{token}"]
else:
paths = [f"/v1/downloads/{token}", f"/v1/flrs/{token}"]
last = None
for path in paths:
last = await self.request("GET", path)
if last.status_code < 400:
return last.content
raise ZertoError(
f"FLR download HTTP {last.status_code if last else '?'}: {(last.text[:300] if last else '')}",
status_code=last.status_code if last else None,
)
async def end_flr(self, session_id: str) -> None:
try:
+35 -10
View File
@@ -8,6 +8,15 @@ from typing import Any
from zerto_rewind_mcp.client import ZertoClient, ZertoError
from zerto_rewind_mcp.util import pick
READY_STATUSES = {
"ready",
"mounted",
"available",
"mountcompletedsuccessfully",
"completed",
}
FAILED_STATUSES = {"failed", "error", "mountfailed"}
def session_id_from(payload: Any) -> str:
if payload is None:
@@ -17,6 +26,7 @@ def session_id_from(payload: Any) -> str:
if isinstance(payload, dict):
value = pick(
payload,
"FlrSessionIdentifier",
"sessionId",
"SessionId",
"flrSessionIdentifier",
@@ -40,6 +50,22 @@ def download_token_from(payload: Any) -> str:
raise ZertoError(f"Could not read FLR download token from {payload!r}")
def flr_row(payload: Any) -> dict[str, Any]:
if isinstance(payload, list):
if not payload:
return {}
first = payload[0]
return first if isinstance(first, dict) else {}
if isinstance(payload, dict):
return payload
return {}
def flr_status(row: dict[str, Any]) -> str:
value = pick(row, "FlrSessionStatus", "Status", "status", "state", "State") or ""
return str(value)
async def wait_flr_ready(
client: ZertoClient,
session_id: str,
@@ -51,19 +77,18 @@ async def wait_flr_ready(
last: Any = None
while asyncio.get_event_loop().time() < deadline:
last = await client.get_flr(session_id)
status = ""
if isinstance(last, dict):
status = str(pick(last, "Status", "status", "state", "State") or "")
if status.lower() in {"ready", "mounted", "available"}:
return last if isinstance(last, dict) else {"status": status}
if "mountinprogress" in status.lower() or "inprogress" in status.lower():
row = flr_row(last)
status = flr_status(row)
low = status.lower()
if low in READY_STATUSES:
return row
if "mountinprogress" in low or (low.endswith("inprogress") and "fail" not in low):
await asyncio.sleep(interval_s)
continue
if status.lower() in {"failed", "error"}:
if low in FAILED_STATUSES:
raise ZertoError(f"FLR session {session_id} failed: {last}")
# some appliances omit status once mounted
if isinstance(last, dict) and not status:
return last
if row and not status:
return row
await asyncio.sleep(interval_s)
raise ZertoError(
f"FLR session {session_id} not ready within {timeout_s:.0f}s (last={last!r}). "
+19 -1
View File
@@ -1,12 +1,30 @@
from zerto_rewind_mcp.recover import download_token_from, session_id_from
from zerto_rewind_mcp.recover import (
download_token_from,
flr_row,
flr_status,
session_id_from,
)
def test_session_id_shapes():
assert session_id_from("abc") == "abc"
assert session_id_from({"sessionId": "s1"}) == "s1"
assert session_id_from({"Identifier": "s2"}) == "s2"
assert session_id_from({"FlrSessionIdentifier": "s3"}) == "s3"
def test_download_token_shapes():
assert download_token_from("tok") == "tok"
assert download_token_from({"downloadToken": "t2"}) == "t2"
assert download_token_from("v1/downloads/abc").startswith("v1/")
def test_flr_list_status():
payload = [
{
"FlrSessionStatus": "MountCompletedSuccessfully",
"FlrSessionIdentifier": "sess-1",
}
]
row = flr_row(payload)
assert flr_status(row).lower() == "mountcompletedsuccessfully"