diff --git a/openwrt/Justfile b/openwrt/Justfile index 1e18a83a5..ee5fb41f8 100644 --- a/openwrt/Justfile +++ b/openwrt/Justfile @@ -22,7 +22,7 @@ apply: EOF # Apply uci configuration - just eval-config | just ssh 'uci batch; uci commit' + just eval-config | just ssh 'sh' # Set up internet after a firmware reset if ! just ssh "ip link | grep -q pppoe-wan"; then diff --git a/openwrt/README.md b/openwrt/README.md index fdc63b24d..ee9c2ee40 100644 --- a/openwrt/README.md +++ b/openwrt/README.md @@ -22,13 +22,6 @@ on how to use this project. Tipp: The justfile is read by this project, let me know and I might switch to your project and contribute to your project. -## Known bugs - -- It is not possible to reliably delete all elements of a list section with uci - batch command. Currently, the code attempts to delete up to 10 list sections - and then create a new list: - https://github.com/Mic92/dotfiles/blob/00e178a0953461f676ecc9c0ab5b36a0742e12bd/openwrt/nix_uci/**init**.py#L108 - ## Related work - https://gitea.c3d2.de/zentralwerk/network/src/branch/master/nix/pkgs/ap.nix#L142 diff --git a/openwrt/nix_uci/__init__.py b/openwrt/nix_uci/__init__.py index a705991ea..7a6e3eeb8 100644 --- a/openwrt/nix_uci/__init__.py +++ b/openwrt/nix_uci/__init__.py @@ -38,10 +38,10 @@ def serialize_option_val( """Serialize an option value to UCI format.""" if isinstance(val, float | int | str): val = interpolate_secrets(str(val), secrets) - return [f"set {key}='{val}'"] + return [f"uci set {key}='{val}'"] if isinstance(val, list): - return [f"add_list {key}='{interpolate_secrets(v, secrets)}'" for v in val] + return [f"uci add_list {key}='{interpolate_secrets(v, secrets)}'" for v in val] msg = f"{val} is not a string" raise ConfigError(msg) @@ -58,15 +58,12 @@ def serialize_list_section( lines = [] _type = None - # TODO(@joerg): Investigate how to delete all list sections - # truncate list so we can start from fresh - _type = list_obj.get("_type") if _type is None: msg = f"{config_name}.@{section_name}[{idx}] has no type!" raise ConfigError(msg) del list_obj["_type"] - lines.append(f"set {config_name}.@{section_name}[{idx}]={_type}") + lines.append(f"uci set {config_name}.@{section_name}[{idx}]={_type}") for option_name, option in list_obj.items(): lines.extend( @@ -89,14 +86,14 @@ def serialize_named_section( lines = [] _type = None # truncate section so we can start from fresh - lines.append(f"delete {config_name}.{section_name}") + lines.append(f"uci delete {config_name}.{section_name}") _type = section.get("_type") if _type is None: msg = f"{config_name}.{section_name} has no type" raise ConfigError(msg) del section["_type"] - lines.append(f"set {config_name}.{section_name}={_type}") + lines.append(f"uci set {config_name}.{section_name}={_type}") for option_name, option in section.items(): # TODO(@joerg): Investigate how escaping works in UCI @@ -113,6 +110,11 @@ def serialize_named_section( def serialize_uci(configs: dict[str, Any], secrets: dict[str, str]) -> str: """Serialize configuration dictionary to UCI format.""" lines: list[str] = [] + # Add shebang to make it a proper shell script + lines.append("#!/bin/sh") + lines.append("set -e") + lines.append("") + config_names = [] for config_name, sections in configs.items(): config_names.append(config_name) @@ -124,12 +126,14 @@ def serialize_uci(configs: dict[str, Any], secrets: dict[str, str]) -> str: for section_name, section in sections.items(): if isinstance(section, list): - # NOTE: Clear out existing list sections (no better method available) - lines.extend( - f"delete {config_name}.@{section_name}[0]" for _i in range(10) - ) + # Use shell loop to properly delete all existing list sections + lines.append(f"# Clear all existing {config_name}.@{section_name} sections") + lines.append(f"while uci -q delete {config_name}.@{section_name}[-1] 2>/dev/null; do :; done") + lines.append("") + + # Add new sections lines.extend( - f"add {config_name} {section_name}" for _i in range(len(section)) + f"uci add {config_name} {section_name}" for _i in range(len(section)) ) for idx, list_obj in enumerate(section): @@ -142,7 +146,7 @@ def serialize_uci(configs: dict[str, Any], secrets: dict[str, str]) -> str: secrets, ), ) - # for option_name, option in section: + lines.append("") elif isinstance(section, dict): lines.extend( serialize_named_section( @@ -152,12 +156,16 @@ def serialize_uci(configs: dict[str, Any], secrets: dict[str, str]) -> str: secrets, ), ) + lines.append("") else: msg = f"{config_name}.{section_name} is not a valid section object, expected dict, got: {type(section).__name__}" raise ConfigError( msg, ) + # Commit all changes at the end + lines.append("uci commit") + return "\n".join(lines)