fix(flr): 10.9 session list + download path (#1)
Co-authored-by: claude <[email protected]>
This commit was merged in pull request #1.
This commit is contained in:
@@ -13,5 +13,6 @@ config.json
|
||||
catalog.json
|
||||
*.log
|
||||
.mcp.json
|
||||
recovered/
|
||||
.claude/*
|
||||
!.claude/gitea-ship.json
|
||||
|
||||
@@ -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:
|
||||
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 {response.status_code}: {response.text[:300]}",
|
||||
status_code=response.status_code,
|
||||
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,
|
||||
)
|
||||
return response.content
|
||||
|
||||
async def end_flr(self, session_id: str) -> None:
|
||||
try:
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user