Skip to content

Commit 6c4ddb0

Browse files
platanus-krhalucinor
authored andcommitted
feat: Add network router interface tools (#83)
* feat(network): add router interface tools (#82) * improve(network): simplify if condition to adding router interface (#82) * improve(network): simplify if condition to retrieving router interface (#82)
1 parent 7f87639 commit 6c4ddb0

File tree

3 files changed

+158
-0
lines changed

3 files changed

+158
-0
lines changed

src/openstack_mcp_server/tools/network_tools.py

Lines changed: 87 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,89 @@ 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+
args["subnet_id"] = subnet_id
1063+
args["port_id"] = port_id
1064+
res = conn.network.add_interface_to_router(router_id, **args)
1065+
return RouterInterface(
1066+
router_id=res.get("router_id", router_id),
1067+
port_id=res.get("port_id"),
1068+
subnet_id=res.get("subnet_id"),
1069+
)
1070+
1071+
def get_router_interfaces(self, router_id: str) -> list[RouterInterface]:
1072+
"""
1073+
List interfaces attached to a Router.
1074+
1075+
:param router_id: Target router ID
1076+
:return: List of RouterInterface objects representing router-owned ports
1077+
"""
1078+
conn = get_openstack_conn()
1079+
filters = {
1080+
"device_id": router_id,
1081+
"device_owner": "network:router_interface",
1082+
}
1083+
ports = conn.network.ports(**filters)
1084+
result: list[RouterInterface] = []
1085+
for p in ports:
1086+
subnet_id = None
1087+
if getattr(p, "fixed_ips", None):
1088+
first = p.fixed_ips[0]
1089+
if isinstance(first, dict):
1090+
subnet_id = first.get("subnet_id")
1091+
result.append(
1092+
RouterInterface(
1093+
router_id=router_id,
1094+
port_id=p.id,
1095+
subnet_id=subnet_id,
1096+
)
1097+
)
1098+
return result
1099+
1100+
def remove_router_interface(
1101+
self,
1102+
router_id: str,
1103+
subnet_id: str | None = None,
1104+
port_id: str | None = None,
1105+
) -> RouterInterface:
1106+
"""
1107+
Remove an interface from a Router by subnet or port.
1108+
Provide either subnet_id or port_id.
1109+
1110+
:param router_id: Target router ID
1111+
:param subnet_id: Subnet ID to detach (mutually exclusive with port_id)
1112+
:param port_id: Port ID to detach (mutually exclusive with subnet_id)
1113+
:return: Detached interface information as RouterInterface
1114+
"""
1115+
conn = get_openstack_conn()
1116+
args: dict = {}
1117+
if subnet_id is not None:
1118+
args["subnet_id"] = subnet_id
1119+
if port_id is not None:
1120+
args["port_id"] = port_id
1121+
res = conn.network.remove_interface_from_router(router_id, **args)
1122+
return RouterInterface(
1123+
router_id=res.get("router_id", router_id),
1124+
port_id=res.get("port_id"),
1125+
subnet_id=res.get("subnet_id"),
1126+
)
1127+
10411128
def _convert_to_router_model(self, openstack_router) -> Router:
10421129
"""
10431130
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

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

0 commit comments

Comments
 (0)