From 7d1967422f2ac3f3cc9ce57c54f8cd134a7f6b29 Mon Sep 17 00:00:00 2001 From: choieastsea Date: Tue, 19 Aug 2025 22:11:59 +0900 Subject: [PATCH 1/2] feat: rest server tools --- .../tools/compute_tools.py | 47 ++++++- .../tools/response/compute.py | 4 + tests/tools/test_compute_tools.py | 125 +++++++++++++++++- 3 files changed, 174 insertions(+), 2 deletions(-) diff --git a/src/openstack_mcp_server/tools/compute_tools.py b/src/openstack_mcp_server/tools/compute_tools.py index 90e14d8..bc4b963 100644 --- a/src/openstack_mcp_server/tools/compute_tools.py +++ b/src/openstack_mcp_server/tools/compute_tools.py @@ -43,6 +43,8 @@ def register_tools(self, mcp: FastMCP): mcp.tool()(self.create_server) mcp.tool()(self.get_flavors) mcp.tool()(self.action_server) + mcp.tool()(self.update_server) + mcp.tool()(self.delete_server) def get_servers(self) -> list[Server]: """ @@ -123,7 +125,7 @@ def get_flavors(self) -> list[Flavor]: flavor_list.append(Flavor(**flavor)) return flavor_list - def action_server(self, id: str, action: ServerActionEnum): + def action_server(self, id: str, action: ServerActionEnum) -> None: """ Perform an action on a Compute server. @@ -169,3 +171,46 @@ def action_server(self, id: str, action: ServerActionEnum): action_methods[action](id) return None + + def update_server( + self, + id: str, + accessIPv4: str | None = None, + accessIPv6: str | None = None, + name: str | None = None, + hostname: str | None = None, + description: str | None = None, + ) -> Server: + """ + Update a Compute server's name, hostname, or description. + + :param id: The UUID of the server. + :param accessIPv4: (Optional) IPv4 address that should be used to access this server. + :param accessIPv6: (Optional) IPv6 address that should be used to access this server. + :param name: (Optional) The server name. + :param hostname: (Optional) The hostname to configure for the instance in the metadata service. + :param description: (Optional) A free form description of the server. + :return: The updated Server object. + """ + conn = get_openstack_conn() + server_params = { + "accessIPv4": accessIPv4, + "accessIPv6": accessIPv6, + "name": name, + "hostname": hostname, + "description": description, + } + server_params = { + k: v for k, v in server_params.items() if v is not None + } + server = conn.compute.update_server(id, **server_params) + return Server(**server) + + def delete_server(self, id: str) -> None: + """ + Delete a Compute server. + + :param id: The UUID of the server. + """ + conn = get_openstack_conn() + conn.compute.delete_server(id) diff --git a/src/openstack_mcp_server/tools/response/compute.py b/src/openstack_mcp_server/tools/response/compute.py index 1cf8ccc..cc13d55 100644 --- a/src/openstack_mcp_server/tools/response/compute.py +++ b/src/openstack_mcp_server/tools/response/compute.py @@ -25,12 +25,16 @@ class SecurityGroup(BaseModel): id: str name: str + hostname: str | None = None + description: str | None = None status: str | None = None flavor: Flavor | None = None image: Image | None = None addresses: dict[str, list[IPAddress]] | None = None key_name: str | None = None security_groups: list[SecurityGroup] | None = None + accessIPv4: str | None = None + accessIPv6: str | None = None class Flavor(BaseModel): diff --git a/tests/tools/test_compute_tools.py b/tests/tools/test_compute_tools.py index f610718..5c78435 100644 --- a/tests/tools/test_compute_tools.py +++ b/tests/tools/test_compute_tools.py @@ -266,9 +266,11 @@ def test_register_tools(self): call(compute_tools.create_server), call(compute_tools.get_flavors), call(compute_tools.action_server), + call(compute_tools.update_server), + call(compute_tools.delete_server), ], ) - assert mock_tool_decorator.call_count == 5 + assert mock_tool_decorator.call_count == 7 def test_compute_tools_instantiation(self): """Test ComputeTools can be instantiated.""" @@ -432,3 +434,124 @@ def test_action_server_conflict_exception(self, mock_get_openstack_conn): compute_tools.action_server(server_id, action) mock_conn.compute.start_server.assert_called_once_with(server_id) + + def test_update_server_success(self, mock_get_openstack_conn): + """Test updating a server successfully with all parameters.""" + mock_conn = mock_get_openstack_conn + server_id = "test-server-id" + + mock_server = { + "name": "updated-server", + "id": server_id, + "status": "ACTIVE", + "hostname": "updated-hostname", + "description": "Updated server description", + "accessIPv4": "192.168.1.100", + "accessIPv6": "2001:db8::1", + } + + mock_conn.compute.update_server.return_value = mock_server + + compute_tools = ComputeTools() + server_params = mock_server.copy() + server_params.pop("status") + result = compute_tools.update_server(**server_params) + + expected_output = Server(**mock_server) + assert result == expected_output + + expected_params = { + "accessIPv4": "192.168.1.100", + "accessIPv6": "2001:db8::1", + "name": "updated-server", + "hostname": "updated-hostname", + "description": "Updated server description", + } + mock_conn.compute.update_server.assert_called_once_with( + server_id, **expected_params + ) + + @pytest.mark.parametrize( + "params", + [ + {"param_key": "name", "value": "new-name"}, + {"param_key": "hostname", "value": "new-hostname"}, + {"param_key": "description", "value": "New description"}, + {"param_key": "accessIPv4", "value": "192.168.1.100"}, + {"param_key": "accessIPv6", "value": "2001:db8::1"}, + ], + ) + def test_update_server_optional_params( + self, mock_get_openstack_conn, params + ): + """Test updating a server with optional parameters.""" + mock_conn = mock_get_openstack_conn + server_id = "test-server-id" + + mock_server = { + "id": server_id, + "name": "original-name", + "description": "Original description", + "hostname": "original-hostname", + "accessIPv4": "1.1.1.1", + "accessIPv6": "::", + "status": "ACTIVE", + **{params["param_key"]: params["value"]}, + } + + mock_conn.compute.update_server.return_value = mock_server + + compute_tools = ComputeTools() + result = compute_tools.update_server( + id=server_id, + **{params["param_key"]: params["value"]}, + ) + assert result == Server(**mock_server) + + expected_params = {params["param_key"]: params["value"]} + mock_conn.compute.update_server.assert_called_once_with( + server_id, **expected_params + ) + + def test_update_server_not_found(self, mock_get_openstack_conn): + """Test updating a server that does not exist.""" + mock_conn = mock_get_openstack_conn + server_id = "non-existent-server-id" + + # Mock the update_server method to raise NotFoundException + mock_conn.compute.update_server.side_effect = NotFoundException() + + compute_tools = ComputeTools() + + with pytest.raises(NotFoundException): + compute_tools.update_server(id=server_id) + + mock_conn.compute.update_server.assert_called_once_with(server_id) + + def test_delete_server_success(self, mock_get_openstack_conn): + """Test deleting a server successfully.""" + mock_conn = mock_get_openstack_conn + server_id = "test-server-id" + + mock_conn.compute.delete_server.return_value = None + + compute_tools = ComputeTools() + result = compute_tools.delete_server(server_id) + + assert result is None + mock_conn.compute.delete_server.assert_called_once_with(server_id) + + def test_delete_server_not_found(self, mock_get_openstack_conn): + """Test deleting a server that does not exist.""" + mock_conn = mock_get_openstack_conn + server_id = "non-existent-server-id" + + # Mock the delete_server method to raise NotFoundException + mock_conn.compute.delete_server.side_effect = NotFoundException() + + compute_tools = ComputeTools() + + with pytest.raises(NotFoundException): + compute_tools.delete_server(server_id) + + mock_conn.compute.delete_server.assert_called_once_with(server_id) From d0439edc9cc787dc16cd9822d07da66b9acf4422 Mon Sep 17 00:00:00 2001 From: choieastsea Date: Wed, 20 Aug 2025 09:37:03 +0900 Subject: [PATCH 2/2] docs: remove optional --- src/openstack_mcp_server/tools/compute_tools.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/openstack_mcp_server/tools/compute_tools.py b/src/openstack_mcp_server/tools/compute_tools.py index bc4b963..342c4cb 100644 --- a/src/openstack_mcp_server/tools/compute_tools.py +++ b/src/openstack_mcp_server/tools/compute_tools.py @@ -185,11 +185,11 @@ def update_server( Update a Compute server's name, hostname, or description. :param id: The UUID of the server. - :param accessIPv4: (Optional) IPv4 address that should be used to access this server. - :param accessIPv6: (Optional) IPv6 address that should be used to access this server. - :param name: (Optional) The server name. - :param hostname: (Optional) The hostname to configure for the instance in the metadata service. - :param description: (Optional) A free form description of the server. + :param accessIPv4: IPv4 address that should be used to access this server. + :param accessIPv6: IPv6 address that should be used to access this server. + :param name: The server name. + :param hostname: The hostname to configure for the instance in the metadata service. + :param description: A free form description of the server. :return: The updated Server object. """ conn = get_openstack_conn()