Skip to content

Commit f5f3dcf

Browse files
committed
feat(network): add router interface tools (#82)
1 parent ef46d14 commit f5f3dcf

File tree

3 files changed

+162
-0
lines changed

3 files changed

+162
-0
lines changed

src/openstack_mcp_server/tools/network_tools.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Network,
1111
Port,
1212
Router,
13+
RouterInterface,
1314
Subnet,
1415
)
1516

@@ -52,6 +53,9 @@ def register_tools(self, mcp: FastMCP):
5253
mcp.tool()(self.get_router_detail)
5354
mcp.tool()(self.update_router)
5455
mcp.tool()(self.delete_router)
56+
mcp.tool()(self.add_router_interface)
57+
mcp.tool()(self.get_router_interfaces)
58+
mcp.tool()(self.remove_router_interface)
5559

5660
def get_networks(
5761
self,
@@ -1038,6 +1042,93 @@ def delete_router(self, router_id: str) -> None:
10381042
conn.network.delete_router(router_id, ignore_missing=False)
10391043
return None
10401044

1045+
def add_router_interface(
1046+
self,
1047+
router_id: str,
1048+
subnet_id: str | None = None,
1049+
port_id: str | None = None,
1050+
) -> RouterInterface:
1051+
"""
1052+
Add an interface to a Router by subnet or port.
1053+
Provide either subnet_id or port_id.
1054+
1055+
:param router_id: Target router ID
1056+
:param subnet_id: Subnet ID to attach (mutually exclusive with port_id)
1057+
:param port_id: Port ID to attach (mutually exclusive with subnet_id)
1058+
:return: Created/attached router interface information as RouterInterface
1059+
"""
1060+
conn = get_openstack_conn()
1061+
args: dict = {}
1062+
if subnet_id is not None:
1063+
args["subnet_id"] = subnet_id
1064+
if port_id is not None:
1065+
args["port_id"] = port_id
1066+
res = conn.network.add_interface_to_router(router_id, **args)
1067+
return RouterInterface(
1068+
router_id=res.get("router_id", router_id),
1069+
port_id=res.get("port_id"),
1070+
subnet_id=res.get("subnet_id"),
1071+
)
1072+
1073+
def get_router_interfaces(self, router_id: str) -> list[RouterInterface]:
1074+
"""
1075+
List interfaces attached to a Router.
1076+
1077+
:param router_id: Target router ID
1078+
:return: List of RouterInterface objects representing router-owned ports
1079+
"""
1080+
conn = get_openstack_conn()
1081+
filters = {
1082+
"device_id": router_id,
1083+
"device_owner": "network:router_interface",
1084+
}
1085+
ports = conn.network.ports(**filters)
1086+
result: list[RouterInterface] = []
1087+
for p in ports:
1088+
subnet_id = None
1089+
if getattr(p, "fixed_ips", None):
1090+
first = p.fixed_ips[0]
1091+
if isinstance(first, dict):
1092+
subnet_id = first.get("subnet_id")
1093+
else:
1094+
subnet_id = None
1095+
result.append(
1096+
RouterInterface(
1097+
router_id=router_id,
1098+
port_id=p.id,
1099+
subnet_id=subnet_id,
1100+
)
1101+
)
1102+
return result
1103+
1104+
def remove_router_interface(
1105+
self,
1106+
router_id: str,
1107+
subnet_id: str | None = None,
1108+
port_id: str | None = None,
1109+
) -> RouterInterface:
1110+
"""
1111+
Remove an interface from a Router by subnet or port.
1112+
Provide either subnet_id or port_id.
1113+
1114+
:param router_id: Target router ID
1115+
:param subnet_id: Subnet ID to detach (mutually exclusive with port_id)
1116+
:param port_id: Port ID to detach (mutually exclusive with subnet_id)
1117+
:return: Detached interface information as RouterInterface
1118+
"""
1119+
conn = get_openstack_conn()
1120+
args: dict = {}
1121+
if subnet_id is not None:
1122+
args["subnet_id"] = subnet_id
1123+
if port_id is not None:
1124+
args["port_id"] = port_id
1125+
res = conn.network.remove_interface_from_router(router_id, **args)
1126+
return RouterInterface(
1127+
router_id=res.get("router_id", router_id),
1128+
port_id=res.get("port_id"),
1129+
subnet_id=res.get("subnet_id"),
1130+
)
1131+
10411132
def _convert_to_router_model(self, openstack_router) -> Router:
10421133
"""
10431134
Convert an OpenStack Router object to a Router pydantic model.

src/openstack_mcp_server/tools/response/network.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ class Router(BaseModel):
5959
routes: list[dict] | None = None
6060

6161

62+
class RouterInterface(BaseModel):
63+
router_id: str
64+
port_id: str
65+
subnet_id: str | None = None
66+
67+
6268
class SecurityGroup(BaseModel):
6369
id: str
6470
name: str | None = None

tests/tools/test_network_tools.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Network,
1111
Port,
1212
Router,
13+
RouterInterface,
1314
Subnet,
1415
)
1516

@@ -1569,3 +1570,67 @@ def test_delete_router_success(self, mock_openstack_connect_network):
15691570
"router-6",
15701571
ignore_missing=False,
15711572
)
1573+
1574+
def test_add_get_remove_router_interface_by_subnet(
1575+
self, mock_openstack_connect_network
1576+
):
1577+
mock_conn = mock_openstack_connect_network
1578+
1579+
add_res = {"router_id": "r-if-1", "port_id": "p-1", "subnet_id": "s-1"}
1580+
mock_conn.network.add_interface_to_router.return_value = add_res
1581+
1582+
p = Mock()
1583+
p.id = "p-1"
1584+
p.fixed_ips = [{"subnet_id": "s-1", "ip_address": "10.0.0.1"}]
1585+
mock_conn.network.ports.return_value = [p]
1586+
1587+
rm_res = {"router_id": "r-if-1", "port_id": "p-1", "subnet_id": "s-1"}
1588+
mock_conn.network.remove_interface_from_router.return_value = rm_res
1589+
1590+
tools = self.get_network_tools()
1591+
added = tools.add_router_interface("r-if-1", subnet_id="s-1")
1592+
assert added == RouterInterface(
1593+
router_id="r-if-1", port_id="p-1", subnet_id="s-1"
1594+
)
1595+
1596+
lst = tools.get_router_interfaces("r-if-1")
1597+
assert lst == [
1598+
RouterInterface(router_id="r-if-1", port_id="p-1", subnet_id="s-1")
1599+
]
1600+
1601+
removed = tools.remove_router_interface("r-if-1", subnet_id="s-1")
1602+
assert removed == RouterInterface(
1603+
router_id="r-if-1", port_id="p-1", subnet_id="s-1"
1604+
)
1605+
1606+
def test_add_get_remove_router_interface_by_port(
1607+
self, mock_openstack_connect_network
1608+
):
1609+
mock_conn = mock_openstack_connect_network
1610+
1611+
add_res = {"router_id": "r-if-2", "port_id": "p-2", "subnet_id": "s-2"}
1612+
mock_conn.network.add_interface_to_router.return_value = add_res
1613+
1614+
p = Mock()
1615+
p.id = "p-2"
1616+
p.fixed_ips = [{"subnet_id": "s-2", "ip_address": "10.0.1.1"}]
1617+
mock_conn.network.ports.return_value = [p]
1618+
1619+
rm_res = {"router_id": "r-if-2", "port_id": "p-2", "subnet_id": "s-2"}
1620+
mock_conn.network.remove_interface_from_router.return_value = rm_res
1621+
1622+
tools = self.get_network_tools()
1623+
added = tools.add_router_interface("r-if-2", port_id="p-2")
1624+
assert added == RouterInterface(
1625+
router_id="r-if-2", port_id="p-2", subnet_id="s-2"
1626+
)
1627+
1628+
lst = tools.get_router_interfaces("r-if-2")
1629+
assert lst == [
1630+
RouterInterface(router_id="r-if-2", port_id="p-2", subnet_id="s-2")
1631+
]
1632+
1633+
removed = tools.remove_router_interface("r-if-2", port_id="p-2")
1634+
assert removed == RouterInterface(
1635+
router_id="r-if-2", port_id="p-2", subnet_id="s-2"
1636+
)

0 commit comments

Comments
 (0)