Skip to content

Commit 714cf3e

Browse files
choieastseahalucinor
authored andcommitted
feat: rest server tools (#54)
* feat: rest server tools
1 parent 87bccea commit 714cf3e

File tree

3 files changed

+174
-2
lines changed

3 files changed

+174
-2
lines changed

src/openstack_mcp_server/tools/compute_tools.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ def register_tools(self, mcp: FastMCP):
4343
mcp.tool()(self.create_server)
4444
mcp.tool()(self.get_flavors)
4545
mcp.tool()(self.action_server)
46+
mcp.tool()(self.update_server)
47+
mcp.tool()(self.delete_server)
4648

4749
def get_servers(self) -> list[Server]:
4850
"""
@@ -123,7 +125,7 @@ def get_flavors(self) -> list[Flavor]:
123125
flavor_list.append(Flavor(**flavor))
124126
return flavor_list
125127

126-
def action_server(self, id: str, action: ServerActionEnum):
128+
def action_server(self, id: str, action: ServerActionEnum) -> None:
127129
"""
128130
Perform an action on a Compute server.
129131
@@ -169,3 +171,46 @@ def action_server(self, id: str, action: ServerActionEnum):
169171

170172
action_methods[action](id)
171173
return None
174+
175+
def update_server(
176+
self,
177+
id: str,
178+
accessIPv4: str | None = None,
179+
accessIPv6: str | None = None,
180+
name: str | None = None,
181+
hostname: str | None = None,
182+
description: str | None = None,
183+
) -> Server:
184+
"""
185+
Update a Compute server's name, hostname, or description.
186+
187+
:param id: The UUID of the server.
188+
:param accessIPv4: IPv4 address that should be used to access this server.
189+
:param accessIPv6: IPv6 address that should be used to access this server.
190+
:param name: The server name.
191+
:param hostname: The hostname to configure for the instance in the metadata service.
192+
:param description: A free form description of the server.
193+
:return: The updated Server object.
194+
"""
195+
conn = get_openstack_conn()
196+
server_params = {
197+
"accessIPv4": accessIPv4,
198+
"accessIPv6": accessIPv6,
199+
"name": name,
200+
"hostname": hostname,
201+
"description": description,
202+
}
203+
server_params = {
204+
k: v for k, v in server_params.items() if v is not None
205+
}
206+
server = conn.compute.update_server(id, **server_params)
207+
return Server(**server)
208+
209+
def delete_server(self, id: str) -> None:
210+
"""
211+
Delete a Compute server.
212+
213+
:param id: The UUID of the server.
214+
"""
215+
conn = get_openstack_conn()
216+
conn.compute.delete_server(id)

src/openstack_mcp_server/tools/response/compute.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ class SecurityGroup(BaseModel):
2525

2626
id: str
2727
name: str
28+
hostname: str | None = None
29+
description: str | None = None
2830
status: str | None = None
2931
flavor: Flavor | None = None
3032
image: Image | None = None
3133
addresses: dict[str, list[IPAddress]] | None = None
3234
key_name: str | None = None
3335
security_groups: list[SecurityGroup] | None = None
36+
accessIPv4: str | None = None
37+
accessIPv6: str | None = None
3438

3539

3640
class Flavor(BaseModel):

tests/tools/test_compute_tools.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,11 @@ def test_register_tools(self):
266266
call(compute_tools.create_server),
267267
call(compute_tools.get_flavors),
268268
call(compute_tools.action_server),
269+
call(compute_tools.update_server),
270+
call(compute_tools.delete_server),
269271
],
270272
)
271-
assert mock_tool_decorator.call_count == 5
273+
assert mock_tool_decorator.call_count == 7
272274

273275
def test_compute_tools_instantiation(self):
274276
"""Test ComputeTools can be instantiated."""
@@ -432,3 +434,124 @@ def test_action_server_conflict_exception(self, mock_get_openstack_conn):
432434
compute_tools.action_server(server_id, action)
433435

