Skip to content

Commit 95e8d03

Browse files
choieastseahalucinor
authored andcommitted
feat: volume attachments (#62)
1 parent a34aa34 commit 95e8d03

File tree

3 files changed

+136
-15
lines changed

3 files changed

+136
-15
lines changed

src/openstack_mcp_server/tools/compute_tools.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ def register_tools(self, mcp: FastMCP):
4545
mcp.tool()(self.action_server)
4646
mcp.tool()(self.update_server)
4747
mcp.tool()(self.delete_server)
48+
mcp.tool()(self.attach_volume)
49+
mcp.tool()(self.detach_volume)
4850

4951
def get_servers(self) -> list[Server]:
5052
"""
@@ -125,7 +127,7 @@ def get_flavors(self) -> list[Flavor]:
125127
flavor_list.append(Flavor(**flavor))
126128
return flavor_list
127129

128-
def action_server(self, id: str, action: ServerActionEnum) -> None:
130+
def action_server(self, id: str, action: str) -> None:
129131
"""
130132
Perform an action on a Compute server.
131133
@@ -151,19 +153,19 @@ def action_server(self, id: str, action: ServerActionEnum) -> None:
151153
conn = get_openstack_conn()
152154

153155
action_methods = {
154-
ServerActionEnum.PAUSE: conn.compute.pause_server,
155-
ServerActionEnum.UNPAUSE: conn.compute.unpause_server,
156-
ServerActionEnum.SUSPEND: conn.compute.suspend_server,
157-
ServerActionEnum.RESUME: conn.compute.resume_server,
158-
ServerActionEnum.LOCK: conn.compute.lock_server,
159-
ServerActionEnum.UNLOCK: conn.compute.unlock_server,
160-
ServerActionEnum.RESCUE: conn.compute.rescue_server,
161-
ServerActionEnum.UNRESCUE: conn.compute.unrescue_server,
162-
ServerActionEnum.START: conn.compute.start_server,
163-
ServerActionEnum.STOP: conn.compute.stop_server,
164-
ServerActionEnum.SHELVE: conn.compute.shelve_server,
165-
ServerActionEnum.SHELVE_OFFLOAD: conn.compute.shelve_offload_server,
166-
ServerActionEnum.UNSHELVE: conn.compute.unshelve_server,
156+
ServerActionEnum.PAUSE.value: conn.compute.pause_server,
157+
ServerActionEnum.UNPAUSE.value: conn.compute.unpause_server,
158+
ServerActionEnum.SUSPEND.value: conn.compute.suspend_server,
159+
ServerActionEnum.RESUME.value: conn.compute.resume_server,
160+
ServerActionEnum.LOCK.value: conn.compute.lock_server,
161+
ServerActionEnum.UNLOCK.value: conn.compute.unlock_server,
162+
ServerActionEnum.RESCUE.value: conn.compute.rescue_server,
163+
ServerActionEnum.UNRESCUE.value: conn.compute.unrescue_server,
164+
ServerActionEnum.START.value: conn.compute.start_server,
165+
ServerActionEnum.STOP.value: conn.compute.stop_server,
166+
ServerActionEnum.SHELVE.value: conn.compute.shelve_server,
167+
ServerActionEnum.SHELVE_OFFLOAD.value: conn.compute.shelve_offload_server,
168+
ServerActionEnum.UNSHELVE.value: conn.compute.unshelve_server,
167169
}
168170

169171
if action not in action_methods:
@@ -214,3 +216,28 @@ def delete_server(self, id: str) -> None:
214216
"""
215217
conn = get_openstack_conn()
216218
conn.compute.delete_server(id)
219+
220+
def attach_volume(
221+
self, server_id: str, volume_id: str, device: str | None = None
222+
) -> None:
223+
"""
224+
Attach a volume to a Compute server.
225+
226+
:param server_id: The UUID of the server.
227+
:param volume_id: The UUID of the volume to attach.
228+
:param device: Name of the device such as, /dev/vdb. If you specify this parameter, the device must not exist in the guest operating system.
229+
"""
230+
conn = get_openstack_conn()
231+
conn.compute.create_volume_attachment(
232+
server_id, volume_id=volume_id, device=device
233+
)
234+
235+
def detach_volume(self, server_id: str, volume_id: str) -> None:
236+
"""
237+
Detach a volume from a Compute server.
238+
239+
:param server_id: The UUID of the server.
240+
:param volume_id: The UUID of the volume to detach.
241+
"""
242+
conn = get_openstack_conn()
243+
conn.compute.delete_volume_attachment(server_id, volume_id)

src/openstack_mcp_server/tools/response/compute.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class IPAddress(BaseModel):
2020

2121
model_config = ConfigDict(validate_by_name=True)
2222

23+
class VolumeAttachment(BaseModel):
24+
id: str
25+
delete_on_termination: bool
26+
2327
class SecurityGroup(BaseModel):
2428
name: str
2529

@@ -35,6 +39,7 @@ class SecurityGroup(BaseModel):
3539
security_groups: list[SecurityGroup] | None = None
3640
accessIPv4: str | None = None
3741
accessIPv6: str | None = None
42+
attached_volumes: list[VolumeAttachment] | None = Field(default=None)
3843

3944

4045
class Flavor(BaseModel):

tests/tools/test_compute_tools.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,11 @@ def test_register_tools(self):
268268
call(compute_tools.action_server),
269269
call(compute_tools.update_server),
270270
call(compute_tools.delete_server),
271+
call(compute_tools.attach_volume),
272+
call(compute_tools.detach_volume),
271273
],
272274
)
273-
assert mock_tool_decorator.call_count == 7
275+
assert mock_tool_decorator.call_count == 9
274276

275277
def test_compute_tools_instantiation(self):
276278
"""Test ComputeTools can be instantiated."""
@@ -555,3 +557,90 @@ def test_delete_server_not_found(self, mock_get_openstack_conn):
555557
compute_tools.delete_server(server_id)
556558

557559
mock_conn.compute.delete_server.assert_called_once_with(server_id)
560+
561+
def test_attach_volume_success(self, mock_get_openstack_conn):
562+
"""Test attaching a volume to a server successfully."""
563+
mock_conn = mock_get_openstack_conn
564+
server_id = "test-server-id"
565+
volume_id = "test-volume-id"
566+
567+
mock_conn.compute.create_volume_attachment.return_value = None
568+
569+
compute_tools = ComputeTools()
570+
result = compute_tools.attach_volume(server_id, volume_id)
571+
572+
assert result is None
573+
mock_conn.compute.create_volume_attachment.assert_called_once_with(
574+
server_id, volume_id=volume_id, device=None
575+
)
576+
577+
def test_attach_volume_with_device(self, mock_get_openstack_conn):
578+
"""Test attaching a volume to a server with a specific device."""
579+
mock_conn = mock_get_openstack_conn
580+
server_id = "test-server-id"
581+
volume_id = "test-volume-id"
582+
device = "/dev/vdb"
583+
584+
mock_conn.compute.create_volume_attachment.return_value = None
585+
586+
compute_tools = ComputeTools()
587+
result = compute_tools.attach_volume(server_id, volume_id, device)
588+
589+
assert result is None
590+
mock_conn.compute.create_volume_attachment.assert_called_once_with(
591+
server_id, volume_id=volume_id, device=device
592+
)
593+
594+
def test_attach_volume_exception(self, mock_get_openstack_conn):
595+
"""Test attaching a volume when exception occurs."""
596+
mock_conn = mock_get_openstack_conn
597+
server_id = "test-server-id"
598+
volume_id = "test-volume-id"
599+
600+
mock_conn.compute.create_volume_attachment.side_effect = (
601+
NotFoundException()
602+
)
603+
604+
compute_tools = ComputeTools()
605+
606+
with pytest.raises(NotFoundException):
607+
compute_tools.attach_volume(server_id, volume_id)
608+
609+
mock_conn.compute.create_volume_attachment.assert_called_once_with(
610+
server_id, volume_id=volume_id, device=None
611+
)
612+
613+
def test_detach_volume_success(self, mock_get_openstack_conn):
614+
"""Test detaching a volume from a server successfully."""
615+
mock_conn = mock_get_openstack_conn
616+
server_id = "test-server-id"
617+
volume_id = "test-volume-id"
618+
619+
mock_conn.compute.delete_volume_attachment.return_value = None
620+
621+
compute_tools = ComputeTools()
622+
result = compute_tools.detach_volume(server_id, volume_id)
623+
624+
assert result is None
625+
mock_conn.compute.delete_volume_attachment.assert_called_once_with(
626+
server_id, volume_id
627+
)
628+
629+
def test_detach_volume_exception(self, mock_get_openstack_conn):
630+
"""Test detaching a volume when exception occurs."""
631+
mock_conn = mock_get_openstack_conn
632+
server_id = "test-server-id"
633+
volume_id = "test-volume-id"
634+
635+
mock_conn.compute.delete_volume_attachment.side_effect = (
636+
NotFoundException()
637+
)
638+
639+
compute_tools = ComputeTools()
640+
641+
with pytest.raises(NotFoundException):
642+
compute_tools.detach_volume(server_id, volume_id)
643+
644+
mock_conn.compute.delete_volume_attachment.assert_called_once_with(
645+
server_id, volume_id
646+
)

0 commit comments

Comments
 (0)