Skip to content

Commit 10a990f

Browse files
authored
feat: Add neutron network function tools (#27)
* feat(neutron): Add network feature * feat(neutron): Add network feature test * chore(neutron): code linting * fix(neutron): remove comments * test(neutron): Remove unnecessary tests * fix(neutron): remove comments in test * fix(neutron): openstack connection call fix in confest
1 parent 555d7df commit 10a990f

File tree

6 files changed

+776
-7
lines changed

6 files changed

+776
-7
lines changed

src/openstack_mcp_server/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ def register_tool(mcp: FastMCP):
77
"""
88
from .glance_tools import GlanceTools
99
from .keystone_tools import KeystoneTools
10+
from .neutron_tools import NeutronTools
1011
from .nova_tools import NovaTools
1112

1213
NovaTools().register_tools(mcp)
1314
GlanceTools().register_tools(mcp)
1415
KeystoneTools().register_tools(mcp)
16+
NeutronTools().register_tools(mcp)
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
from .base import get_openstack_conn
2+
from fastmcp import FastMCP
3+
from openstack_mcp_server.tools.response.neutron import Network
4+
5+
6+
class NeutronTools:
7+
"""
8+
A class to encapsulate Neutron-related tools and utilities.
9+
"""
10+
11+
def register_tools(self, mcp: FastMCP):
12+
"""
13+
Register Neutron-related tools with the FastMCP instance.
14+
"""
15+
16+
mcp.tool()(self.get_neutron_networks)
17+
mcp.tool()(self.create_network)
18+
mcp.tool()(self.get_network_detail)
19+
mcp.tool()(self.update_network)
20+
mcp.tool()(self.delete_network)
21+
22+
def get_neutron_networks(
23+
self, status_filter: str | None = None, shared_only: bool = False
24+
) -> list[Network]:
25+
"""
26+
Get the list of Neutron networks with optional filtering.
27+
28+
Args:
29+
status_filter: Filter networks by status (e.g., 'ACTIVE', 'DOWN')
30+
shared_only: If True, only show shared networks
31+
32+
Returns:
33+
List of Network objects
34+
"""
35+
conn = get_openstack_conn()
36+
37+
filters = {}
38+
39+
if status_filter:
40+
filters["status"] = status_filter.upper()
41+
42+
if shared_only:
43+
filters["shared"] = True
44+
45+
networks = conn.list_networks(filters=filters)
46+
47+
return [
48+
self._convert_to_network_model(network) for network in networks
49+
]
50+
51+
def create_network(
52+
self,
53+
name: str,
54+
description: str | None = None,
55+
is_admin_state_up: bool = True,
56+
is_shared: bool = False,
57+
provider_network_type: str | None = None,
58+
provider_physical_network: str | None = None,
59+
provider_segmentation_id: int | None = None,
60+
) -> Network:
61+
"""
62+
Create a new Neutron network.
63+
64+
Args:
65+
name: Network name
66+
description: Network description
67+
is_admin_state_up: Administrative state
68+
is_shared: Whether the network is shared
69+
provider_network_type: Provider network type (e.g., 'vlan', 'flat', 'vxlan')
70+
provider_physical_network: Physical network name
71+
provider_segmentation_id: Segmentation ID for VLAN/VXLAN
72+
73+
Returns:
74+
Created Network object
75+
"""
76+
conn = get_openstack_conn()
77+
78+
network_args = {
79+
"name": name,
80+
"admin_state_up": is_admin_state_up,
81+
"shared": is_shared,
82+
}
83+
84+
if description:
85+
network_args["description"] = description
86+
87+
if provider_network_type:
88+
network_args["provider_network_type"] = provider_network_type
89+
90+
if provider_physical_network:
91+
network_args["provider_physical_network"] = (
92+
provider_physical_network
93+
)
94+
95+
if provider_segmentation_id is not None:
96+
network_args["provider_segmentation_id"] = provider_segmentation_id
97+
98+
network = conn.network.create_network(**network_args)
99+
100+
return self._convert_to_network_model(network)
101+
102+
def get_network_detail(self, network_id: str) -> Network:
103+
"""
104+
Get detailed information about a specific Neutron network.
105+
106+
Args:
107+
network_id: ID of the network to retrieve
108+
109+
Returns:
110+
Network object with detailed information
111+
"""
112+
conn = get_openstack_conn()
113+
114+
network = conn.network.get_network(network_id)
115+
if not network:
116+
raise Exception(f"Network with ID {network_id} not found")
117+
118+
return self._convert_to_network_model(network)
119+
120+
def update_network(
121+
self,
122+
network_id: str,
123+
name: str | None = None,
124+
description: str | None = None,
125+
is_admin_state_up: bool | None = None,
126+
is_shared: bool | None = None,
127+
) -> Network:
128+
"""
129+
Update an existing Neutron network.
130+
131+
Args:
132+
network_id: ID of the network to update
133+
name: New network name
134+
description: New network description
135+
is_admin_state_up: New administrative state
136+
is_shared: New shared state
137+
138+
Returns:
139+
Updated Network object
140+
"""
141+
conn = get_openstack_conn()
142+
143+
update_args = {}
144+
145+
if name is not None:
146+
update_args["name"] = name
147+
if description is not None:
148+
update_args["description"] = description
149+
if is_admin_state_up is not None:
150+
update_args["admin_state_up"] = is_admin_state_up
151+
if is_shared is not None:
152+
update_args["shared"] = is_shared
153+
154+
if not update_args:
155+
raise Exception("No update parameters provided")
156+
157+
network = conn.network.update_network(network_id, **update_args)
158+
159+
return self._convert_to_network_model(network)
160+
161+
def delete_network(self, network_id: str) -> None:
162+
"""
163+
Delete a Neutron network.
164+
165+
Args:
166+
network_id: ID of the network to delete
167+
168+
Returns:
169+
None
170+
"""
171+
conn = get_openstack_conn()
172+
173+
network = conn.network.get_network(network_id)
174+
if not network:
175+
raise Exception(f"Network with ID {network_id} not found")
176+
177+
conn.network.delete_network(network_id, ignore_missing=False)
178+
179+
return None
180+
181+
def _convert_to_network_model(self, openstack_network) -> Network:
182+
"""
183+
Convert an OpenStack network object to a Network pydantic model.
184+
185+
Args:
186+
openstack_network: OpenStack network object
187+
188+
Returns:
189+
Network pydantic model
190+
"""
191+
return Network(
192+
id=openstack_network.id,
193+
name=openstack_network.name or "",
194+
status=openstack_network.status or "",
195+
description=openstack_network.description or None,
196+
is_admin_state_up=openstack_network.admin_state_up or False,
197+
is_shared=openstack_network.shared or False,
198+
mtu=openstack_network.mtu or None,
199+
provider_network_type=openstack_network.provider_network_type
200+
or None,
201+
provider_physical_network=openstack_network.provider_physical_network
202+
or None,
203+
provider_segmentation_id=openstack_network.provider_segmentation_id
204+
or None,
205+
project_id=openstack_network.project_id or None,
206+
)

src/openstack_mcp_server/tools/nova_tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def get_nova_servers(self) -> list[Server]:
3737
def get_nova_server(self, id: str) -> Server:
3838
"""
3939
Get a specific Nova server by invoking the registered tool.
40-
40+
4141
:param id: The ID of the server to retrieve.
4242
:return: A Server object representing the Nova server.
4343
"""
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from pydantic import BaseModel
2+
3+
4+
class Network(BaseModel):
5+
id: str
6+
name: str
7+
status: str
8+
description: str | None = None
9+
is_admin_state_up: bool = True
10+
is_shared: bool = False
11+
mtu: int | None = None
12+
provider_network_type: str | None = None
13+
provider_physical_network: str | None = None
14+
provider_segmentation_id: int | None = None
15+
project_id: str | None = None
16+
17+
18+
class Subnet(BaseModel):
19+
id: str
20+
name: str
21+
status: str
22+
23+
24+
class Port(BaseModel):
25+
id: str
26+
name: str
27+
status: str
28+
29+
30+
class Router(BaseModel):
31+
id: str
32+
name: str
33+
status: str
34+
35+
36+
class SecurityGroup(BaseModel):
37+
id: str
38+
name: str
39+
status: str
40+
41+
42+
class SecurityGroupRule(BaseModel):
43+
id: str
44+
name: str
45+
status: str
46+
47+
48+
class FloatingIP(BaseModel):
49+
id: str
50+
name: str
51+
status: str

tests/conftest.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def mock_get_openstack_conn():
99

1010
with patch(
1111
"openstack_mcp_server.tools.nova_tools.get_openstack_conn",
12-
return_value=mock_conn
12+
return_value=mock_conn,
1313
):
1414
yield mock_conn
1515

@@ -21,28 +21,42 @@ def mock_get_openstack_conn_glance():
2121

2222
with patch(
2323
"openstack_mcp_server.tools.glance_tools.get_openstack_conn",
24-
return_value=mock_conn
24+
return_value=mock_conn,
2525
):
2626
yield mock_conn
2727

28+
2829
@pytest.fixture
2930
def mock_get_openstack_conn_keystone():
3031
"""Mock get_openstack_conn function for keystone_tools."""
3132
mock_conn = Mock()
32-
33+
3334
with patch(
3435
"openstack_mcp_server.tools.keystone_tools.get_openstack_conn",
35-
return_value=mock_conn
36+
return_value=mock_conn,
3637
):
3738
yield mock_conn
3839

40+
3941
@pytest.fixture
4042
def mock_openstack_base():
4143
"""Mock base module functions."""
4244
mock_conn = Mock()
4345

4446
with patch(
4547
"openstack_mcp_server.tools.base.get_openstack_conn",
46-
return_value=mock_conn
48+
return_value=mock_conn,
49+
):
50+
yield mock_conn
51+
52+
53+
@pytest.fixture
54+
def mock_openstack_connect_neutron():
55+
"""Mock get_openstack_conn function for neutron_tools."""
56+
mock_conn = Mock()
57+
58+
with patch(
59+
"openstack_mcp_server.tools.neutron_tools.get_openstack_conn",
60+
return_value=mock_conn,
4761
):
48-
yield mock_conn
62+
yield mock_conn

0 commit comments

Comments
 (0)