fix issue where you couldn't update ports
Build and Push Container / build-and-push (push) Successful in 22s

This commit is contained in:
2026-08-01 19:50:32 -04:00
parent e360546de9
commit a81dd4f3df
4 changed files with 587 additions and 9 deletions
+91 -8
View File
@@ -19,10 +19,9 @@ Exported env vars:
from __future__ import annotations
import logging
import os
import sys
import logging
from typing import Literal, TypedDict
import requests
@@ -31,9 +30,13 @@ from dotenv import load_dotenv
from ip_lookup import get_ipv4, get_ipv6
from unifi_firewall import (
FirewallPolicy,
FirewallZone,
build_policy_payload,
create_policy,
get_action_from_policy,
get_dest_ports_from_policy,
get_protocol_from_policy,
get_session,
get_source_ip_from_policy,
list_policies,
@@ -101,6 +104,7 @@ class RuleConfig(TypedDict, total=False):
dest_port_ranges: list[PortRange] | None
logging_enabled: bool
enabled: bool
description: str | None
class RulesConfig(TypedDict):
@@ -168,6 +172,84 @@ def build_zone_map(zones: list[FirewallZone]) -> dict[str, str]:
return zone_map
def policy_matches_config(
existing_policy: FirewallPolicy,
rule: RuleConfig,
zone_map: dict[str, str],
public_ip: str,
) -> bool:
"""Check if an existing policy matches the desired config exactly."""
# 1. Source IP
existing_ip = get_source_ip_from_policy(existing_policy)
if existing_ip != public_ip:
return False
# 2. Dest ports (compare as sorted lists)
existing_ports_result = get_dest_ports_from_policy(existing_policy)
rule_dest_ports = rule.get("dest_ports") or []
if existing_ports_result is None:
if rule_dest_ports:
return False
else:
existing_ports, existing_ranges = existing_ports_result
if sorted(existing_ports) != sorted(rule_dest_ports):
return False
# 3. Dest port ranges
rule_dest_ranges = rule.get("dest_port_ranges") or []
if len(existing_ranges) != len(rule_dest_ranges):
return False
for er, rr in zip(sorted(existing_ranges, key=lambda x: x["start"]), sorted(rule_dest_ranges, key=lambda x: x["start"])):
if er["start"] != rr["start"] or er["stop"] != rr["stop"]:
return False
# 4. Protocol (case-insensitive)
existing_protocol = get_protocol_from_policy(existing_policy)
rule_protocol = rule.get("protocol")
if existing_protocol is not None and rule_protocol is not None:
if existing_protocol.upper() != rule_protocol.upper():
return False
elif existing_protocol is not None or rule_protocol is not None:
return False
# 5. Action type
existing_action_result = get_action_from_policy(existing_policy)
if existing_action_result is None:
return False
existing_action_type, existing_allow_return = existing_action_result
if existing_action_type != rule.get("action", "ALLOW"):
return False
# 6. allowReturnTraffic
if existing_action_type == "ALLOW" and existing_allow_return != rule.get("allow_return_traffic", True):
return False
# 7. Source zone ID
rule_source_zone = rule.get("source_zone", "WAN")
if existing_policy["source"]["zoneId"] != zone_map.get(rule_source_zone):
return False
# 8. Dest zone ID
rule_dest_zone = rule.get("dest_zone", "LAN")
if existing_policy["destination"]["zoneId"] != zone_map.get(rule_dest_zone):
return False
# 9. IP version
if existing_policy["ipProtocolScope"]["ipVersion"] != rule.get("ip_version", "IPV4"):
return False
# 10. enabled
if existing_policy["enabled"] != rule.get("enabled", True):
return False
# 11. loggingEnabled
if existing_policy["loggingEnabled"] != rule.get("logging_enabled", False):
return False
# 12. description
return existing_policy["description"] == rule.get("description")
def process_rule(
session: requests.Session,
host: str,
@@ -208,15 +290,16 @@ def process_rule(
existing_policy = existing_policies[0] if existing_policies else None
# Check if policy exists and IP matches
# Check if policy exists and matches config
if existing_policy:
existing_ip = get_source_ip_from_policy(existing_policy)
if existing_ip == public_ip:
logger.info("Rule '%s' already has correct IP (%s), skipping", rule_name, public_ip)
if policy_matches_config(existing_policy, rule, zone_map, public_ip):
logger.info("Rule '%s' already matches config, skipping", rule_name)
return {"rule_name": rule_name, "action": "skipped", "ip": public_ip, "error": None}
# Update existing policy
logger.info("Rule '%s' exists with IP %s, updating to %s", rule_name, existing_ip, public_ip)
# Update existing policy (IP or other attributes changed)
existing_ip = get_source_ip_from_policy(existing_policy)
logger.info("Rule '%s' exists but config changed (current IP: %s, desired IP: %s), updating",
rule_name, existing_ip, public_ip)
try:
payload = build_policy_payload(
name=rule_name,