fix(flr): 10.9 session list + download path #1
@@ -13,5 +13,6 @@ config.json
|
|||||||
catalog.json
|
catalog.json
|
||||||
*.log
|
*.log
|
||||||
.mcp.json
|
.mcp.json
|
||||||
|
recovered/
|
||||||
.claude/*
|
.claude/*
|
||||||
!.claude/gitea-ship.json
|
!.claude/gitea-ship.json
|
||||||
|
|||||||
@@ -216,16 +216,20 @@ class ZertoClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def fetch_download(self, token: str) -> bytes:
|
async def fetch_download(self, token: str) -> bytes:
|
||||||
response = await self.request("GET", f"/v1/downloads/{token}")
|
token = str(token).strip().strip('"')
|
||||||
if response.status_code >= 400:
|
if token.startswith("v1/") or token.startswith("/v1/"):
|
||||||
# workflow also shows GET /v1/flrs/{token}
|
paths = [token if token.startswith("/") else f"/{token}"]
|
||||||
response = await self.request("GET", f"/v1/flrs/{token}")
|
else:
|
||||||
if response.status_code >= 400:
|
paths = [f"/v1/downloads/{token}", f"/v1/flrs/{token}"]
|
||||||
raise ZertoError(
|
last = None
|
||||||
f"FLR download HTTP {response.status_code}: {response.text[:300]}",
|
for path in paths:
|
||||||
status_code=response.status_code,
|
last = await self.request("GET", path)
|
||||||
)
|
if last.status_code < 400:
|
||||||
return response.content
|
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:
|
async def end_flr(self, session_id: str) -> None:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ from typing import Any
|
|||||||
from zerto_rewind_mcp.client import ZertoClient, ZertoError
|
from zerto_rewind_mcp.client import ZertoClient, ZertoError
|
||||||
from zerto_rewind_mcp.util import pick
|
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:
|
def session_id_from(payload: Any) -> str:
|
||||||
if payload is None:
|
if payload is None:
|
||||||
@@ -17,6 +26,7 @@ def session_id_from(payload: Any) -> str:
|
|||||||
if isinstance(payload, dict):
|
if isinstance(payload, dict):
|
||||||
value = pick(
|
value = pick(
|
||||||
payload,
|
payload,
|
||||||
|
"FlrSessionIdentifier",
|
||||||
"sessionId",
|
"sessionId",
|
||||||
"SessionId",
|
"SessionId",
|
||||||
"flrSessionIdentifier",
|
"flrSessionIdentifier",
|
||||||
@@ -40,6 +50,22 @@ def download_token_from(payload: Any) -> str:
|
|||||||
raise ZertoError(f"Could not read FLR download token from {payload!r}")
|
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(
|
async def wait_flr_ready(
|
||||||
client: ZertoClient,
|
client: ZertoClient,
|
||||||
session_id: str,
|
session_id: str,
|
||||||
@@ -51,19 +77,18 @@ async def wait_flr_ready(
|
|||||||
last: Any = None
|
last: Any = None
|
||||||
while asyncio.get_event_loop().time() < deadline:
|
while asyncio.get_event_loop().time() < deadline:
|
||||||
last = await client.get_flr(session_id)
|
last = await client.get_flr(session_id)
|
||||||
status = ""
|
row = flr_row(last)
|
||||||
if isinstance(last, dict):
|
status = flr_status(row)
|
||||||
status = str(pick(last, "Status", "status", "state", "State") or "")
|
low = status.lower()
|
||||||
if status.lower() in {"ready", "mounted", "available"}:
|
if low in READY_STATUSES:
|
||||||
return last if isinstance(last, dict) else {"status": status}
|
return row
|
||||||
if "mountinprogress" in status.lower() or "inprogress" in status.lower():
|
if "mountinprogress" in low or (low.endswith("inprogress") and "fail" not in low):
|
||||||
await asyncio.sleep(interval_s)
|
await asyncio.sleep(interval_s)
|
||||||
continue
|
continue
|
||||||
if status.lower() in {"failed", "error"}:
|
if low in FAILED_STATUSES:
|
||||||
raise ZertoError(f"FLR session {session_id} failed: {last}")
|
raise ZertoError(f"FLR session {session_id} failed: {last}")
|
||||||
# some appliances omit status once mounted
|
if row and not status:
|
||||||
if isinstance(last, dict) and not status:
|
return row
|
||||||
return last
|
|
||||||
await asyncio.sleep(interval_s)
|
await asyncio.sleep(interval_s)
|
||||||
raise ZertoError(
|
raise ZertoError(
|
||||||
f"FLR session {session_id} not ready within {timeout_s:.0f}s (last={last!r}). "
|
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():
|
def test_session_id_shapes():
|
||||||
assert session_id_from("abc") == "abc"
|
assert session_id_from("abc") == "abc"
|
||||||
assert session_id_from({"sessionId": "s1"}) == "s1"
|
assert session_id_from({"sessionId": "s1"}) == "s1"
|
||||||
assert session_id_from({"Identifier": "s2"}) == "s2"
|
assert session_id_from({"Identifier": "s2"}) == "s2"
|
||||||
|
assert session_id_from({"FlrSessionIdentifier": "s3"}) == "s3"
|
||||||
|
|
||||||
|
|
||||||
def test_download_token_shapes():
|
def test_download_token_shapes():
|
||||||
assert download_token_from("tok") == "tok"
|
assert download_token_from("tok") == "tok"
|
||||||
assert download_token_from({"downloadToken": "t2"}) == "t2"
|
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