Skip to content

Commit 09578a5

Browse files
Merge pull request #39 from openstack-kr/feature/cinder-tools
2 parents 225f174 + 29ce13f commit 09578a5

File tree

5 files changed

+857
-0
lines changed

5 files changed

+857
-0
lines changed

src/openstack_mcp_server/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ def register_tool(mcp: FastMCP):
1010
from .identity_tools import IdentityTools
1111
from .image_tools import ImageTools
1212
from .network_tools import NetworkTools
13+
from .block_storage_tools import BlockStorageTools
1314

1415
ComputeTools().register_tools(mcp)
1516
ImageTools().register_tools(mcp)
1617
IdentityTools().register_tools(mcp)
1718
NetworkTools().register_tools(mcp)
19+
BlockStorageTools().register_tools(mcp)
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
from .response.block_storage import (
2+
Volume,
3+
VolumeAttachment,
4+
)
5+
from .base import get_openstack_conn
6+
from fastmcp import FastMCP
7+
8+
9+
class BlockStorageTools:
10+
"""
11+
A class to encapsulate Block Storage-related tools and utilities.
12+
"""
13+
14+
def register_tools(self, mcp: FastMCP):
15+
"""
16+
Register Block Storage-related tools with the FastMCP instance.
17+
"""
18+
mcp.tool()(self.get_volumes)
19+
mcp.tool()(self.get_volume_details)
20+
mcp.tool()(self.create_volume)
21+
mcp.tool()(self.delete_volume)
22+
mcp.tool()(self.extend_volume)
23+
24+
def get_volumes(self) -> list[Volume]:
25+
"""
26+
Get the list of Block Storage volumes.
27+
28+
:return: A list of Volume objects representing the volumes.
29+
"""
30+
conn = get_openstack_conn()
31+
32+
# List the volumes
33+
volume_list = []
34+
for volume in conn.block_storage.volumes():
35+
attachments = []
36+
for attachment in volume.attachments or []:
37+
attachments.append(
38+
VolumeAttachment(
39+
server_id=attachment.get("server_id"),
40+
device=attachment.get("device"),
41+
attachment_id=attachment.get("id"),
42+
)
43+
)
44+
45+
volume_list.append(
46+
Volume(
47+
id=volume.id,
48+
name=volume.name,
49+
status=volume.status,
50+
size=volume.size,
51+
volume_type=volume.volume_type,
52+
availability_zone=volume.availability_zone,
53+
created_at=str(volume.created_at)
54+
if volume.created_at
55+
else None,
56+
is_bootable=volume.is_bootable,
57+
is_encrypted=volume.is_encrypted,
58+
description=volume.description,
59+
attachments=attachments,
60+
)
61+
)
62+
63+
return volume_list
64+
65+
def get_volume_details(self, volume_id: str) -> Volume:
66+
"""
67+
Get detailed information about a specific volume.
68+
69+
:param volume_id: The ID of the volume to get details for
70+
:return: A Volume object with detailed information
71+
"""
72+
conn = get_openstack_conn()
73+
74+
volume = conn.block_storage.get_volume(volume_id)
75+
76+
attachments = []
77+
for attachment in volume.attachments or []:
78+
attachments.append(
79+
VolumeAttachment(
80+
server_id=attachment.get("server_id"),
81+
device=attachment.get("device"),
82+
attachment_id=attachment.get("id"),
83+
)
84+
)
85+
86+
return Volume(
87+
id=volume.id,
88+
name=volume.name,
89+
status=volume.status,
90+
size=volume.size,
91+
volume_type=volume.volume_type,
92+
availability_zone=volume.availability_zone,
93+
created_at=str(volume.created_at),
94+
is_bootable=volume.is_bootable,
95+
is_encrypted=volume.is_encrypted,
96+
description=volume.description,
97+
attachments=attachments,
98+
)
99+
100+
def create_volume(
101+
self,
102+
name: str,
103+
size: int,
104+
description: str | None = None,
105+
volume_type: str | None = None,
106+
availability_zone: str | None = None,
107+
bootable: bool | None = None,
108+
image: str | None = None,
109+
) -> Volume:
110+
"""
111+
Create a new volume.
112+
113+
:param name: Name for the new volume
114+
:param size: Size of the volume in GB
115+
:param description: Optional description for the volume
116+
:param volume_type: Optional volume type
117+
:param availability_zone: Optional availability zone
118+
:param bootable: Optional flag to make the volume bootable
119+
:param image: Optional Image name, ID or object from which to create
120+
:return: The created Volume object
121+
"""
122+
conn = get_openstack_conn()
123+
124+
volume_kwargs = {
125+
"name": name,
126+
}
127+
128+
if description is not None:
129+
volume_kwargs["description"] = description
130+
if volume_type is not None:
131+
volume_kwargs["volume_type"] = volume_type
132+
if availability_zone is not None:
133+
volume_kwargs["availability_zone"] = availability_zone
134+
135+
volume = conn.block_storage.create_volume(
136+
size=size, image=image, bootable=bootable, **volume_kwargs
137+
)
138+
139+
volume_obj = Volume(
140+
id=volume.id,
141+
name=volume.name,
142+
status=volume.status,
143+
size=volume.size,
144+
volume_type=volume.volume_type,
145+
availability_zone=volume.availability_zone,
146+
created_at=str(volume.created_at),
147+
is_bootable=volume.is_bootable,
148+
is_encrypted=volume.is_encrypted,
149+
description=volume.description,
150+
attachments=[],
151+
)
152+
153+
return volume_obj
154+
155+
def delete_volume(self, volume_id: str, force: bool = False) -> None:
156+
"""
157+
Delete a volume.
158+
159+
:param volume_id: The ID of the volume to delete
160+
:param force: Whether to force delete the volume
161+
:return: None
162+
"""
163+
conn = get_openstack_conn()
164+
165+
conn.block_storage.delete_volume(volume_id, force=force, ignore_missing=False)
166+
167+
def extend_volume(self, volume_id: str, new_size: int) -> None:
168+
"""
169+
Extend a volume to a new size.
170+
171+
:param volume_id: The ID of the volume to extend
172+
:param new_size: The new size in GB (must be larger than current size)
173+
:return: None
174+
"""
175+
conn = get_openstack_conn()
176+
177+
conn.block_storage.extend_volume(volume_id, new_size)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from pydantic import BaseModel
2+
3+
4+
class VolumeAttachment(BaseModel):
5+
server_id: str | None = None
6+
device: str | None = None
7+
attachment_id: str | None = None
8+
9+
10+
class Volume(BaseModel):
11+
id: str
12+
name: str | None = None
13+
status: str
14+
size: int
15+
volume_type: str | None = None
16+
availability_zone: str | None = None
17+
created_at: str
18+
is_bootable: bool | None = None
19+
is_encrypted: bool | None = None
20+
description: str | None = None
21+
attachments: list[VolumeAttachment] = []

tests/conftest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,14 @@ def mock_openstack_connect_network():
6161
return_value=mock_conn,
6262
):
6363
yield mock_conn
64+
65+
@pytest.fixture
66+
def mock_get_openstack_conn_block_storage():
67+
"""Mock get_openstack_conn function for block_storage_tools."""
68+
mock_conn = Mock()
69+
70+
with patch(
71+
"openstack_mcp_server.tools.block_storage_tools.get_openstack_conn",
72+
return_value=mock_conn,
73+
):
74+
yield mock_conn

0 commit comments

Comments
 (0)