Skip to content

Commit 0a13a3b

Browse files
committed
feat(network): add router CRUD tools (#60)
1 parent 354079f commit 0a13a3b

File tree

3 files changed

+420
-0
lines changed

3 files changed

+420
-0
lines changed

src/openstack_mcp_server/tools/network_tools.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
from fastmcp import FastMCP
22

33
from .base import get_openstack_conn
4+
from .request.network import (
5+
ExternalGatewayInfo,
6+
Route,
7+
)
48
from .response.network import (
59
FloatingIP,
610
Network,
711
Port,
12+
Router,
813
Subnet,
914
)
1015

@@ -80,6 +85,7 @@ def create_network(
8085
provider_network_type: str | None = None,
8186
provider_physical_network: str | None = None,
8287
provider_segmentation_id: int | None = None,
88+
project_id: str | None = None,
8389
) -> Network:
8490
"""
8591
Create a new Network.
@@ -107,6 +113,9 @@ def create_network(
107113
if provider_network_type:
108114
network_args["provider_network_type"] = provider_network_type
109115

116+
if project_id:
117+
network_args["project_id"] = project_id
118+
110119
if provider_physical_network:
111120
network_args["provider_physical_network"] = (
112121
provider_physical_network
@@ -860,3 +869,176 @@ def _convert_to_floating_ip_model(self, openstack_ip) -> FloatingIP:
860869
port_id=openstack_ip.port_id,
861870
router_id=openstack_ip.router_id,
862871
)
872+
873+
def get_routers(
874+
self,
875+
status_filter: str | None = None,
876+
project_id: str | None = None,
877+
is_admin_state_up: bool | None = None,
878+
) -> list[Router]:
879+
"""
880+
Get the list of Routers with optional filtering.
881+
:param status_filter: Filter by router status (e.g., `ACTIVE`, `DOWN`)
882+
:param project_id: Filter by project ID
883+
:param is_admin_state_up: Filter by admin state
884+
:return: List of Router objects
885+
"""
886+
conn = get_openstack_conn()
887+
filters: dict = {}
888+
if status_filter:
889+
filters["status"] = status_filter.upper()
890+
if project_id:
891+
filters["project_id"] = project_id
892+
if is_admin_state_up is not None:
893+
filters["admin_state_up"] = is_admin_state_up
894+
routers = conn.list_routers(filters=filters)
895+
return [self._convert_to_router_model(r) for r in routers]
896+
897+
def create_router(
898+
self,
899+
name: str | None = None,
900+
description: str | None = None,
901+
is_admin_state_up: bool = True,
902+
is_distributed: bool | None = None,
903+
project_id: str | None = None,
904+
external_gateway_info: ExternalGatewayInfo | None = None,
905+
) -> Router:
906+
"""
907+
Create a new Router.
908+
Typical use-cases:
909+
- Create basic router: name="r1" (defaults to admin_state_up=True)
910+
- Create distributed router: is_distributed=True
911+
- Create with external gateway for north-south traffic:
912+
external_gateway_info={"network_id": "ext-net", "enable_snat": True,
913+
"external_fixed_ips": [{"subnet_id": "ext-subnet", "ip_address": "203.0.113.10"}]}
914+
- Create with project ownership: project_id="proj-1"
915+
Notes:
916+
- external_gateway_info should follow Neutron schema: at minimum include
917+
"network_id"; optional keys include "enable_snat" and "external_fixed_ips".
918+
:param name: Router name
919+
:param description: Router description
920+
:param is_admin_state_up: Administrative state
921+
:param is_distributed: Distributed router flag
922+
:param project_id: Project ownership
923+
:param external_gateway_info: External gateway info dict
924+
:return: Created Router object
925+
"""
926+
conn = get_openstack_conn()
927+
router_args: dict = {"admin_state_up": is_admin_state_up}
928+
if name is not None:
929+
router_args["name"] = name
930+
if description is not None:
931+
router_args["description"] = description
932+
if is_distributed is not None:
933+
router_args["distributed"] = is_distributed
934+
if project_id is not None:
935+
router_args["project_id"] = project_id
936+
if external_gateway_info is not None:
937+
router_args["external_gateway_info"] = (
938+
external_gateway_info.model_dump(exclude_none=True)
939+
)
940+
router = conn.network.create_router(**router_args)
941+
return self._convert_to_router_model(router)
942+
943+
def get_router_detail(self, router_id: str) -> Router:
944+
"""
945+
Get detailed information about a specific Router.
946+
:param router_id: ID of the router to retrieve
947+
:return: Router details
948+
"""
949+
conn = get_openstack_conn()
950+
router = conn.network.get_router(router_id)
951+
return self._convert_to_router_model(router)
952+
953+
def update_router(
954+
self,
955+
router_id: str,
956+
name: str | None = None,
957+
description: str | None = None,
958+
is_admin_state_up: bool | None = None,
959+
is_distributed: bool | None = None,
960+
external_gateway_info: ExternalGatewayInfo | None = None,
961+
clear_external_gateway: bool = False,
962+
routes: list[Route] | None = None,
963+
) -> Router:
964+
"""
965+
Update Router attributes atomically. Only provided parameters are changed;
966+
omitted parameters remain untouched.
967+
Typical use-cases:
968+
- Rename and change description: name="r-new", description="d".
969+
- Toggle admin state: read current via get_router_detail(); pass inverted bool to is_admin_state_up.
970+
- Set distributed flag: is_distributed=True or False.
971+
- Set external gateway: external_gateway_info={"network_id": "ext-net", "enable_snat": True, "external_fixed_ips": [...]}.
972+
- Clear external gateway: clear_external_gateway=True (takes precedence over external_gateway_info).
973+
- Replace static routes: routes=[{"destination": "192.0.2.0/24", "nexthop": "10.0.0.1"}]. Pass [] to remove all routes.
974+
Notes:
975+
- For list-typed fields (routes), the provided list replaces the entire list on the server.
976+
- To clear external gateway, use clear_external_gateway=True. If both provided, clear_external_gateway takes precedence.
977+
:param router_id: ID of the router to update
978+
:param name: New router name
979+
:param description: New router description
980+
:param is_admin_state_up: Administrative state
981+
:param is_distributed: Distributed router flag
982+
:param external_gateway_info: External gateway info dict to set
983+
:param clear_external_gateway: If True, clear external gateway (set to None)
984+
:param routes: Static routes (replaces entire list)
985+
:return: Updated Router object
986+
"""
987+
conn = get_openstack_conn()
988+
update_args: dict = {}
989+
if name is not None:
990+
update_args["name"] = name
991+
if description is not None:
992+
update_args["description"] = description
993+
if is_admin_state_up is not None:
994+
update_args["admin_state_up"] = is_admin_state_up
995+
if is_distributed is not None:
996+
update_args["distributed"] = is_distributed
997+
if clear_external_gateway:
998+
update_args["external_gateway_info"] = None
999+
elif external_gateway_info is not None:
1000+
update_args["external_gateway_info"] = (
1001+
external_gateway_info.model_dump(exclude_none=True)
1002+
)
1003+
if routes is not None:
1004+
update_args["routes"] = [
1005+
r.model_dump(exclude_none=True) for r in routes
1006+
]
1007+
if not update_args:
1008+
current = conn.network.get_router(router_id)
1009+
return self._convert_to_router_model(current)
1010+
router = conn.network.update_router(router_id, **update_args)
1011+
return self._convert_to_router_model(router)
1012+
1013+
def delete_router(self, router_id: str) -> None:
1014+
"""
1015+
Delete a Router.
1016+
:param router_id: ID of the router to delete
1017+
:return: None
1018+
"""
1019+
conn = get_openstack_conn()
1020+
conn.network.delete_router(router_id, ignore_missing=False)
1021+
return None
1022+
1023+
def _convert_to_router_model(self, openstack_router) -> Router:
1024+
"""
1025+
Convert an OpenStack Router object to a Router pydantic model.
1026+
:param openstack_router: OpenStack router object
1027+
:return: Pydantic Router model
1028+
"""
1029+
return Router(
1030+
id=openstack_router.id,
1031+
name=getattr(openstack_router, "name", None),
1032+
status=getattr(openstack_router, "status", None),
1033+
description=getattr(openstack_router, "description", None),
1034+
project_id=getattr(openstack_router, "project_id", None),
1035+
is_admin_state_up=getattr(
1036+
openstack_router, "is_admin_state_up", None
1037+
),
1038+
external_gateway_info=getattr(
1039+
openstack_router, "external_gateway_info", None
1040+
),
1041+
is_distributed=getattr(openstack_router, "is_distributed", None),
1042+
is_ha=getattr(openstack_router, "is_ha", None),
1043+
routes=getattr(openstack_router, "routes", None),
1044+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from pydantic import BaseModel
2+
3+
4+
class Route(BaseModel):
5+
"""Static route for a router."""
6+
7+
destination: str
8+
nexthop: str
9+
10+
11+
class ExternalFixedIP(BaseModel):
12+
"""External fixed IP assignment for router gateway."""
13+
14+
subnet_id: str | None = None
15+
ip_address: str | None = None
16+
17+
18+
class ExternalGatewayInfo(BaseModel):
19+
"""External gateway information for a router.
20+
At minimum include `network_id`. Optionally include `enable_snat` and
21+
`external_fixed_ips`.
22+
"""
23+
24+
network_id: str
25+
enable_snat: bool | None = None
26+
external_fixed_ips: list[ExternalFixedIP] | None = None

0 commit comments

Comments
 (0)