mirror of
https://github.com/jmagar/unraid-mcp.git
synced 2026-03-23 12:39:24 -07:00
feat(array): add parity_history, start/stop array, disk add/remove/mount/unmount/clear_stats
Expands unraid_array from 5 to 13 actions: adds parity_history query, start_array/stop_array state mutations, and disk operations (add_disk, remove_disk, mount_disk, unmount_disk, clear_disk_stats). Destructive actions remove_disk and clear_disk_stats require confirm=True. Safety audit tests updated to cover the new DESTRUCTIVE_ACTIONS registry entry.
This commit is contained in:
@@ -1,21 +1,32 @@
|
||||
"""Array parity check operations.
|
||||
"""Array management: parity checks, array state, and disk operations.
|
||||
|
||||
Provides the `unraid_array` tool with 5 actions for parity check management.
|
||||
Provides the `unraid_array` tool with 13 actions covering parity check
|
||||
management, array start/stop, and disk add/remove/mount operations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal, get_args
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp import Context, FastMCP
|
||||
|
||||
from ..config.logging import logger
|
||||
from ..core.client import make_graphql_request
|
||||
from ..core.exceptions import ToolError, tool_error_handler
|
||||
from ..core.setup import elicit_destructive_confirmation
|
||||
|
||||
|
||||
QUERIES: dict[str, str] = {
|
||||
"parity_status": """
|
||||
query GetParityStatus {
|
||||
array { parityCheckStatus { progress speed errors } }
|
||||
array { parityCheckStatus { progress speed errors status paused running correcting } }
|
||||
}
|
||||
""",
|
||||
"parity_history": """
|
||||
query GetParityHistory {
|
||||
parityHistory {
|
||||
date duration speed status errors progress correcting paused running
|
||||
}
|
||||
}
|
||||
""",
|
||||
}
|
||||
@@ -41,16 +52,68 @@ MUTATIONS: dict[str, str] = {
|
||||
parityCheck { cancel }
|
||||
}
|
||||
""",
|
||||
"start_array": """
|
||||
mutation StartArray {
|
||||
array { setState(input: { desiredState: START }) {
|
||||
state capacity { kilobytes { free used total } }
|
||||
}}
|
||||
}
|
||||
""",
|
||||
"stop_array": """
|
||||
mutation StopArray {
|
||||
array { setState(input: { desiredState: STOP }) {
|
||||
state
|
||||
}}
|
||||
}
|
||||
""",
|
||||
"add_disk": """
|
||||
mutation AddDisk($id: PrefixedID!, $slot: Int) {
|
||||
array { addDiskToArray(input: { id: $id, slot: $slot }) {
|
||||
state disks { id name device type status }
|
||||
}}
|
||||
}
|
||||
""",
|
||||
"remove_disk": """
|
||||
mutation RemoveDisk($id: PrefixedID!) {
|
||||
array { removeDiskFromArray(input: { id: $id }) {
|
||||
state disks { id name device type }
|
||||
}}
|
||||
}
|
||||
""",
|
||||
"mount_disk": """
|
||||
mutation MountDisk($id: PrefixedID!) {
|
||||
array { mountArrayDisk(id: $id) { id name device status } }
|
||||
}
|
||||
""",
|
||||
"unmount_disk": """
|
||||
mutation UnmountDisk($id: PrefixedID!) {
|
||||
array { unmountArrayDisk(id: $id) { id name device status } }
|
||||
}
|
||||
""",
|
||||
"clear_disk_stats": """
|
||||
mutation ClearDiskStats($id: PrefixedID!) {
|
||||
array { clearArrayDiskStatistics(id: $id) }
|
||||
}
|
||||
""",
|
||||
}
|
||||
|
||||
DESTRUCTIVE_ACTIONS = {"remove_disk", "clear_disk_stats"}
|
||||
ALL_ACTIONS = set(QUERIES) | set(MUTATIONS)
|
||||
|
||||
ARRAY_ACTIONS = Literal[
|
||||
"parity_start",
|
||||
"add_disk",
|
||||
"clear_disk_stats",
|
||||
"mount_disk",
|
||||
"parity_cancel",
|
||||
"parity_history",
|
||||
"parity_pause",
|
||||
"parity_resume",
|
||||
"parity_cancel",
|
||||
"parity_start",
|
||||
"parity_status",
|
||||
"remove_disk",
|
||||
"start_array",
|
||||
"stop_array",
|
||||
"unmount_disk",
|
||||
]
|
||||
|
||||
if set(get_args(ARRAY_ACTIONS)) != ALL_ACTIONS:
|
||||
@@ -68,41 +131,86 @@ def register_array_tool(mcp: FastMCP) -> None:
|
||||
@mcp.tool()
|
||||
async def unraid_array(
|
||||
action: ARRAY_ACTIONS,
|
||||
ctx: Context | None = None,
|
||||
confirm: bool = False,
|
||||
correct: bool | None = None,
|
||||
disk_id: str | None = None,
|
||||
slot: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Manage Unraid array parity checks.
|
||||
"""Manage Unraid array: parity checks, array state, and disk operations.
|
||||
|
||||
Actions:
|
||||
parity_start - Start parity check (correct=True to fix errors, correct=False for read-only; required)
|
||||
parity_pause - Pause running parity check
|
||||
parity_resume - Resume paused parity check
|
||||
parity_cancel - Cancel running parity check
|
||||
parity_status - Get current parity check status
|
||||
Parity check actions:
|
||||
parity_start - Start parity check (correct=True to write fixes; required)
|
||||
parity_pause - Pause running parity check
|
||||
parity_resume - Resume paused parity check
|
||||
parity_cancel - Cancel running parity check
|
||||
parity_status - Get current parity check status and progress
|
||||
parity_history - Get parity check history log
|
||||
|
||||
Array state actions:
|
||||
start_array - Start the array (desiredState=START)
|
||||
stop_array - Stop the array (desiredState=STOP)
|
||||
|
||||
Disk operations (requires disk_id):
|
||||
add_disk - Add a disk to the array (requires disk_id; optional slot)
|
||||
remove_disk - Remove a disk from the array (requires disk_id, confirm=True; array must be stopped)
|
||||
mount_disk - Mount a disk (requires disk_id)
|
||||
unmount_disk - Unmount a disk (requires disk_id)
|
||||
clear_disk_stats - Clear I/O statistics for a disk (requires disk_id, confirm=True)
|
||||
"""
|
||||
if action not in ALL_ACTIONS:
|
||||
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(ALL_ACTIONS)}")
|
||||
|
||||
if action in DESTRUCTIVE_ACTIONS and not confirm:
|
||||
desc_map = {
|
||||
"remove_disk": f"Remove disk **{disk_id}** from the array. The array must be stopped first.",
|
||||
"clear_disk_stats": f"Clear all I/O statistics for disk **{disk_id}**. This cannot be undone.",
|
||||
}
|
||||
confirmed = await elicit_destructive_confirmation(ctx, action, desc_map[action])
|
||||
if not confirmed:
|
||||
raise ToolError(
|
||||
f"Action '{action}' was not confirmed. "
|
||||
"Re-run with confirm=True to bypass elicitation."
|
||||
)
|
||||
|
||||
with tool_error_handler("array", action, logger):
|
||||
logger.info(f"Executing unraid_array action={action}")
|
||||
|
||||
# --- Queries ---
|
||||
if action in QUERIES:
|
||||
data = await make_graphql_request(QUERIES[action])
|
||||
return {"success": True, "action": action, "data": data}
|
||||
|
||||
query = MUTATIONS[action]
|
||||
variables: dict[str, Any] | None = None
|
||||
|
||||
# --- Mutations ---
|
||||
if action == "parity_start":
|
||||
if correct is None:
|
||||
raise ToolError("correct is required for 'parity_start' action")
|
||||
variables = {"correct": correct}
|
||||
data = await make_graphql_request(MUTATIONS[action], {"correct": correct})
|
||||
return {"success": True, "action": action, "data": data}
|
||||
|
||||
data = await make_graphql_request(query, variables)
|
||||
if action in ("parity_pause", "parity_resume", "parity_cancel"):
|
||||
data = await make_graphql_request(MUTATIONS[action])
|
||||
return {"success": True, "action": action, "data": data}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"action": action,
|
||||
"data": data,
|
||||
}
|
||||
if action in ("start_array", "stop_array"):
|
||||
data = await make_graphql_request(MUTATIONS[action])
|
||||
return {"success": True, "action": action, "data": data}
|
||||
|
||||
if action == "add_disk":
|
||||
if not disk_id:
|
||||
raise ToolError("disk_id is required for 'add_disk' action")
|
||||
variables: dict[str, Any] = {"id": disk_id}
|
||||
if slot is not None:
|
||||
variables["slot"] = slot
|
||||
data = await make_graphql_request(MUTATIONS[action], variables)
|
||||
return {"success": True, "action": action, "data": data}
|
||||
|
||||
if action in ("remove_disk", "mount_disk", "unmount_disk", "clear_disk_stats"):
|
||||
if not disk_id:
|
||||
raise ToolError(f"disk_id is required for '{action}' action")
|
||||
data = await make_graphql_request(MUTATIONS[action], {"id": disk_id})
|
||||
return {"success": True, "action": action, "data": data}
|
||||
|
||||
raise ToolError(f"Unhandled action '{action}' — this is a bug")
|
||||
|
||||
logger.info("Array tool registered successfully")
|
||||
|
||||
Reference in New Issue
Block a user