Skip to content

Commit 6e12c4f

Browse files
choieastseahalucinor
authored andcommitted
feat: server action (#47)
* feat: server action * fix: apply suggestion (action
1 parent 11dccfa commit 6e12c4f

File tree

2 files changed

+154
-1
lines changed

2 files changed

+154
-1
lines changed

src/openstack_mcp_server/tools/compute_tools.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from enum import Enum
12
from typing import Any
23

34
from fastmcp import FastMCP
@@ -10,6 +11,24 @@
1011
from .base import get_openstack_conn
1112

1213

14+
class ServerActionEnum(str, Enum):
15+
"""available actions without parameter for compute tools"""
16+
17+
PAUSE = "pause"
18+
UNPAUSE = "unpause"
19+
SUSPEND = "suspend"
20+
RESUME = "resume"
21+
LOCK = "lock"
22+
UNLOCK = "unlock"
23+
RESCUE = "rescue"
24+
UNRESCUE = "unrescue"
25+
START = "start"
26+
STOP = "stop"
27+
SHELVE = "shelve"
28+
SHELVE_OFFLOAD = "shelve_offload"
29+
UNSHELVE = "unshelve"
30+
31+
1332
class ComputeTools:
1433
"""
1534
A class to encapsulate Compute-related tools and utilities.
@@ -23,6 +42,7 @@ def register_tools(self, mcp: FastMCP):
2342
mcp.tool()(self.get_server)
2443
mcp.tool()(self.create_server)
2544
mcp.tool()(self.get_flavors)
45+
mcp.tool()(self.action_server)
2646

2747
def get_servers(self) -> list[Server]:
2848
"""
@@ -102,3 +122,50 @@ def get_flavors(self) -> list[Flavor]:
102122
for flavor in conn.compute.flavors():
103123
flavor_list.append(Flavor(**flavor))
104124
return flavor_list
125+
126+
def action_server(self, id: str, action: ServerActionEnum):
127+
"""
128+
Perform an action on a Compute server.
129+
130+
:param id: The ID of the server.
131+
:param action: The action to perform.
132+
Available actions:
133+
- pause: Pauses a server. Changes its status to PAUSED
134+
- unpause: Unpauses a paused server and changes its status to ACTIVE
135+
- suspend: Suspends a server and changes its status to SUSPENDED
136+
- resume: Resumes a suspended server and changes its status to ACTIVE
137+
- lock: Locks a server
138+
- unlock: Unlocks a locked server
139+
- rescue: Puts a server in rescue mode and changes its status to RESCUE
140+
- unrescue: Unrescues a server. Changes status to ACTIVE
141+
- start: Starts a stopped server and changes its status to ACTIVE
142+
- stop: Stops a running server and changes its status to SHUTOFF
143+
- shelve: Shelves a server
144+
- shelve_offload: Shelf-offloads, or removes, a shelved server
145+
- unshelve: Unshelves, or restores, a shelved server
146+
Only above actions are currently supported
147+
:raises ValueError: If the action is not supported or invalid(ConflictException).
148+
"""
149+
conn = get_openstack_conn()
150+
151+
action_methods = {
152+
ServerActionEnum.PAUSE: conn.compute.pause_server,
153+
ServerActionEnum.UNPAUSE: conn.compute.unpause_server,
154+
ServerActionEnum.SUSPEND: conn.compute.suspend_server,
155+
ServerActionEnum.RESUME: conn.compute.resume_server,
156+
ServerActionEnum.LOCK: conn.compute.lock_server,
157+
ServerActionEnum.UNLOCK: conn.compute.unlock_server,
158+
ServerActionEnum.RESCUE: conn.compute.rescue_server,
159+
ServerActionEnum.UNRESCUE: conn.compute.unrescue_server,
160+
ServerActionEnum.START: conn.compute.start_server,
161+
ServerActionEnum.STOP: conn.compute.stop_server,
162+
ServerActionEnum.SHELVE: conn.compute.shelve_server,
163+
ServerActionEnum.SHELVE_OFFLOAD: conn.compute.shelve_offload_server,
164+
ServerActionEnum.UNSHELVE: conn.compute.unshelve_server,
165+
}
166+
167+
if action not in action_methods:
168+
raise ValueError(f"Unsupported action: {action}")
169+
170+
action_methods[action](id)
171+
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, mock_get_openstack_conn):
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)