Skip to content

Commit 95877eb

Browse files
committed
improve(network): integrating port update feature (#30)
1 parent a353556 commit 95877eb

File tree

2 files changed

+54
-177
lines changed

2 files changed

+54
-177
lines changed

src/openstack_mcp_server/tools/network_tools.py

Lines changed: 29 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,10 @@ def register_tools(self, mcp: FastMCP):
3737
mcp.tool()(self.get_port_detail)
3838
mcp.tool()(self.update_port)
3939
mcp.tool()(self.delete_port)
40-
mcp.tool()(self.add_port_fixed_ip)
41-
mcp.tool()(self.remove_port_fixed_ip)
40+
4241
mcp.tool()(self.get_port_allowed_address_pairs)
43-
mcp.tool()(self.add_port_allowed_address_pair)
44-
mcp.tool()(self.remove_port_allowed_address_pair)
4542
mcp.tool()(self.set_port_binding)
46-
mcp.tool()(self.set_port_admin_state)
47-
mcp.tool()(self.toggle_port_admin_state)
43+
4844
mcp.tool()(self.get_floating_ips)
4945
mcp.tool()(self.create_floating_ip)
5046
mcp.tool()(self.attach_floating_ip_to_port)
@@ -430,63 +426,6 @@ def get_ports(
430426
ports = conn.list_ports(filters=filters)
431427
return [self._convert_to_port_model(port) for port in ports]
432428

433-
def add_port_fixed_ip(
434-
self,
435-
port_id: str,
436-
subnet_id: str | None = None,
437-
ip_address: str | None = None,
438-
) -> Port:
439-
"""
440-
Add a fixed IP to a port.
441-
442-
:param port_id: Target port ID
443-
:param subnet_id: Subnet ID of the fixed IP entry
444-
:param ip_address: Fixed IP address to add
445-
:return: Updated Port object
446-
"""
447-
conn = get_openstack_conn()
448-
port = conn.network.get_port(port_id)
449-
fixed_ips = list(port.fixed_ips or [])
450-
entry: dict = {}
451-
if subnet_id is not None:
452-
entry["subnet_id"] = subnet_id
453-
if ip_address is not None:
454-
entry["ip_address"] = ip_address
455-
fixed_ips.append(entry)
456-
updated = conn.network.update_port(port_id, fixed_ips=fixed_ips)
457-
return self._convert_to_port_model(updated)
458-
459-
def remove_port_fixed_ip(
460-
self,
461-
port_id: str,
462-
ip_address: str | None = None,
463-
subnet_id: str | None = None,
464-
) -> Port:
465-
"""
466-
Remove a fixed IP entry from a port.
467-
468-
:param port_id: Target port ID
469-
:param ip_address: Fixed IP address to remove
470-
:param subnet_id: Subnet ID of the entry to remove
471-
:return: Updated Port object
472-
"""
473-
conn = get_openstack_conn()
474-
port = conn.network.get_port(port_id)
475-
current = list(port.fixed_ips or [])
476-
if not current:
477-
return self._convert_to_port_model(port)
478-
479-
def predicate(item: dict) -> bool:
480-
if ip_address is not None and item.get("ip_address") == ip_address:
481-
return False
482-
if subnet_id is not None and item.get("subnet_id") == subnet_id:
483-
return False
484-
return True
485-
486-
new_fixed = [fi for fi in current if predicate(fi)]
487-
updated = conn.network.update_port(port_id, fixed_ips=new_fixed)
488-
return self._convert_to_port_model(updated)
489-
490429
def get_port_allowed_address_pairs(self, port_id: str) -> list[dict]:
491430
"""
492431
Get allowed address pairs configured on a port.
@@ -498,68 +437,6 @@ def get_port_allowed_address_pairs(self, port_id: str) -> list[dict]:
498437
port = conn.network.get_port(port_id)
499438
return list(port.allowed_address_pairs or [])
500439

501-
def add_port_allowed_address_pair(
502-
self,
503-
port_id: str,
504-
ip_address: str,
505-
mac_address: str | None = None,
506-
) -> Port:
507-
"""
508-
Add an allowed address pair to a port.
509-
510-
:param port_id: Port ID
511-
:param ip_address: IP address to allow
512-
:param mac_address: MAC address to allow
513-
:return: Updated Port object
514-
"""
515-
conn = get_openstack_conn()
516-
port = conn.network.get_port(port_id)
517-
518-
pairs = list(port.allowed_address_pairs or [])
519-
entry = {"ip_address": ip_address}
520-
if mac_address is not None:
521-
entry["mac_address"] = mac_address
522-
pairs.append(entry)
523-
524-
updated = conn.network.update_port(
525-
port_id,
526-
allowed_address_pairs=pairs,
527-
)
528-
return self._convert_to_port_model(updated)
529-
530-
def remove_port_allowed_address_pair(
531-
self,
532-
port_id: str,
533-
ip_address: str,
534-
mac_address: str | None = None,
535-
) -> Port:
536-
"""
537-
Remove an allowed address pair from a port.
538-
539-
:param port_id: Port ID
540-
:param ip_address: IP address to remove
541-
:param mac_address: MAC address to remove. If not provided, remove all pairs with the IP
542-
:return: Updated Port object
543-
"""
544-
conn = get_openstack_conn()
545-
port = conn.network.get_port(port_id)
546-
pairs = list(port.allowed_address_pairs or [])
547-
548-
def keep(p: dict) -> bool:
549-
if mac_address is None:
550-
return p.get("ip_address") != ip_address
551-
return not (
552-
p.get("ip_address") == ip_address
553-
and p.get("mac_address") == mac_address
554-
)
555-
556-
new_pairs = [p for p in pairs if keep(p)]
557-
updated = conn.network.update_port(
558-
port_id,
559-
allowed_address_pairs=new_pairs,
560-
)
561-
return self._convert_to_port_model(updated)
562-
563440
def set_port_binding(
564441
self,
565442
port_id: str,
@@ -590,40 +467,6 @@ def set_port_binding(
590467
updated = conn.network.update_port(port_id, **update_args)
591468
return self._convert_to_port_model(updated)
592469

593-
def set_port_admin_state(
594-
self,
595-
port_id: str,
596-
is_admin_state_up: bool,
597-
) -> Port:
598-
"""
599-
Set the administrative state of a port.
600-
601-
:param port_id: Port ID
602-
:param is_admin_state_up: Administrative state
603-
:return: Updated Port object
604-
"""
605-
conn = get_openstack_conn()
606-
updated = conn.network.update_port(
607-
port_id,
608-
admin_state_up=is_admin_state_up,
609-
)
610-
return self._convert_to_port_model(updated)
611-
612-
def toggle_port_admin_state(self, port_id: str) -> Port:
613-
"""
614-
Toggle the administrative state of a port.
615-
616-
:param port_id: Port ID
617-
:return: Updated Port object
618-
"""
619-
conn = get_openstack_conn()
620-
current = conn.network.get_port(port_id)
621-
updated = conn.network.update_port(
622-
port_id,
623-
admin_state_up=not current.admin_state_up,
624-
)
625-
return self._convert_to_port_model(updated)
626-
627470
def create_port(
628471
self,
629472
network_id: str,
@@ -683,16 +526,37 @@ def update_port(
683526
is_admin_state_up: bool | None = None,
684527
device_id: str | None = None,
685528
security_group_ids: list[str] | None = None,
529+
allowed_address_pairs: list[dict] | None = None,
530+
fixed_ips: list[dict] | None = None,
686531
) -> Port:
687532
"""
688-
Update an existing Port.
533+
Update an existing Port. Only provided parameters are changed; omitted parameters remain untouched.
534+
535+
Typical use-cases:
536+
- Set admin state down: is_admin_state_up=False
537+
- Toggle admin state: read current via get_port_detail() then pass the inverted value
538+
- Replace security groups: security_group_ids=["sg-1", "sg-2"]
539+
- Replace allowed address pairs:
540+
1) current = get_port_allowed_address_pairs(port_id)
541+
2) edit the list (append/remove dicts)
542+
3) update_port(port_id, allowed_address_pairs=current)
543+
- Replace fixed IPs:
544+
1) current = get_port_detail(port_id).fixed_ips
545+
2) edit the list
546+
3) update_port(port_id, fixed_ips=current)
547+
548+
Notes:
549+
- For list-typed fields like security groups or allowed address pairs, this method replaces
550+
the entire list with the provided value. To remove all entries, pass an empty list [].
689551
690552
:param port_id: ID of the port to update
691553
:param name: New port name
692554
:param description: New port description
693555
:param is_admin_state_up: Administrative state
694556
:param device_id: Device ID
695-
:param security_group_ids: Security group ID list
557+
:param security_group_ids: Security group ID list (replaces entire list)
558+
:param allowed_address_pairs: Allowed address pairs (replaces entire list)
559+
:param fixed_ips: Fixed IP assignments (replaces entire list)
696560
:return: Updated Port object
697561
"""
698562
conn = get_openstack_conn()
@@ -707,6 +571,10 @@ def update_port(
707571
update_args["device_id"] = device_id
708572
if security_group_ids is not None:
709573
update_args["security_groups"] = security_group_ids
574+
if allowed_address_pairs is not None:
575+
update_args["allowed_address_pairs"] = allowed_address_pairs
576+
if fixed_ips is not None:
577+
update_args["fixed_ips"] = fixed_ips
710578
if not update_args:
711579
current = conn.network.get_port(port_id)
712580
return self._convert_to_port_model(current)

tests/tools/test_network_tools.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -668,11 +668,9 @@ def test_add_port_fixed_ip(self, mock_openstack_connect_network):
668668
mock_conn.network.update_port.return_value = updated
669669

670670
tools = self.get_network_tools()
671-
res = tools.add_port_fixed_ip(
672-
"port-1",
673-
subnet_id="subnet-2",
674-
ip_address="10.0.1.10",
675-
)
671+
new_fixed = list(current.fixed_ips)
672+
new_fixed.append({"subnet_id": "subnet-2", "ip_address": "10.0.1.10"})
673+
res = tools.update_port("port-1", fixed_ips=new_fixed)
676674
assert len(res.fixed_ips or []) == 2
677675

678676
def test_remove_port_fixed_ip(self, mock_openstack_connect_network):
@@ -703,7 +701,10 @@ def test_remove_port_fixed_ip(self, mock_openstack_connect_network):
703701
mock_conn.network.update_port.return_value = updated
704702

705703
tools = self.get_network_tools()
706-
res = tools.remove_port_fixed_ip("port-1", ip_address="10.0.1.10")
704+
filtered = [
705+
fi for fi in current.fixed_ips if fi["ip_address"] != "10.0.1.10"
706+
]
707+
res = tools.update_port("port-1", fixed_ips=filtered)
707708
assert len(res.fixed_ips or []) == 1
708709

709710
def test_get_and_update_allowed_address_pairs(
@@ -735,17 +736,23 @@ def test_get_and_update_allowed_address_pairs(
735736
updated.security_group_ids = None
736737
mock_conn.network.update_port.return_value = updated
737738

738-
res_add = tools.add_port_allowed_address_pair(
739-
"port-1",
740-
"192.0.2.5",
741-
mac_address="aa:bb:cc:dd:ee:ff",
739+
pairs = []
740+
pairs.append(
741+
{"ip_address": "192.0.2.5", "mac_address": "aa:bb:cc:dd:ee:ff"}
742742
)
743+
res_add = tools.update_port("port-1", allowed_address_pairs=pairs)
743744
assert isinstance(res_add, Port)
744745

745-
res_remove = tools.remove_port_allowed_address_pair(
746-
"port-1",
747-
"192.0.2.5",
748-
mac_address="aa:bb:cc:dd:ee:ff",
746+
filtered = [
747+
p
748+
for p in pairs
749+
if not (
750+
p["ip_address"] == "192.0.2.5"
751+
and p["mac_address"] == "aa:bb:cc:dd:ee:ff"
752+
)
753+
]
754+
res_remove = tools.update_port(
755+
"port-1", allowed_address_pairs=filtered
749756
)
750757
assert isinstance(res_remove, Port)
751758

@@ -779,14 +786,16 @@ def test_set_port_binding_and_admin_state(
779786
)
780787
assert isinstance(res_bind, Port)
781788

782-
res_set = tools.set_port_admin_state("port-1", False)
789+
res_set = tools.update_port("port-1", is_admin_state_up=False)
783790
assert res_set.is_admin_state_up is False
784791

785792
current = Mock()
786793
current.admin_state_up = False
787794
mock_conn.network.get_port.return_value = current
788795
updated.admin_state_up = True
789-
res_toggle = tools.toggle_port_admin_state("port-1")
796+
res_toggle = tools.update_port(
797+
"port-1", is_admin_state_up=not current.admin_state_up
798+
)
790799
assert res_toggle.is_admin_state_up is True
791800

792801
def test_get_subnets_filters_and_has_gateway_true(

0 commit comments

Comments
 (0)