Skip to content

Commit 3920e57

Browse files
authored
Merge branch 'develop' into feat/identity-project
2 parents 823aa7d + d1a13f6 commit 3920e57

File tree

6 files changed

+269
-79
lines changed

6 files changed

+269
-79
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ share/python-wheels/
2626
.installed.cfg
2727
*.egg
2828
MANIFEST
29+
src/*/_version.py
2930

3031
# PyInstaller
3132
# Usually these files are written by a python script from a template
@@ -192,4 +193,4 @@ cython_debug/
192193
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
193194
# refer to https://docs.cursor.com/context/ignore-files
194195
.cursorignore
195-
.cursorindexingignore
196+
.cursorindexingignore

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/image_tools.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,43 @@ def register_tools(self, mcp: FastMCP):
1616
Register Image-related tools with the FastMCP instance.
1717
"""
1818

19-
mcp.tool()(self.get_image_images)
19+
mcp.tool()(self.get_images)
2020
mcp.tool()(self.create_image)
2121

22-
def get_image_images(self) -> str:
22+
def get_images(
23+
self,
24+
name: str | None = None,
25+
status: str | None = None,
26+
visibility: str | None = None,
27+
) -> list[Image]:
2328
"""
24-
Get the list of Image images by invoking the registered tool.
29+
Get the list of OpenStack images with optional filtering.
2530
26-
:return: A string containing the names, IDs, and statuses of the images.
31+
The filtering behavior is as follows:
32+
- By default, all available images are returned without any filtering applied.
33+
- Filters are only applied when specific values are provided by the user.
34+
35+
:param name: Filter by image name
36+
:param status: Filter by status
37+
:param visibility: Filter by visibility
38+
:return: A list of Image objects.
2739
"""
28-
# Initialize connection
2940
conn = get_openstack_conn()
3041

31-
# List the servers
42+
# Build filters for the image query
43+
filters = {}
44+
if name and name.strip():
45+
filters["name"] = name.strip()
46+
if status and status.strip():
47+
filters["status"] = status.strip()
48+
if visibility and visibility.strip():
49+
filters["visibility"] = visibility.strip()
50+
3251
image_list = []
33-
for image in conn.image.images():
34-
image_list.append(
35-
f"{image.name} ({image.id}) - Status: {image.status}",
36-
)
52+
for image in conn.image.images(**filters):
53+
image_list.append(Image(**image))
3754

38-
return "\n".join(image_list)
55+
return image_list
3956

4057
def create_image(self, image_data: CreateImage) -> Image:
4158
"""Create a new Openstack image.

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)