Skip to content

Commit beb214f

Browse files
choieastseahalucinor
authored andcommitted
feat: get flavors (#42)
* feat: get flavors * fix: to inner class * fix: format
1 parent c2be1f1 commit beb214f

File tree

3 files changed

+130
-40
lines changed

3 files changed

+130
-40
lines changed

src/openstack_mcp_server/tools/compute_tools.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
from fastmcp import FastMCP
44

5-
from openstack_mcp_server.tools.response.compute import Server
5+
from openstack_mcp_server.tools.response.compute import (
6+
Flavor,
7+
Server,
8+
)
69

710
from .base import get_openstack_conn
811

@@ -16,10 +19,10 @@ def register_tools(self, mcp: FastMCP):
1619
"""
1720
Register Compute-related tools with the FastMCP instance.
1821
"""
19-
2022
mcp.tool()(self.get_servers)
2123
mcp.tool()(self.get_server)
2224
mcp.tool()(self.create_server)
25+
mcp.tool()(self.get_flavors)
2326

2427
def get_servers(self) -> list[Server]:
2528
"""
@@ -60,7 +63,7 @@ def create_server(
6063
6164
:param name: The name of the server.
6265
:param image: The ID of the image to use.
63-
:param flavor: The (integer) ID of the flavor to use.
66+
:param flavor: The ID of the flavor to use.
6467
:param network: The ID of the network to attach.
6568
:param key_name: The name of the key pair to use.
6669
:param security_groups: A list of security group names to attach.
@@ -87,3 +90,15 @@ def create_server(
8790
server = conn.compute.get_server(resp.id)
8891

8992
return Server(**server)
93+
94+
def get_flavors(self) -> list[Flavor]:
95+
"""
96+
Get flavors (server hardware configurations).
97+
98+
:return: A list of Flavor objects.
99+
"""
100+
conn = get_openstack_conn()
101+
flavor_list = []
102+
for flavor in conn.compute.flavors():
103+
flavor_list.append(Flavor(**flavor))
104+
return flavor_list
Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,42 @@
11
from pydantic import BaseModel, ConfigDict, Field
22

33

4-
class Flavor(BaseModel):
5-
id: str | None = Field(default=None, exclude=True)
6-
name: str = Field(validation_alias="original_name")
7-
model_config = ConfigDict(validate_by_name=True)
8-
9-
10-
class Image(BaseModel):
11-
id: str
12-
13-
14-
class ServerIp(BaseModel):
15-
addr: str
16-
version: int
17-
type: str = Field(validation_alias="OS-EXT-IPS:type")
4+
class Server(BaseModel):
5+
class Flavor(BaseModel):
6+
id: str | None = Field(default=None, exclude=True)
7+
name: str = Field(validation_alias="original_name")
8+
model_config = ConfigDict(validate_by_name=True)
189

19-
model_config = ConfigDict(validate_by_name=True)
10+
class Image(BaseModel):
11+
id: str
2012

13+
class IPAddress(BaseModel):
14+
addr: str
15+
version: int
16+
type: str = Field(validation_alias="OS-EXT-IPS:type")
2117

22-
class ServerSecurityGroup(BaseModel):
23-
name: str
18+
model_config = ConfigDict(validate_by_name=True)
2419

20+
class SecurityGroup(BaseModel):
21+
name: str
2522

26-
class Server(BaseModel):
2723
id: str
2824
name: str
2925
status: str | None = None
3026
flavor: Flavor | None = None
3127
image: Image | None = None
32-
addresses: dict[str, list[ServerIp]] | None = None
28+
addresses: dict[str, list[IPAddress]] | None = None
3329
key_name: str | None = None
34-
security_groups: list[ServerSecurityGroup] | None = None
30+
security_groups: list[SecurityGroup] | None = None
31+
32+
33+
class Flavor(BaseModel):
34+
id: str
35+
name: str
36+
vcpus: int
37+
ram: int
38+
disk: int
39+
swap: int | None = None
40+
is_public: bool = Field(validation_alias="os-flavor-access:is_public")
41+
42+
model_config = ConfigDict(validate_by_name=True)

tests/tools/test_compute_tools.py

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
from unittest.mock import Mock, call
22

33
from openstack_mcp_server.tools.compute_tools import ComputeTools
4-
from openstack_mcp_server.tools.response.compute import (
5-
Flavor,
6-
Image,
7-
Server,
8-
ServerIp,
9-
ServerSecurityGroup,
10-
)
4+
from openstack_mcp_server.tools.response.compute import Flavor, Server
115

126

137
class TestComputeTools:
@@ -79,31 +73,39 @@ def test_get_servers_success(self, mock_get_openstack_conn):
7973
id="434eb822-3fbd-44a1-a000-3b511ac9b516",
8074
name="web-server-01",
8175
status="ACTIVE",
82-
flavor=Flavor(id=None, name="m1.tiny"),
83-
image=Image(id="de527f30-d078-41f4-8f18-a23bf2d39366"),
76+
flavor=Server.Flavor(id=None, name="m1.tiny"),
77+
image=Server.Image(id="de527f30-d078-41f4-8f18-a23bf2d39366"),
8478
addresses={
8579
"private": [
86-
ServerIp(addr="192.168.1.10", version=4, type="fixed"),
80+
Server.IPAddress(
81+
addr="192.168.1.10",
82+
version=4,
83+
type="fixed",
84+
),
8785
],
8886
},
8987
key_name="my-key",
90-
security_groups=[ServerSecurityGroup(name="default")],
88+
security_groups=[Server.SecurityGroup(name="default")],
9189
),
9290
Server(
9391
id="ffd071fe-1334-45f6-8894-5b0bcac262a6",
9492
name="db-server-01",
9593
status="SHUTOFF",
96-
flavor=Flavor(id=None, name="m1.small"),
97-
image=Image(id="3d897e0e-4117-46bb-ae77-e734bb16a1ca"),
94+
flavor=Server.Flavor(id=None, name="m1.small"),
95+
image=Server.Image(id="3d897e0e-4117-46bb-ae77-e734bb16a1ca"),
9896
addresses={
9997
"net1": [
100-
ServerIp(addr="192.168.1.11", version=4, type="fixed"),
98+
Server.IPAddress(
99+
addr="192.168.1.11",
100+
version=4,
101+
type="fixed",
102+
),
101103
],
102104
},
103105
key_name=None,
104106
security_groups=[
105-
ServerSecurityGroup(name="default"),
106-
ServerSecurityGroup(name="group1"),
107+
Server.SecurityGroup(name="default"),
108+
Server.SecurityGroup(name="group1"),
107109
],
108110
),
109111
]
@@ -258,9 +260,10 @@ def test_register_tools(self):
258260
call(compute_tools.get_servers),
259261
call(compute_tools.get_server),
260262
call(compute_tools.create_server),
263+
call(compute_tools.get_flavors),
261264
],
262265
)
263-
assert mock_tool_decorator.call_count == 3
266+
assert mock_tool_decorator.call_count == 4
264267

265268
def test_compute_tools_instantiation(self):
266269
"""Test ComputeTools can be instantiated."""
@@ -279,3 +282,67 @@ def test_get_servers_docstring(self):
279282
assert docstring is not None
280283
assert "Get the list of Compute servers" in docstring
281284
assert "return" in docstring.lower() or "Return" in docstring
285+
286+
def test_get_flavors_success(self, mock_get_openstack_conn):
287+
"""Test getting flavors successfully."""
288+
mock_conn = mock_get_openstack_conn
289+
290+
# Create mock flavor objects
291+
mock_flavor1 = {
292+
"id": "1",
293+
"name": "m1.tiny",
294+
"vcpus": 1,
295+
"ram": 512,
296+
"disk": 1,
297+
"swap": 0,
298+
"os-flavor-access:is_public": True,
299+
}
300+
301+
mock_flavor2 = {
302+
"id": "2",
303+
"name": "m1.small",
304+
"vcpus": 2,
305+
"ram": 2048,
306+
"disk": 20,
307+
"swap": 0,
308+
"os-flavor-access:is_public": True,
309+
}
310+
311+
mock_conn.compute.flavors.return_value = [mock_flavor1, mock_flavor2]
312+
313+
compute_tools = ComputeTools()
314+
result = compute_tools.get_flavors()
315+
316+
expected_output = [
317+
Flavor(
318+
id="1",
319+
name="m1.tiny",
320+
vcpus=1,
321+
ram=512,
322+
disk=1,
323+
swap=0,
324+
is_public=True,
325+
),
326+
Flavor(
327+
id="2",
328+
name="m1.small",
329+
vcpus=2,
330+
ram=2048,
331+
disk=20,
332+
swap=0,
333+
is_public=True,
334+
),
335+
]
336+
assert result == expected_output
337+
mock_conn.compute.flavors.assert_called_once()
338+
339+
def test_get_flavors_empty_list(self, mock_get_openstack_conn):
340+
"""Test getting flavors when no flavors exist."""
341+
mock_conn = mock_get_openstack_conn
342+
mock_conn.compute.flavors.return_value = []
343+
344+
compute_tools = ComputeTools()
345+
result = compute_tools.get_flavors()
346+
347+
assert result == []
348+
mock_conn.compute.flavors.assert_called_once()

0 commit comments

Comments
 (0)