From 0f071d4ba32896c5b2d42d85e64bc832703a129a Mon Sep 17 00:00:00 2001 From: S0okJu Date: Thu, 25 Sep 2025 17:33:40 +0900 Subject: [PATCH 1/6] feat(block): Add get_attachment_id tool(#70) - Add get_attachmet_id that return attchement details - Tests are updated and passing --- .../tools/block_storage_tools.py | 44 ++++++++++++- .../tools/response/block_storage.py | 23 +++++++ tests/tools/test_block_storage_tools.py | 63 ++++++++++++++++++- 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/src/openstack_mcp_server/tools/block_storage_tools.py b/src/openstack_mcp_server/tools/block_storage_tools.py index d682ebd..43e8f78 100644 --- a/src/openstack_mcp_server/tools/block_storage_tools.py +++ b/src/openstack_mcp_server/tools/block_storage_tools.py @@ -2,6 +2,8 @@ from .base import get_openstack_conn from .response.block_storage import ( + Attachment, + ConnectionInfo, Volume, VolumeAttachment, ) @@ -22,6 +24,8 @@ def register_tools(self, mcp: FastMCP): mcp.tool()(self.delete_volume) mcp.tool()(self.extend_volume) + mcp.tool()(self.get_attachment_details) + def get_volumes(self) -> list[Volume]: """ Get the list of Block Storage volumes. @@ -39,7 +43,7 @@ def get_volumes(self) -> list[Volume]: VolumeAttachment( server_id=attachment.get("server_id"), device=attachment.get("device"), - attachment_id=attachment.get("id"), + attachment_id=attachment.get("attachment_id"), ), ) @@ -183,3 +187,41 @@ def extend_volume(self, volume_id: str, new_size: int) -> None: conn = get_openstack_conn() conn.block_storage.extend_volume(volume_id, new_size) + + def get_attachment_details(self, attachment_id: str) -> Attachment: + """ + Get detailed information about a specific attachment. + + :param attachment_id: The ID of the attachment to get details for + :return: An Attachment object with detailed information + """ + conn = get_openstack_conn() + + attachment = conn.block_storage.get_attachment(attachment_id) + + # NOTE: We exclude the auth_* fields for security reasons + connection_info = attachment.connection_info + filtered_connection_info = ConnectionInfo( + access_mode=connection_info.get("access_mode"), + cacheable=connection_info.get("cacheable"), + driver_volume_type=connection_info.get("driver_volume_type"), + encrypted=connection_info.get("encrypted"), + qos_specs=connection_info.get("qos_specs"), + target_discovered=connection_info.get("target_discovered"), + target_iqn=connection_info.get("target_iqn"), + target_lun=connection_info.get("target_lun"), + target_portal=connection_info.get("target_portal"), + ) + + params = { + "id": attachment.id, + "instance": attachment.instance, + "volume_id": attachment.volume_id, + "attached_at": attachment.attached_at, + "detached_at": attachment.detached_at, + "attach_mode": attachment.attach_mode, + "connection_info": filtered_connection_info, + "connector": attachment.connector, + } + + return Attachment(**params) diff --git a/src/openstack_mcp_server/tools/response/block_storage.py b/src/openstack_mcp_server/tools/response/block_storage.py index d4f8be3..ae7eee5 100644 --- a/src/openstack_mcp_server/tools/response/block_storage.py +++ b/src/openstack_mcp_server/tools/response/block_storage.py @@ -19,3 +19,26 @@ class Volume(BaseModel): is_encrypted: bool | None = None description: str | None = None attachments: list[VolumeAttachment] = [] + + +class ConnectionInfo(BaseModel): + access_mode: str | None = None + cacheable: bool | None = None + driver_volume_type: str | None = None + encrypted: bool | None = None + qos_specs: str | None = None + target_discovered: bool | None = None + target_iqn: str | None = None + target_lun: int | None = None + target_portal: str | None = None + + +class Attachment(BaseModel): + id: str + instance: str + volume_id: str + attached_at: str | None = None + detached_at: str | None = None + attach_mode: str | None = None + connection_info: ConnectionInfo | None = None + connector: str | None = None diff --git a/tests/tools/test_block_storage_tools.py b/tests/tools/test_block_storage_tools.py index 305f66e..e408e5f 100644 --- a/tests/tools/test_block_storage_tools.py +++ b/tests/tools/test_block_storage_tools.py @@ -4,6 +4,8 @@ from openstack_mcp_server.tools.block_storage_tools import BlockStorageTools from openstack_mcp_server.tools.response.block_storage import ( + Attachment, + ConnectionInfo, Volume, VolumeAttachment, ) @@ -614,7 +616,7 @@ def test_register_tools(self): block_storage_tools.register_tools(mock_mcp) # Verify mcp.tool() was called for each method - assert mock_mcp.tool.call_count == 5 + assert mock_mcp.tool.call_count == 6 # Verify all methods were registered registered_methods = [ @@ -683,3 +685,62 @@ def test_all_block_storage_methods_have_docstrings(self): assert len(docstring.strip()) > 0, ( f"{method_name} docstring should not be empty" ) + + def test_get_attachment_details( + self, mock_get_openstack_conn_block_storage + ): + """Test getting attachment details.""" + + # Set up the attachment mock object + mock_attachment = Mock() + mock_attachment.id = "attach-123" + mock_attachment.instance = "server-123" + mock_attachment.volume_id = "vol-123" + mock_attachment.attached_at = "2024-01-01T12:00:00Z" + mock_attachment.detached_at = None + mock_attachment.attach_mode = "attach" + mock_attachment.connection_info = { + "access_mode": "rw", + "cacheable": True, + "driver_volume_type": "iscsi", + "encrypted": False, + "qos_specs": None, + "target_discovered": True, + "target_iqn": "iqn.2024-01-01.com.example:volume-123", + "target_lun": 0, + "target_portal": "192.168.1.100:3260", + } + mock_attachment.connector = "connector-123" + + # Configure the mock block_storage.get_attachment() + mock_conn = mock_get_openstack_conn_block_storage + mock_conn.block_storage.get_attachment.return_value = mock_attachment + + block_storage_tools = BlockStorageTools() + result = block_storage_tools.get_attachment_details("attach-123") + + # Verify the result + assert isinstance(result, Attachment) + assert result.id == "attach-123" + assert result.instance == "server-123" + assert result.attached_at == "2024-01-01T12:00:00Z" + assert result.detached_at is None + assert result.attach_mode == "attach" + assert result.connection_info == ConnectionInfo( + access_mode="rw", + cacheable=True, + driver_volume_type="iscsi", + encrypted=False, + qos_specs=None, + target_discovered=True, + target_iqn="iqn.2024-01-01.com.example:volume-123", + target_lun=0, + target_portal="192.168.1.100:3260", + ) + assert result.connector == "connector-123" + assert result.volume_id == "vol-123" + + # Verify the mock calls + mock_conn.block_storage.get_attachment.assert_called_once_with( + "attach-123" + ) From 0b84ea2d76f6baeffaeb5bca939f5bac30049341 Mon Sep 17 00:00:00 2001 From: S0okJu Date: Fri, 3 Oct 2025 12:49:11 +0900 Subject: [PATCH 2/6] fix(block): Delete tool counting test --- tests/tools/test_block_storage_tools.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/tools/test_block_storage_tools.py b/tests/tools/test_block_storage_tools.py index e408e5f..744173a 100644 --- a/tests/tools/test_block_storage_tools.py +++ b/tests/tools/test_block_storage_tools.py @@ -615,9 +615,6 @@ def test_register_tools(self): block_storage_tools = BlockStorageTools() block_storage_tools.register_tools(mock_mcp) - # Verify mcp.tool() was called for each method - assert mock_mcp.tool.call_count == 6 - # Verify all methods were registered registered_methods = [ call[0][0] for call in mock_tool_decorator.call_args_list From e6491222b307bd19ceb6831963955550d9ee4ab7 Mon Sep 17 00:00:00 2001 From: S0okJu Date: Sun, 12 Oct 2025 22:04:50 +0900 Subject: [PATCH 3/6] feat(cinder): Add get attachments tool(#70) - Add get attachments tool - Tests are updated and passing --- .../tools/block_storage_tools.py | 33 +++++++++++++++++++ .../tools/response/block_storage.py | 7 ++++ tests/tools/test_block_storage_tools.py | 33 +++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/src/openstack_mcp_server/tools/block_storage_tools.py b/src/openstack_mcp_server/tools/block_storage_tools.py index 43e8f78..3d047f9 100644 --- a/src/openstack_mcp_server/tools/block_storage_tools.py +++ b/src/openstack_mcp_server/tools/block_storage_tools.py @@ -3,6 +3,7 @@ from .base import get_openstack_conn from .response.block_storage import ( Attachment, + AttachmentSummary, ConnectionInfo, Volume, VolumeAttachment, @@ -25,6 +26,7 @@ def register_tools(self, mcp: FastMCP): mcp.tool()(self.extend_volume) mcp.tool()(self.get_attachment_details) + mcp.tool()(self.get_attachments) def get_volumes(self) -> list[Volume]: """ @@ -225,3 +227,34 @@ def get_attachment_details(self, attachment_id: str) -> Attachment: } return Attachment(**params) + + def get_attachments( + self, + volume_id: str | None = None, + instance: str | None = None, + ) -> list[Attachment]: + """ + Get the list of attachments. + + :return: A list of Attachment objects. + """ + conn = get_openstack_conn() + + filter = {} + if volume_id: + filter["volume_id"] = volume_id + if instance: + filter["instance"] = instance + + attachments = [] + for attachment in conn.block_storage.attachments(**filter): + attachments.append( + AttachmentSummary( + id=attachment.id, + instance=attachment.instance, + volume_id=attachment.volume_id, + status=attachment.status, + ) + ) + + return attachments diff --git a/src/openstack_mcp_server/tools/response/block_storage.py b/src/openstack_mcp_server/tools/response/block_storage.py index ae7eee5..3d59f69 100644 --- a/src/openstack_mcp_server/tools/response/block_storage.py +++ b/src/openstack_mcp_server/tools/response/block_storage.py @@ -42,3 +42,10 @@ class Attachment(BaseModel): attach_mode: str | None = None connection_info: ConnectionInfo | None = None connector: str | None = None + + +class AttachmentSummary(BaseModel): + id: str + instance: str + volume_id: str + status: str diff --git a/tests/tools/test_block_storage_tools.py b/tests/tools/test_block_storage_tools.py index 744173a..dbf5e6a 100644 --- a/tests/tools/test_block_storage_tools.py +++ b/tests/tools/test_block_storage_tools.py @@ -741,3 +741,36 @@ def test_get_attachment_details( mock_conn.block_storage.get_attachment.assert_called_once_with( "attach-123" ) + + def test_get_attachments(self, mock_get_openstack_conn_block_storage): + """Test getting attachments.""" + mock_conn = mock_get_openstack_conn_block_storage + + # Create mock attachment object + mock_attachment = Mock() + mock_attachment.id = "attach-123" + mock_attachment.instance = "server-123" + mock_attachment.volume_id = "vol-123" + mock_attachment.status = "attached" + + mock_conn.block_storage.attachments.return_value = [mock_attachment] + + # Test attachments + block_storage_tools = BlockStorageTools() + + filter = { + "volume_id": "vol-123", + "instance": "server-123", + } + result = block_storage_tools.get_attachments(**filter) + + # Verify the result + assert isinstance(result, list) + assert len(result) == 1 + assert result[0].id == "attach-123" + assert result[0].instance == "server-123" + assert result[0].volume_id == "vol-123" + assert result[0].status == "attached" + + # Verify the mock calls + mock_conn.block_storage.attachments.assert_called_once_with(**filter) From 103b84c71e621e397c560984bf09e621070ee2a6 Mon Sep 17 00:00:00 2001 From: S0okJu Date: Sun, 12 Oct 2025 22:12:51 +0900 Subject: [PATCH 4/6] feat(cinder): Add get attachments tool(#70) - Add get attachments tool - Tests are updated and passing --- src/openstack_mcp_server/tools/block_storage_tools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/openstack_mcp_server/tools/block_storage_tools.py b/src/openstack_mcp_server/tools/block_storage_tools.py index bfa5c8f..5b8d85e 100644 --- a/src/openstack_mcp_server/tools/block_storage_tools.py +++ b/src/openstack_mcp_server/tools/block_storage_tools.py @@ -236,6 +236,8 @@ def get_attachments( """ Get the list of attachments. + :param volume_id: The ID of the volume. + :param instance: The ID of the instance. :return: A list of Attachment objects. """ conn = get_openstack_conn() From ff266b0c96f06c0164132af790a4daa2ef2a4943 Mon Sep 17 00:00:00 2001 From: S0okJu Date: Sun, 12 Oct 2025 22:15:56 +0900 Subject: [PATCH 5/6] feat(cinder): Add get attachments tool(#70) - Add get attachments tool - Tests are updated and passing --- src/openstack_mcp_server/tools/response/block_storage.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/openstack_mcp_server/tools/response/block_storage.py b/src/openstack_mcp_server/tools/response/block_storage.py index 713a798..3d59f69 100644 --- a/src/openstack_mcp_server/tools/response/block_storage.py +++ b/src/openstack_mcp_server/tools/response/block_storage.py @@ -43,10 +43,9 @@ class Attachment(BaseModel): connection_info: ConnectionInfo | None = None connector: str | None = None - + class AttachmentSummary(BaseModel): id: str instance: str volume_id: str status: str - From a2394d36dd572e69093ffbe05fdfcb3dc888da0e Mon Sep 17 00:00:00 2001 From: S0okJu Date: Thu, 16 Oct 2025 15:12:25 +0900 Subject: [PATCH 6/6] fix(block): Change response object to Attachment in a get_attchments tool(#84) --- src/openstack_mcp_server/tools/block_storage_tools.py | 8 ++++++-- .../tools/response/block_storage.py | 7 ------- tests/tools/test_block_storage_tools.py | 11 ++++++++++- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/openstack_mcp_server/tools/block_storage_tools.py b/src/openstack_mcp_server/tools/block_storage_tools.py index 5b8d85e..5c558bd 100644 --- a/src/openstack_mcp_server/tools/block_storage_tools.py +++ b/src/openstack_mcp_server/tools/block_storage_tools.py @@ -3,7 +3,6 @@ from .base import get_openstack_conn from .response.block_storage import ( Attachment, - AttachmentSummary, ConnectionInfo, Volume, VolumeAttachment, @@ -251,11 +250,16 @@ def get_attachments( attachments = [] for attachment in conn.block_storage.attachments(**filter): attachments.append( - AttachmentSummary( + Attachment( id=attachment.id, instance=attachment.instance, volume_id=attachment.volume_id, status=attachment.status, + connection_info=attachment.connection_info, + attach_mode=attachment.attach_mode, + connector=attachment.connector, + attached_at=attachment.attached_at, + detached_at=attachment.detached_at, ) ) diff --git a/src/openstack_mcp_server/tools/response/block_storage.py b/src/openstack_mcp_server/tools/response/block_storage.py index 3d59f69..ae7eee5 100644 --- a/src/openstack_mcp_server/tools/response/block_storage.py +++ b/src/openstack_mcp_server/tools/response/block_storage.py @@ -42,10 +42,3 @@ class Attachment(BaseModel): attach_mode: str | None = None connection_info: ConnectionInfo | None = None connector: str | None = None - - -class AttachmentSummary(BaseModel): - id: str - instance: str - volume_id: str - status: str diff --git a/tests/tools/test_block_storage_tools.py b/tests/tools/test_block_storage_tools.py index 929a414..09b57e0 100644 --- a/tests/tools/test_block_storage_tools.py +++ b/tests/tools/test_block_storage_tools.py @@ -752,6 +752,11 @@ def test_get_attachments(self, mock_get_openstack_conn_block_storage): mock_attachment.instance = "server-123" mock_attachment.volume_id = "vol-123" mock_attachment.status = "attached" + mock_attachment.connection_info = None + mock_attachment.connector = None + mock_attachment.attach_mode = None + mock_attachment.attached_at = None + mock_attachment.detached_at = None mock_conn.block_storage.attachments.return_value = [mock_attachment] @@ -770,7 +775,11 @@ def test_get_attachments(self, mock_get_openstack_conn_block_storage): assert result[0].id == "attach-123" assert result[0].instance == "server-123" assert result[0].volume_id == "vol-123" - assert result[0].status == "attached" + assert result[0].attached_at is None + assert result[0].detached_at is None + assert result[0].attach_mode is None + assert result[0].connection_info is None + assert result[0].connector is None # Verify the mock calls mock_conn.block_storage.attachments.assert_called_once_with(**filter)