From 079f7917ec2fd6b51f8d61e8f8e7fcea0e60f713 Mon Sep 17 00:00:00 2001 From: choieastsea Date: Wed, 13 Aug 2025 20:57:36 +0900 Subject: [PATCH 1/3] feat: get flavors --- .../tools/compute_tools.py | 21 ++++- .../tools/response/compute.py | 20 ++++- tests/tools/test_compute_tools.py | 78 +++++++++++++++++-- 3 files changed, 106 insertions(+), 13 deletions(-) diff --git a/src/openstack_mcp_server/tools/compute_tools.py b/src/openstack_mcp_server/tools/compute_tools.py index ec61e89..dbb4d7d 100644 --- a/src/openstack_mcp_server/tools/compute_tools.py +++ b/src/openstack_mcp_server/tools/compute_tools.py @@ -2,7 +2,10 @@ from fastmcp import FastMCP -from openstack_mcp_server.tools.response.compute import Server +from openstack_mcp_server.tools.response.compute import ( + Flavor, + Server, +) from .base import get_openstack_conn @@ -16,10 +19,10 @@ def register_tools(self, mcp: FastMCP): """ Register Compute-related tools with the FastMCP instance. """ - mcp.tool()(self.get_servers) mcp.tool()(self.get_server) mcp.tool()(self.create_server) + mcp.tool()(self.get_flavors) def get_servers(self) -> list[Server]: """ @@ -60,7 +63,7 @@ def create_server( :param name: The name of the server. :param image: The ID of the image to use. - :param flavor: The (integer) ID of the flavor to use. + :param flavor: The ID of the flavor to use. :param network: The ID of the network to attach. :param key_name: The name of the key pair to use. :param security_groups: A list of security group names to attach. @@ -87,3 +90,15 @@ def create_server( server = conn.compute.get_server(resp.id) return Server(**server) + + def get_flavors(self) -> list[Flavor]: + """ + Get flavors (server hardware configurations). + + :return: A list of Flavor objects. + """ + conn = get_openstack_conn() + flavor_list = [] + for flavor in conn.compute.flavors(): + flavor_list.append(Flavor(**flavor)) + return flavor_list diff --git a/src/openstack_mcp_server/tools/response/compute.py b/src/openstack_mcp_server/tools/response/compute.py index 86f2e64..0c9b3b7 100644 --- a/src/openstack_mcp_server/tools/response/compute.py +++ b/src/openstack_mcp_server/tools/response/compute.py @@ -1,13 +1,13 @@ from pydantic import BaseModel, ConfigDict, Field -class Flavor(BaseModel): +class ServerFlavor(BaseModel): id: str | None = Field(default=None, exclude=True) name: str = Field(validation_alias="original_name") model_config = ConfigDict(validate_by_name=True) -class Image(BaseModel): +class ServerImage(BaseModel): id: str @@ -27,8 +27,20 @@ class Server(BaseModel): id: str name: str status: str | None = None - flavor: Flavor | None = None - image: Image | None = None + flavor: ServerFlavor | None = None + image: ServerImage | None = None addresses: dict[str, list[ServerIp]] | None = None key_name: str | None = None security_groups: list[ServerSecurityGroup] | None = None + + +class Flavor(BaseModel): + id: str + name: str + vcpus: int + ram: int + disk: int + swap: int | None = None + is_public: bool = Field(validation_alias="os-flavor-access:is_public") + + model_config = ConfigDict(validate_by_name=True) diff --git a/tests/tools/test_compute_tools.py b/tests/tools/test_compute_tools.py index f21f17f..c45c450 100644 --- a/tests/tools/test_compute_tools.py +++ b/tests/tools/test_compute_tools.py @@ -3,8 +3,9 @@ from openstack_mcp_server.tools.compute_tools import ComputeTools from openstack_mcp_server.tools.response.compute import ( Flavor, - Image, Server, + ServerFlavor, + ServerImage, ServerIp, ServerSecurityGroup, ) @@ -79,8 +80,8 @@ def test_get_servers_success(self, mock_get_openstack_conn): id="434eb822-3fbd-44a1-a000-3b511ac9b516", name="web-server-01", status="ACTIVE", - flavor=Flavor(id=None, name="m1.tiny"), - image=Image(id="de527f30-d078-41f4-8f18-a23bf2d39366"), + flavor=ServerFlavor(id=None, name="m1.tiny"), + image=ServerImage(id="de527f30-d078-41f4-8f18-a23bf2d39366"), addresses={ "private": [ ServerIp(addr="192.168.1.10", version=4, type="fixed"), @@ -93,8 +94,8 @@ def test_get_servers_success(self, mock_get_openstack_conn): id="ffd071fe-1334-45f6-8894-5b0bcac262a6", name="db-server-01", status="SHUTOFF", - flavor=Flavor(id=None, name="m1.small"), - image=Image(id="3d897e0e-4117-46bb-ae77-e734bb16a1ca"), + flavor=ServerFlavor(id=None, name="m1.small"), + image=ServerImage(id="3d897e0e-4117-46bb-ae77-e734bb16a1ca"), addresses={ "net1": [ ServerIp(addr="192.168.1.11", version=4, type="fixed"), @@ -258,9 +259,10 @@ def test_register_tools(self): call(compute_tools.get_servers), call(compute_tools.get_server), call(compute_tools.create_server), + call(compute_tools.get_flavors), ], ) - assert mock_tool_decorator.call_count == 3 + assert mock_tool_decorator.call_count == 4 def test_compute_tools_instantiation(self): """Test ComputeTools can be instantiated.""" @@ -279,3 +281,67 @@ def test_get_servers_docstring(self): assert docstring is not None assert "Get the list of Compute servers" in docstring assert "return" in docstring.lower() or "Return" in docstring + + def test_get_flavors_success(self, mock_get_openstack_conn): + """Test getting flavors successfully.""" + mock_conn = mock_get_openstack_conn + + # Create mock flavor objects + mock_flavor1 = { + "id": "1", + "name": "m1.tiny", + "vcpus": 1, + "ram": 512, + "disk": 1, + "swap": 0, + "os-flavor-access:is_public": True, + } + + mock_flavor2 = { + "id": "2", + "name": "m1.small", + "vcpus": 2, + "ram": 2048, + "disk": 20, + "swap": 0, + "os-flavor-access:is_public": True, + } + + mock_conn.compute.flavors.return_value = [mock_flavor1, mock_flavor2] + + compute_tools = ComputeTools() + result = compute_tools.get_flavors() + + expected_output = [ + Flavor( + id="1", + name="m1.tiny", + vcpus=1, + ram=512, + disk=1, + swap=0, + is_public=True, + ), + Flavor( + id="2", + name="m1.small", + vcpus=2, + ram=2048, + disk=20, + swap=0, + is_public=True, + ), + ] + assert result == expected_output + mock_conn.compute.flavors.assert_called_once() + + def test_get_flavors_empty_list(self, mock_get_openstack_conn): + """Test getting flavors when no flavors exist.""" + mock_conn = mock_get_openstack_conn + mock_conn.compute.flavors.return_value = [] + + compute_tools = ComputeTools() + result = compute_tools.get_flavors() + + assert result == [] + mock_conn.compute.flavors.assert_called_once() From 34c1386eac1321a7487b8e6f3739ad99f791a8c4 Mon Sep 17 00:00:00 2001 From: choieastsea Date: Wed, 13 Aug 2025 20:57:36 +0900 Subject: [PATCH 2/3] fix: to inner class --- .../tools/response/compute.py | 40 +++++++++---------- tests/tools/test_compute_tools.py | 27 +++++-------- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/src/openstack_mcp_server/tools/response/compute.py b/src/openstack_mcp_server/tools/response/compute.py index 0c9b3b7..5056911 100644 --- a/src/openstack_mcp_server/tools/response/compute.py +++ b/src/openstack_mcp_server/tools/response/compute.py @@ -1,37 +1,33 @@ from pydantic import BaseModel, ConfigDict, Field -class ServerFlavor(BaseModel): - id: str | None = Field(default=None, exclude=True) - name: str = Field(validation_alias="original_name") - model_config = ConfigDict(validate_by_name=True) - - -class ServerImage(BaseModel): - id: str - +class Server(BaseModel): + class Flavor(BaseModel): + id: str | None = Field(default=None, exclude=True) + name: str = Field(validation_alias="original_name") + model_config = ConfigDict(validate_by_name=True) -class ServerIp(BaseModel): - addr: str - version: int - type: str = Field(validation_alias="OS-EXT-IPS:type") + class Image(BaseModel): + id: str - model_config = ConfigDict(validate_by_name=True) + class IPAddress(BaseModel): + addr: str + version: int + type: str = Field(validation_alias="OS-EXT-IPS:type") + model_config = ConfigDict(validate_by_name=True) -class ServerSecurityGroup(BaseModel): - name: str + class SecurityGroup(BaseModel): + name: str - -class Server(BaseModel): id: str name: str status: str | None = None - flavor: ServerFlavor | None = None - image: ServerImage | None = None - addresses: dict[str, list[ServerIp]] | 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[ServerSecurityGroup] | None = None + security_groups: list[SecurityGroup] | None = None class Flavor(BaseModel): diff --git a/tests/tools/test_compute_tools.py b/tests/tools/test_compute_tools.py index c45c450..7bfab26 100644 --- a/tests/tools/test_compute_tools.py +++ b/tests/tools/test_compute_tools.py @@ -1,14 +1,7 @@ from unittest.mock import Mock, call from openstack_mcp_server.tools.compute_tools import ComputeTools -from openstack_mcp_server.tools.response.compute import ( - Flavor, - Server, - ServerFlavor, - ServerImage, - ServerIp, - ServerSecurityGroup, -) +from openstack_mcp_server.tools.response.compute import Flavor, Server class TestComputeTools: @@ -80,31 +73,31 @@ def test_get_servers_success(self, mock_get_openstack_conn): id="434eb822-3fbd-44a1-a000-3b511ac9b516", name="web-server-01", status="ACTIVE", - flavor=ServerFlavor(id=None, name="m1.tiny"), - image=ServerImage(id="de527f30-d078-41f4-8f18-a23bf2d39366"), + flavor=Server.Flavor(id=None, name="m1.tiny"), + image=Server.Image(id="de527f30-d078-41f4-8f18-a23bf2d39366"), addresses={ "private": [ - ServerIp(addr="192.168.1.10", version=4, type="fixed"), + Server.IPAddress(addr="192.168.1.10", version=4, type="fixed"), ], }, key_name="my-key", - security_groups=[ServerSecurityGroup(name="default")], + security_groups=[Server.SecurityGroup(name="default")], ), Server( id="ffd071fe-1334-45f6-8894-5b0bcac262a6", name="db-server-01", status="SHUTOFF", - flavor=ServerFlavor(id=None, name="m1.small"), - image=ServerImage(id="3d897e0e-4117-46bb-ae77-e734bb16a1ca"), + flavor=Server.Flavor(id=None, name="m1.small"), + image=Server.Image(id="3d897e0e-4117-46bb-ae77-e734bb16a1ca"), addresses={ "net1": [ - ServerIp(addr="192.168.1.11", version=4, type="fixed"), + Server.IPAddress(addr="192.168.1.11", version=4, type="fixed"), ], }, key_name=None, security_groups=[ - ServerSecurityGroup(name="default"), - ServerSecurityGroup(name="group1"), + Server.SecurityGroup(name="default"), + Server.SecurityGroup(name="group1"), ], ), ] From bb453bc1d218c6efd745a7c21b6720831681bac4 Mon Sep 17 00:00:00 2001 From: choieastsea Date: Wed, 13 Aug 2025 21:00:28 +0900 Subject: [PATCH 3/3] fix: format --- tests/tools/test_compute_tools.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/tools/test_compute_tools.py b/tests/tools/test_compute_tools.py index 7bfab26..0db340f 100644 --- a/tests/tools/test_compute_tools.py +++ b/tests/tools/test_compute_tools.py @@ -77,7 +77,11 @@ def test_get_servers_success(self, mock_get_openstack_conn): image=Server.Image(id="de527f30-d078-41f4-8f18-a23bf2d39366"), addresses={ "private": [ - Server.IPAddress(addr="192.168.1.10", version=4, type="fixed"), + Server.IPAddress( + addr="192.168.1.10", + version=4, + type="fixed", + ), ], }, key_name="my-key", @@ -91,7 +95,11 @@ def test_get_servers_success(self, mock_get_openstack_conn): image=Server.Image(id="3d897e0e-4117-46bb-ae77-e734bb16a1ca"), addresses={ "net1": [ - Server.IPAddress(addr="192.168.1.11", version=4, type="fixed"), + Server.IPAddress( + addr="192.168.1.11", + version=4, + type="fixed", + ), ], }, key_name=None,