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..744173a 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, ) @@ -613,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 == 5 - # Verify all methods were registered registered_methods = [ call[0][0] for call in mock_tool_decorator.call_args_list @@ -683,3 +682,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" + )