11from fastmcp import FastMCP
22
33from .base import get_openstack_conn
4+ from .request .network import (
5+ ExternalGatewayInfo ,
6+ Route ,
7+ )
48from .response .network import (
59 FloatingIP ,
610 Network ,
711 Port ,
12+ Router ,
813 Subnet ,
914)
1015
@@ -42,6 +47,11 @@ def register_tools(self, mcp: FastMCP):
4247 mcp .tool ()(self .update_floating_ip )
4348 mcp .tool ()(self .create_floating_ips_bulk )
4449 mcp .tool ()(self .assign_first_available_floating_ip )
50+ mcp .tool ()(self .get_routers )
51+ mcp .tool ()(self .create_router )
52+ mcp .tool ()(self .get_router_detail )
53+ mcp .tool ()(self .update_router )
54+ mcp .tool ()(self .delete_router )
4555
4656 def get_networks (
4757 self ,
@@ -63,9 +73,9 @@ def get_networks(
6373 filters ["status" ] = status_filter .upper ()
6474
6575 if shared_only :
66- filters ["shared " ] = True
76+ filters ["is_shared " ] = True
6777
68- networks = conn .list_networks ( filters = filters )
78+ networks = conn .network . networks ( ** filters )
6979
7080 return [
7181 self ._convert_to_network_model (network ) for network in networks
@@ -80,6 +90,7 @@ def create_network(
8090 provider_network_type : str | None = None ,
8191 provider_physical_network : str | None = None ,
8292 provider_segmentation_id : int | None = None ,
93+ project_id : str | None = None ,
8394 ) -> Network :
8495 """
8596 Create a new Network.
@@ -107,6 +118,9 @@ def create_network(
107118 if provider_network_type :
108119 network_args ["provider_network_type" ] = provider_network_type
109120
121+ if project_id :
122+ network_args ["project_id" ] = project_id
123+
110124 if provider_physical_network :
111125 network_args ["provider_physical_network" ] = (
112126 provider_physical_network
@@ -245,7 +259,7 @@ def get_subnets(
245259 filters ["project_id" ] = project_id
246260 if is_dhcp_enabled is not None :
247261 filters ["enable_dhcp" ] = is_dhcp_enabled
248- subnets = conn .list_subnets ( filters = filters )
262+ subnets = conn .network . subnets ( ** filters )
249263 if has_gateway is not None :
250264 subnets = [
251265 s for s in subnets if (s .gateway_ip is not None ) == has_gateway
@@ -440,7 +454,9 @@ def get_ports(
440454 filters ["device_id" ] = device_id
441455 if network_id :
442456 filters ["network_id" ] = network_id
443- ports = conn .list_ports (filters = filters )
457+
458+ ports = conn .network .ports (** filters )
459+
444460 return [self ._convert_to_port_model (port ) for port in ports ]
445461
446462 def get_port_allowed_address_pairs (self , port_id : str ) -> list [dict ]:
@@ -860,3 +876,204 @@ def _convert_to_floating_ip_model(self, openstack_ip) -> FloatingIP:
860876 port_id = openstack_ip .port_id ,
861877 router_id = openstack_ip .router_id ,
862878 )
879+
880+ def get_routers (
881+ self ,
882+ status_filter : str | None = None ,
883+ project_id : str | None = None ,
884+ is_admin_state_up : bool | None = None ,
885+ ) -> list [Router ]:
886+ """
887+ Get the list of Routers with optional filtering.
888+ :param status_filter: Filter by router status (e.g., `ACTIVE`, `DOWN`)
889+ :param project_id: Filter by project ID
890+ :param is_admin_state_up: Filter by admin state
891+ :return: List of Router objects
892+ """
893+ conn = get_openstack_conn ()
894+ filters : dict = {}
895+ if status_filter :
896+ filters ["status" ] = status_filter .upper ()
897+ if project_id :
898+ filters ["project_id" ] = project_id
899+ if is_admin_state_up is not None :
900+ filters ["admin_state_up" ] = is_admin_state_up
901+ # Do not pass unsupported filters (e.g., status) to the server.
902+ server_filters = self ._sanitize_server_filters (filters )
903+ routers = conn .network .routers (** server_filters )
904+
905+ router_models = [self ._convert_to_router_model (r ) for r in routers ]
906+ if status_filter :
907+ status_upper = status_filter .upper ()
908+ router_models = [
909+ r
910+ for r in router_models
911+ if (r .status or "" ).upper () == status_upper
912+ ]
913+ return router_models
914+
915+ def create_router (
916+ self ,
917+ name : str | None = None ,
918+ description : str | None = None ,
919+ is_admin_state_up : bool = True ,
920+ is_distributed : bool | None = None ,
921+ project_id : str | None = None ,
922+ external_gateway_info : ExternalGatewayInfo | None = None ,
923+ ) -> Router :
924+ """
925+ Create a new Router.
926+ Typical use-cases:
927+ - Create basic router: name="r1" (defaults to admin_state_up=True)
928+ - Create distributed router: is_distributed=True
929+ - Create with external gateway for north-south traffic:
930+ external_gateway_info={"network_id": "ext-net", "enable_snat": True,
931+ "external_fixed_ips": [{"subnet_id": "ext-subnet", "ip_address": "203.0.113.10"}]}
932+ - Create with project ownership: project_id="proj-1"
933+ Notes:
934+ - external_gateway_info should follow Neutron schema: at minimum include
935+ "network_id"; optional keys include "enable_snat" and "external_fixed_ips".
936+ :param name: Router name
937+ :param description: Router description
938+ :param is_admin_state_up: Administrative state
939+ :param is_distributed: Distributed router flag
940+ :param project_id: Project ownership
941+ :param external_gateway_info: External gateway info dict
942+ :return: Created Router object
943+ """
944+ conn = get_openstack_conn ()
945+ router_args : dict = {"admin_state_up" : is_admin_state_up }
946+ if name is not None :
947+ router_args ["name" ] = name
948+ if description is not None :
949+ router_args ["description" ] = description
950+ if is_distributed is not None :
951+ router_args ["distributed" ] = is_distributed
952+ if project_id is not None :
953+ router_args ["project_id" ] = project_id
954+ if external_gateway_info is not None :
955+ router_args ["external_gateway_info" ] = (
956+ external_gateway_info .model_dump (exclude_none = True )
957+ )
958+ router = conn .network .create_router (** router_args )
959+ return self ._convert_to_router_model (router )
960+
961+ def get_router_detail (self , router_id : str ) -> Router :
962+ """
963+ Get detailed information about a specific Router.
964+ :param router_id: ID of the router to retrieve
965+ :return: Router details
966+ """
967+ conn = get_openstack_conn ()
968+ router = conn .network .get_router (router_id )
969+ return self ._convert_to_router_model (router )
970+
971+ def update_router (
972+ self ,
973+ router_id : str ,
974+ name : str | None = None ,
975+ description : str | None = None ,
976+ is_admin_state_up : bool | None = None ,
977+ is_distributed : bool | None = None ,
978+ external_gateway_info : ExternalGatewayInfo | None = None ,
979+ clear_external_gateway : bool = False ,
980+ routes : list [Route ] | None = None ,
981+ ) -> Router :
982+ """
983+ Update Router attributes atomically. Only provided parameters are changed;
984+ omitted parameters remain untouched.
985+ Typical use-cases:
986+ - Rename and change description: name="r-new", description="d".
987+ - Toggle admin state: read current via get_router_detail(); pass inverted bool to is_admin_state_up.
988+ - Set distributed flag: is_distributed=True or False.
989+ - Set external gateway: external_gateway_info={"network_id": "ext-net", "enable_snat": True, "external_fixed_ips": [...]}.
990+ - Clear external gateway: clear_external_gateway=True (takes precedence over external_gateway_info).
991+ - Replace static routes: routes=[{"destination": "192.0.2.0/24", "nexthop": "10.0.0.1"}]. Pass [] to remove all routes.
992+ Notes:
993+ - For list-typed fields (routes), the provided list replaces the entire list on the server.
994+ - To clear external gateway, use clear_external_gateway=True. If both provided, clear_external_gateway takes precedence.
995+ :param router_id: ID of the router to update
996+ :param name: New router name
997+ :param description: New router description
998+ :param is_admin_state_up: Administrative state
999+ :param is_distributed: Distributed router flag
1000+ :param external_gateway_info: External gateway info dict to set
1001+ :param clear_external_gateway: If True, clear external gateway (set to None)
1002+ :param routes: Static routes (replaces entire list)
1003+ :return: Updated Router object
1004+ """
1005+ conn = get_openstack_conn ()
1006+ update_args : dict = {}
1007+ if name is not None :
1008+ update_args ["name" ] = name
1009+ if description is not None :
1010+ update_args ["description" ] = description
1011+ if is_admin_state_up is not None :
1012+ update_args ["admin_state_up" ] = is_admin_state_up
1013+ if is_distributed is not None :
1014+ update_args ["distributed" ] = is_distributed
1015+ if clear_external_gateway :
1016+ update_args ["external_gateway_info" ] = None
1017+ elif external_gateway_info is not None :
1018+ update_args ["external_gateway_info" ] = (
1019+ external_gateway_info .model_dump (exclude_none = True )
1020+ )
1021+ if routes is not None :
1022+ update_args ["routes" ] = [
1023+ r .model_dump (exclude_none = True ) for r in routes
1024+ ]
1025+ if not update_args :
1026+ current = conn .network .get_router (router_id )
1027+ return self ._convert_to_router_model (current )
1028+ router = conn .network .update_router (router_id , ** update_args )
1029+ return self ._convert_to_router_model (router )
1030+
1031+ def delete_router (self , router_id : str ) -> None :
1032+ """
1033+ Delete a Router.
1034+ :param router_id: ID of the router to delete
1035+ :return: None
1036+ """
1037+ conn = get_openstack_conn ()
1038+ conn .network .delete_router (router_id , ignore_missing = False )
1039+ return None
1040+
1041+ def _convert_to_router_model (self , openstack_router ) -> Router :
1042+ """
1043+ Convert an OpenStack Router object to a Router pydantic model.
1044+ :param openstack_router: OpenStack router object
1045+ :return: Pydantic Router model
1046+ """
1047+ return Router (
1048+ id = openstack_router .id ,
1049+ name = getattr (openstack_router , "name" , None ),
1050+ status = getattr (openstack_router , "status" , None ),
1051+ description = getattr (openstack_router , "description" , None ),
1052+ project_id = getattr (openstack_router , "project_id" , None ),
1053+ is_admin_state_up = getattr (
1054+ openstack_router , "is_admin_state_up" , None
1055+ ),
1056+ external_gateway_info = getattr (
1057+ openstack_router , "external_gateway_info" , None
1058+ ),
1059+ is_distributed = getattr (openstack_router , "is_distributed" , None ),
1060+ is_ha = getattr (openstack_router , "is_ha" , None ),
1061+ routes = getattr (openstack_router , "routes" , None ),
1062+ )
1063+
1064+ def _sanitize_server_filters (self , filters : dict ) -> dict :
1065+ """
1066+ Remove unsupported query params before sending to Neutron.
1067+
1068+ Currently removed keys:
1069+ - "status": not universally supported for server-side filtering
1070+
1071+ :param filters: original filter dict
1072+ :return: cleaned filter dict safe for server query
1073+ """
1074+ if not filters :
1075+ return {}
1076+ attrs = dict (filters )
1077+ # Remove client-only or unsupported filters
1078+ attrs .pop ("status" , None )
1079+ return attrs
0 commit comments