Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/openstack_mcp_server/tools/compute_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]:
"""
Expand Down Expand Up @@ -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.
Expand All @@ -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
48 changes: 28 additions & 20 deletions src/openstack_mcp_server/tools/response/compute.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,42 @@
from pydantic import BaseModel, ConfigDict, Field


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 Image(BaseModel):
id: str


class ServerIp(BaseModel):
addr: str
version: int
type: str = Field(validation_alias="OS-EXT-IPS:type")
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)

model_config = ConfigDict(validate_by_name=True)
class Image(BaseModel):
id: str

class IPAddress(BaseModel):
addr: str
version: int
type: str = Field(validation_alias="OS-EXT-IPS:type")

class ServerSecurityGroup(BaseModel):
name: str
model_config = ConfigDict(validate_by_name=True)

class SecurityGroup(BaseModel):
name: str

class Server(BaseModel):
id: str
name: str
status: str | None = None
flavor: Flavor | None = None
image: Image | None = None
addresses: dict[str, list[ServerIp]] | 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
Comment on lines +4 to +30
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ❤️



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)
101 changes: 84 additions & 17 deletions tests/tools/test_compute_tools.py
Original file line number Diff line number Diff line change
@@ -1,13 +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,
Image,
Server,
ServerIp,
ServerSecurityGroup,
)
from openstack_mcp_server.tools.response.compute import Flavor, Server


class TestComputeTools:
Expand Down Expand Up @@ -79,31 +73,39 @@ 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=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=Flavor(id=None, name="m1.small"),
image=Image(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"),
],
),
]
Expand Down Expand Up @@ -258,9 +260,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."""
Expand All @@ -279,3 +282,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()