434436
mock_conn.compute.start_server.assert_called_once_with(server_id)
437+
438+
def test_update_server_success(self, mock_get_openstack_conn):
439+
"""Test updating a server successfully with all parameters."""
440+
mock_conn = mock_get_openstack_conn
441+
server_id = "test-server-id"
442+
443+
mock_server = {
444+
"name": "updated-server",
445+
"id": server_id,
446+
"status": "ACTIVE",
447+
"hostname": "updated-hostname",
448+
"description": "Updated server description",
449+
"accessIPv4": "192.168.1.100",
450+
"accessIPv6": "2001:db8::1",
451+
}
452+
453+
mock_conn.compute.update_server.return_value = mock_server
454+
455+
compute_tools = ComputeTools()
456+
server_params = mock_server.copy()
457+
server_params.pop("status")
458+
result = compute_tools.update_server(**server_params)
459+
460+
expected_output = Server(**mock_server)
461+
assert result == expected_output
462+
463+
expected_params = {
464+
"accessIPv4": "192.168.1.100",
465+
"accessIPv6": "2001:db8::1",
466+
"name": "updated-server",
467+
"hostname": "updated-hostname",
468+
"description": "Updated server description",
469+
}
470+
mock_conn.compute.update_server.assert_called_once_with(
471+
server_id, **expected_params
472+
)
473+
474+
@pytest.mark.parametrize(
475+
"params",
476+
[
477+
{"param_key": "name", "value": "new-name"},
478+
{"param_key": "hostname", "value": "new-hostname"},
479+
{"param_key": "description", "value": "New description"},
480+
{"param_key": "accessIPv4", "value": "192.168.1.100"},
481+
{"param_key": "accessIPv6", "value": "2001:db8::1"},
482+
],
483+
)
484+
def test_update_server_optional_params(
485+
self, mock_get_openstack_conn, params
486+
):
487+
"""Test updating a server with optional parameters."""
488+
mock_conn = mock_get_openstack_conn
489+
server_id = "test-server-id"
490+
491+
mock_server = {
492+
"id": server_id,
493+
"name": "original-name",
494+
"description": "Original description",
495+
"hostname": "original-hostname",
496+
"accessIPv4": "1.1.1.1",
497+
"accessIPv6": "::",
498+
"status": "ACTIVE",
499+
**{params["param_key"]: params["value"]},
500+
}
501+
502+
mock_conn.compute.update_server.return_value = mock_server
503+
504+
compute_tools = ComputeTools()
505+
result = compute_tools.update_server(
506+
id=server_id,
507+
**{params["param_key"]: params["value"]},
508+
)
509+
assert result == Server(**mock_server)
510+
511+
expected_params = {params["param_key"]: params["value"]}
512+
mock_conn.compute.update_server.assert_called_once_with(
513+
server_id, **expected_params
514+
)
515+
516+
def test_update_server_not_found(self, mock_get_openstack_conn):
517+
"""Test updating a server that does not exist."""
518+
mock_conn = mock_get_openstack_conn
519+
server_id = "non-existent-server-id"
520+
521+
# Mock the update_server method to raise NotFoundException
522+
mock_conn.compute.update_server.side_effect = NotFoundException()
523+
524+
compute_tools = ComputeTools()
525+
526+
with pytest.raises(NotFoundException):
527+
compute_tools.update_server(id=server_id)
528+
529+
mock_conn.compute.update_server.assert_called_once_with(server_id)
530+
531+
def test_delete_server_success(self, mock_get_openstack_conn):
532+
"""Test deleting a server successfully."""
533+
mock_conn = mock_get_openstack_conn
534+
server_id = "test-server-id"
535+
536+
mock_conn.compute.delete_server.return_value = None
537+
538+
compute_tools = ComputeTools()
539+
result = compute_tools.delete_server(server_id)
540+
541+
assert result is None
542+
mock_conn.compute.delete_server.assert_called_once_with(server_id)
543+
544+
def test_delete_server_not_found(self, mock_get_openstack_conn):
545+
"""Test deleting a server that does not exist."""
546+
mock_conn = mock_get_openstack_conn
547+
server_id = "non-existent-server-id"
548+
549+
# Mock the delete_server method to raise NotFoundException
550+
mock_conn.compute.delete_server.side_effect = NotFoundException()
551+
552+
compute_tools = ComputeTools()
553+
554+
with pytest.raises(NotFoundException):
555+
compute_tools.delete_server(server_id)
556+
557+
mock_conn.compute.delete_server.assert_called_once_with(server_id)

0 commit comments

Comments
 (0)