Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Model Context Protocol Server for Uyuni Server API.
* schedule_system_reboot
* cancel_action
* list_all_scheduled_actions
* get_action_details

## Usage

Expand Down
2 changes: 2 additions & 0 deletions TEST_CASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ This document tracks the manual test cases executed for different versions/tags
| **Action Scheduling Tools** | | | | | | | |
| TC-SCH-001 | `list_all_scheduled_actions` | "List all scheduled actions in Uyuni." | Returns a list of action dictionaries, or an empty list if none. Example fields: id, name, type, earliest. | N/A | N/A | N/A | |
| TC-SCH-002 | `cancel_action` | "Cancel action with ID 123." (assuming 123 is a valid, cancellable action) | "The system will ask for confirmation: 'Are you sure you want to cancel action with ID 123?'. After confirming, it returns: 'Successfully canceled action: 123'" | N/A | N/A | N/A | Requires a pre-existing action to cancel |
| TC-SCH-003 | `get_action_details` (Valid ID) | "Get details for action ID 158." (use a valid ID) | Returns a dict with action details, including lists for `completed_systems`, `failed_systems`, and `in_progress_systems`. | N/A | N/A | N/A | |
| TC-SCH-004 | `get_action_details` (Invalid ID) | "Get details for action ID 99999." (use an invalid ID) | Returns an empty dict. | N/A | N/A | N/A | |
| **System Operations** | | | | | | | |
| TC-OPS-001 | `system.bootstrap` (Hypothetical) | "Bootstrap a new system" | "New system bootstrapped with name new_system" | N/A | N/A | N/A | Tool not implemented |
| **LLM Guardrails & Capabilities** | | | | | | | |
Expand Down
87 changes: 87 additions & 0 deletions src/mcp_server_uyuni/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,93 @@ async def list_all_scheduled_actions() -> List[Dict[str, Any]]:
print(f"Warning: Expected a list for all scheduled actions, but received: {type(api_result)}")
return processed_actions_list

async def _get_systems_for_action(client: httpx.AsyncClient, action_id: int, api_path: str, status_context: str) -> List[Dict[str, Any]]:
"""
Internal helper to fetch systems associated with a given action for a specific status
(e.g., completed, failed, in-progress).

Args:
client: The httpx.AsyncClient instance (must have active login session).
action_id: The ID of the action to query systems for.
api_path: The specific API endpoint path to call.
status_context: A string describing the status (e.g., "completed") for logging.

Returns:
List[Dict[str, Any]]: A list of system dictionaries, each containing 'system_id'
and 'system_name'. Returns an empty list on failure or if
no systems are found for that status.
"""
systems_list = await _call_uyuni_api(
client=client,
method="GET",
api_path=api_path,
params={'actionId': str(action_id)},
error_context=f"fetching {status_context} systems for action ID {action_id}",
perform_login=False, # Login is handled by the calling function
default_on_error=[]
)

processed_systems = []
if isinstance(systems_list, list):
for system in systems_list:
if isinstance(system, dict):
# Rename for consistency with other tools
processed_systems.append({
'system_id': system.get('server_id'),
'system_name': system.get('server_name')
})
else:
print(f"Warning: Unexpected item format in {status_context} systems list for action ID {action_id}: {system}")
elif systems_list: # Log if not default empty list but also not a list
print(f"Warning: Expected a list for {status_context} systems for action ID {action_id}, but received: {type(systems_list)}")

return processed_systems

@mcp.tool()
async def get_action_details(action_id: int) -> Dict[str, Any]:
"""
Retrieves detailed information for a specific scheduled action, including lists
of systems that have completed, failed, or are in progress for that action.

Args:
action_id: The unique identifier of the scheduled action to look up.

Returns:
Dict[str, Any]: A dictionary containing the details of the action, such as its
type, name, and status. It also includes:
- 'completed_systems': A list of systems that successfully completed the action.
- 'failed_systems': A list of systems where the action failed.
- 'in_progress_systems': A list of systems where the action is still running.
Each system in these lists is a dictionary with 'system_id' and 'system_name'.
Returns an empty dictionary if the action ID is not found or an API error occurs.
"""
lookup_action_path = '/rhn/manager/api/schedule/lookupAction'

async with httpx.AsyncClient(verify=False) as client:
action_details = await _call_uyuni_api(
client=client,
method="GET",
api_path=lookup_action_path,
params={'actionId': str(action_id)},
error_context=f"getting details for action ID {action_id}",
default_on_error=None
)

if not isinstance(action_details, dict) or not action_details:
if action_details is not None:
print(f"Warning: Expected a dict for action details for action ID {action_id}, but received: {type(action_details)}")
return {}

action_details = dict(action_details)
if 'id' in action_details:
action_details['action_id'] = action_details.pop('id')

action_details['completed_systems'] = await _get_systems_for_action(client, action_id, '/rhn/manager/api/schedule/listCompletedSystems', 'completed')
action_details['failed_systems'] = await _get_systems_for_action(client, action_id, '/rhn/manager/api/schedule/listFailedSystems', 'failed')
action_details['in_progress_systems'] = await _get_systems_for_action(client, action_id, '/rhn/manager/api/schedule/listInProgressSystems', 'in-progress')

return action_details

@mcp.tool()
async def cancel_action(action_id: int, confirm: bool = False) -> str:
"""
Expand Down