Skip to content

Commit 156e87f

Browse files
committed
feat: server action
1 parent efd7095 commit 156e87f

File tree

2 files changed

+136
-1
lines changed

2 files changed

+136
-1
lines changed

src/openstack_mcp_server/tools/compute_tools.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def register_tools(self, mcp: FastMCP):
2323
mcp.tool()(self.get_server)
2424
mcp.tool()(self.create_server)
2525
mcp.tool()(self.get_flavors)
26+
mcp.tool()(self.action_server)
2627

2728
def get_servers(self) -> list[Server]:
2829
"""
@@ -102,3 +103,51 @@ def get_flavors(self) -> list[Flavor]:
102103
for flavor in conn.compute.flavors():
103104
flavor_list.append(Flavor(**flavor))
104105
return flavor_list
106+
107+
def action_server(self, id: str, action: str) -> None:
108+
"""
109+
Perform an action on a Compute server.
110+
111+
:param id: The ID of the server.
112+
:param action: The action to perform.
113+
Available actions:
114+
- pause: Pauses a server. Changes its status to PAUSED
115+
- unpause: Unpauses a paused server and changes its status to ACTIVE
116+
- suspend: Suspends a server and changes its status to SUSPENDED
117+
- resume: Resumes a suspended server and changes its status to ACTIVE
118+
- lock: Locks a server
119+
- unlock: Unlocks a locked server
120+
- rescue: Puts a server in rescue mode and changes its status to RESCUE
121+
- unrescue: Unrescues a server. Changes status to ACTIVE
122+
- start: Starts a stopped server and changes its status to ACTIVE
123+
- stop: Stops a running server and changes its status to SHUTOFF
124+
- shelve: Shelves a server
125+
- shelve_offload: Shelf-offloads, or removes, a shelved server
126+
- unshelve: Unshelves, or restores, a shelved server
127+
Only above actions are currently supported
128+
:return: None
129+
:raises ValueError: If the action is not supported or invalid(ConflictException).
130+
"""
131+
conn = get_openstack_conn()
132+
133+
action_methods = {
134+
"pause": conn.compute.pause_server,
135+
"unpause": conn.compute.unpause_server,
136+
"suspend": conn.compute.suspend_server,
137+
"resume": conn.compute.resume_server,
138+
"lock": conn.compute.lock_server,
139+
"unlock": conn.compute.unlock_server,
140+
"rescue": conn.compute.rescue_server,
141+
"unrescue": conn.compute.unrescue_server,
142+
"start": conn.compute.start_server,
143+
"stop": conn.compute.stop_server,
144+
"shelve": conn.compute.shelve_server,
145+
"shelve_offload": conn.compute.shelve_offload_server,
146+
"unshelve": conn.compute.unshelve_server,
147+
}
148+
149+
if action not in action_methods:
150+
raise ValueError(f"Unsupported action: {action}")
151+
152+
action_methods[action](id)
153+
return None

tests/tools/test_compute_tools.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from unittest.mock import Mock, call
22

3+
import pytest
4+
5+
from openstack.exceptions import ConflictException, NotFoundException
6+
37
from openstack_mcp_server.tools.compute_tools import ComputeTools
48
from openstack_mcp_server.tools.response.compute import Flavor, Server
59

@@ -261,9 +265,10 @@ def test_register_tools(self):
261265
call(compute_tools.get_server),
262266
call(compute_tools.create_server),
263267
call(compute_tools.get_flavors),
268+
call(compute_tools.action_server),
264269
],
265270
)
266-
assert mock_tool_decorator.call_count == 4
271+
assert mock_tool_decorator.call_count == 5
267272

268273
def test_compute_tools_instantiation(self):
269274
"""Test ComputeTools can be instantiated."""
@@ -346,3 +351,84 @@ def test_get_flavors_empty_list(self, mock_get_openstack_conn):
346351

347352
assert result == []
348353
mock_conn.compute.flavors.assert_called_once()
354+
355+
@pytest.mark.parametrize(
356+
"action",
357+
[
358+
"pause",
359+
"unpause",
360+
"suspend",
361+
"resume",
362+
"lock",
363+
"unlock",
364+
"rescue",
365+
"unrescue",
366+
"start",
367+
"stop",
368+
"shelve",
369+
"shelve_offload",
370+
"unshelve",
371+
],
372+
)
373+
def test_action_server_success(self, mock_get_openstack_conn, action):
374+
"""Test action_server with all supported actions."""
375+
mock_conn = mock_get_openstack_conn
376+
server_id = "test-server-id"
377+
378+
# Mock the action method to avoid calling actual methods
379+
action_method = getattr(mock_conn.compute, f"{action}_server")
380+
action_method.return_value = None
381+
382+
compute_tools = ComputeTools()
383+
result = compute_tools.action_server(server_id, action)
384+
385+
# Verify the result is None (void function)
386+
assert result is None
387+
388+
# Verify the correct method was called with server ID
389+
action_method.assert_called_once_with(server_id)
390+
391+
def test_action_server_unsupported_action(self):
392+
"""Test action_server with unsupported action raises ValueError."""
393+
server_id = "test-server-id"
394+
unsupported_action = "invalid_action"
395+
396+
compute_tools = ComputeTools()
397+
398+
with pytest.raises(
399+
ValueError,
400+
match=f"Unsupported action: {unsupported_action}",
401+
):
402+
compute_tools.action_server(server_id, unsupported_action)
403+
404+
def test_action_server_not_found(self, mock_get_openstack_conn):
405+
"""Test action_server when server does not exist."""
406+
mock_conn = mock_get_openstack_conn
407+
server_id = "non-existent-server-id"
408+
action = "pause"
409+
410+
# Mock the action method to raise NotFoundException
411+
mock_conn.compute.pause_server.side_effect = NotFoundException()
412+
413+
compute_tools = ComputeTools()
414+
415+
with pytest.raises(NotFoundException):
416+
compute_tools.action_server(server_id, action)
417+
418+
mock_conn.compute.pause_server.assert_called_once_with(server_id)
419+
420+
def test_action_server_conflict_exception(self, mock_get_openstack_conn):
421+
"""Test action_server when action cannot be performed due to Conflict Exception."""
422+
mock_conn = mock_get_openstack_conn
423+
server_id = "test-server-id"
424+
action = "start"
425+
426+
# Mock the action method to raise ConflictException
427+
mock_conn.compute.start_server.side_effect = ConflictException()
428+
429+
compute_tools = ComputeTools()
430+
431+
with pytest.raises(ConflictException):
432+
compute_tools.action_server(server_id, action)
433+
434+
mock_conn.compute.start_server.assert_called_once_with(server_id)

0 commit comments

Comments
 (0)