Skip to content

Commit 81f2876

Browse files
committed
Graceful exit in case of non-existent system
1 parent 010e974 commit 81f2876

File tree

3 files changed

+99
-62
lines changed

3 files changed

+99
-62
lines changed

src/mcp_server_uyuni/server.py

Lines changed: 79 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ async def _list_systems(token: str) -> List[Dict[str, Union[str, int]]]:
141141
return filtered_systems
142142

143143
@mcp.tool()
144-
async def get_system_details(system_identifier: Union[str, int], ctx: Context):
144+
async def get_system_details(system_identifier: str, ctx: Context):
145145
"""Gets details of the specified system.
146146
147147
Args:
@@ -210,18 +210,27 @@ async def get_system_details(system_identifier: Union[str, int], ctx: Context):
210210
await ctx.info(log_string)
211211
return await _get_system_details(system_identifier, ctx.get_state('token'))
212212

213-
async def _get_system_details(system_identifier: Union[str, int], token: str) -> Dict[str, Any]:
213+
async def _get_system_details(system_identifier: str, token: str) -> Dict[str, Any]:
214214
system_id = await _resolve_system_id(system_identifier, token)
215215

216216
async with httpx.AsyncClient(verify=CONFIG["UYUNI_MCP_SSL_VERIFY"]) as client:
217-
details_call: Coroutine = call_uyuni_api(
218-
client=client,
219-
method="GET",
220-
api_path="/rhn/manager/api/system/getDetails",
221-
params={'sid': system_id},
222-
error_context=f"Fetching details for system {system_id}",
223-
token=token
224-
)
217+
try:
218+
details_result = await call_uyuni_api(
219+
client=client,
220+
method="GET",
221+
api_path="/rhn/manager/api/system/getDetails",
222+
params={'sid': system_id},
223+
error_context=f"Fetching details for system {system_id}",
224+
token=token
225+
)
226+
except UnexpectedResponse as e:
227+
logger.warning(f"System {system_id} not found or API error: {e}")
228+
return {}
229+
230+
if not isinstance(details_result, dict) or 'id' not in details_result:
231+
logger.warning(f"System {system_id} not found or details could not be fetched. Result: {details_result}")
232+
return {}
233+
225234
uuid_call: Coroutine = call_uyuni_api(
226235
client=client,
227236
method="GET",
@@ -256,60 +265,54 @@ async def _get_system_details(system_identifier: Union[str, int], token: str) ->
256265
)
257266

258267
results = await asyncio.gather(
259-
details_call,
260268
uuid_call,
261269
cpu_call,
262270
network_call,
263-
products_call
271+
products_call,
272+
return_exceptions=True
264273
)
265274

266-
details_result, uuid_result, cpu_result, network_result, products_result = results
267-
268-
if isinstance(details_result, dict):
269-
# Only add the identifier if the API returned actual data
270-
system_details = {
271-
"system_id": details_result["id"],
272-
"system_name": details_result["profile_name"],
273-
"last_boot": details_result["last_boot"],
274-
"uuid": uuid_result
275-
}
275+
uuid_result, cpu_result, network_result, products_result = results
276276

277-
if isinstance(cpu_result, dict):
278-
cpu_details = {
279-
"family": cpu_result["family"],
280-
"mhz": cpu_result["mhz"],
281-
"model": cpu_result["model"],
282-
"vendor": cpu_result["vendor"],
283-
"arch": cpu_result["arch"]
284-
}
285-
system_details["cpu"] = cpu_details
286-
else:
287-
logger.error(f"Unexpected API response when getting CPU information for system {system_id}")
288-
logger.error(cpu_result)
289-
290-
if isinstance(network_result, dict):
291-
network_details = {
292-
"hostname": network_result["hostname"],
293-
"ip": network_result["ip"],
294-
"ip6": network_result["ip6"]
295-
}
296-
system_details["network"] = network_details
297-
else:
298-
logger.error(f"Unexpected API response when getting network information for system {system_id}")
299-
logger.error(network_result)
277+
system_details = {
278+
"system_id": details_result["id"],
279+
"system_name": details_result["profile_name"],
280+
"last_boot": details_result["last_boot"],
281+
"uuid": uuid_result if not isinstance(uuid_result, Exception) else None
282+
}
300283

301-
if isinstance(products_result, list):
302-
base_product = [p["friendlyName"] for p in products_result if p["isBaseProduct"]]
303-
system_details["installed_products"] = base_product
304-
else:
305-
logger.error(f"Unexpected API response when getting installed products for system {system_id}")
306-
logger.error(products_result)
284+
if isinstance(cpu_result, dict):
285+
cpu_details = {
286+
"family": cpu_result.get("family"),
287+
"mhz": cpu_result.get("mhz"),
288+
"model": cpu_result.get("model"),
289+
"vendor": cpu_result.get("vendor"),
290+
"arch": cpu_result.get("arch")
291+
}
292+
system_details["cpu"] = cpu_details
293+
else:
294+
logger.error(f"Unexpected API response when getting CPU information for system {system_id}")
295+
logger.error(cpu_result)
296+
297+
if isinstance(network_result, dict):
298+
network_details = {
299+
"hostname": network_result.get("hostname"),
300+
"ip": network_result.get("ip"),
301+
"ip6": network_result.get("ip6")
302+
}
303+
system_details["network"] = network_details
304+
else:
305+
logger.error(f"Unexpected API response when getting network information for system {system_id}")
306+
logger.error(network_result)
307307

308-
return system_details
308+
if isinstance(products_result, list):
309+
base_product = [p["friendlyName"] for p in products_result if p.get("isBaseProduct")]
310+
system_details["installed_products"] = base_product
309311
else:
310-
logger.error(f"Unexpected API response when getting details for system {system_id}")
311-
logger.error(details_result)
312-
return {}
312+
logger.error(f"Unexpected API response when getting installed products for system {system_id}")
313+
logger.error(products_result)
314+
315+
return system_details
313316

314317
@mcp.tool()
315318
async def get_system_event_history(system_identifier: Union[str, int], ctx: Context, offset: int = 0, limit: int = 10, earliest_date: str = None):
@@ -1627,6 +1630,26 @@ async def create_system_group(name: str, ctx: Context, description: str = "", co
16271630
logger.info(log_string)
16281631
await ctx.info(log_string)
16291632

1633+
token = ctx.get_state('token')
1634+
1635+
# Check if group already exists
1636+
async with httpx.AsyncClient(verify=CONFIG["UYUNI_MCP_SSL_VERIFY"]) as client:
1637+
existing_groups = await call_uyuni_api(
1638+
client=client,
1639+
method="GET",
1640+
api_path='/rhn/manager/api/systemgroup/listAllGroups',
1641+
error_context="checking existing system groups",
1642+
token=token
1643+
)
1644+
1645+
if isinstance(existing_groups, list):
1646+
for group in existing_groups:
1647+
if isinstance(group, dict) and group.get('name') == name:
1648+
msg = f"System group '{name}' already exists. No action taken."
1649+
logger.info(msg)
1650+
await ctx.info(msg)
1651+
return msg
1652+
16301653
is_confirmed = _to_bool(confirm)
16311654

16321655
if not is_confirmed:
@@ -1641,7 +1664,7 @@ async def create_system_group(name: str, ctx: Context, description: str = "", co
16411664
api_path=create_group_path,
16421665
json_body={"name": name, "description": description},
16431666
error_context=f"creating system group '{name}'",
1644-
token=ctx.get_state('token')
1667+
token=token
16451668
)
16461669

16471670
if isinstance(api_result, dict) and 'id' in api_result:

test/acceptance_tests.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,12 @@ def main():
227227
default=None,
228228
help="Model to use for judging the test results. Defaults to the test model if not specified.",
229229
)
230+
parser.add_argument(
231+
"--test-id",
232+
type=str,
233+
default=None,
234+
help="Run a specific test case by its ID.",
235+
)
230236
args = parser.parse_args()
231237

232238
if not args.test_cases_file.is_file():
@@ -262,6 +268,14 @@ def main():
262268
with open(args.test_cases_file, "r", encoding="utf-8") as f:
263269
test_cases = json.load(f)
264270

271+
if args.test_id:
272+
original_count = len(test_cases)
273+
test_cases = [tc for tc in test_cases if tc.get("id") == args.test_id]
274+
if not test_cases:
275+
print(f"Error: Test case with ID '{args.test_id}' not found.", file=sys.stderr)
276+
sys.exit(1)
277+
print(f"Filtered tests: Running 1 test out of {original_count}.")
278+
265279
results = []
266280
passed_count = 0
267281
failed_count = 0

test/test_config.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,30 @@
33
"proxy": {
44
"id": "1000010000",
55
"name": "suma-test-ai-proxy.mgr.suse.de",
6-
"cpu_model": "AMD EPYC-Milan Processor"
6+
"cpu_model": "AMD EPYC-Genoa Processor"
77
},
88
"build_host": {
9-
"id": "1000010005",
9+
"id": "1000010003",
1010
"name": "suma-test-ai-build-host.mgr.suse.de",
1111
"cpu_model": "QEMU Virtual CPU"
1212
},
1313
"deblike_minion": {
14-
"id": "1000010004",
14+
"id": "1000010002",
1515
"name": "suma-test-ai-deblike-minion.mgr.suse.de",
1616
"cpu_model": "QEMU Virtual CPU"
1717
},
1818
"rhlike_minion": {
19-
"id": "1000010003",
19+
"id": "1000010001",
2020
"name": "suma-test-ai-rhlike-minion.mgr.suse.de",
2121
"cpu_model": "QEMU Virtual CPU"
2222
},
2323
"suse_minion": {
24-
"id": "1000010001",
24+
"id": "1000010004",
2525
"name": "suma-test-ai-suse-minion.mgr.suse.de",
2626
"cpu_model": "QEMU Virtual CPU"
2727
},
2828
"suse_ssh_minion": {
29-
"id": "1000010002",
29+
"id": "1000010005",
3030
"name": "suma-test-ai-suse-sshminion.mgr.suse.de",
3131
"cpu_model": "QEMU Virtual CPU"
3232
},

0 commit comments

Comments
 (0)