]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
client: migrate: new command to migrate configuration to newer version
authorAleš Mrázek <ales.mrazek@nic.cz>
Mon, 28 Apr 2025 12:47:13 +0000 (14:47 +0200)
committerAleš Mrázek <ales.mrazek@nic.cz>
Tue, 5 Aug 2025 09:21:00 +0000 (11:21 +0200)
NEWS
doc/user/manager-client.rst
etc/config/config.migrate.yaml [new file with mode: 0644]
python/knot_resolver/client/commands/migrate.py [new file with mode: 0644]
scripts/poe-tasks/check

diff --git a/NEWS b/NEWS
index 9788bb0a5dbae4915f510b5267763d9ff384d741..9ea4cb91fed95045da2c01ffe0d60677ad4fb737 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -4,6 +4,7 @@ Knot Resolver 6.0.16 (2025-0m-dd)
 Improvements
 ------------
 - reduce validation strictness for domain names (#934, !1727)
+- kresctl migrate: new command for migrating the configuration to a newer version
 
 Incompatible changes
 --------------------
index c0c757c01c01083e0849e6263e5cac763de8049c..c08b33adc828d75d2fed5b01d4b9e90b27754281 100644 (file)
@@ -157,6 +157,25 @@ single ``kresctl`` command.
 
         $ kresctl config set -p /workers 8
 
+.. option:: migrate
+
+    Migrates JSON or YAML configuration to the newer version.
+
+    .. option:: --json, --yaml
+
+        :default: --yaml
+
+        Optional, get migrated configuration data in JSON or YAML format.
+
+    .. option:: input_file
+
+        File with configuration in YAML or JSON format.
+
+    .. option:: [output_file]
+
+        Optional, output file for migrated configuration in desired output format.
+        If not specified, the migrated configuration is printed into ``stdout``.
+
 .. option:: metrics
 
     Get aggregated metrics from the running resolver in JSON format (default) or optionally in Prometheus format.
diff --git a/etc/config/config.migrate.yaml b/etc/config/config.migrate.yaml
new file mode 100644 (file)
index 0000000..c7c933e
--- /dev/null
@@ -0,0 +1,41 @@
+# dns64: true
+
+# dnssec: false
+
+dns64:
+  rev-ttl: 1d
+
+dnssec:
+  refresh-time: 10m
+  hold-down-time: 30d
+  time-skew-detection: true
+  keep-removed: 10
+  trust-anchor-sentinel: true
+  trust-anchor-signal-query: true
+
+local-data:
+  root-fallback-addresses:
+    j.root-servers.net.:
+      - 2001:503:c27::2:30
+      - 192.58.128.30
+  root-fallback-addresses-files:
+    - rfa.zone
+
+logging:
+  dnssec-bogus: true
+  debugging:
+    assertion-abort: false
+    assertion-fork: 5m
+
+network:
+  tls:
+    auto-discovery: true
+    files-watchdog: true
+
+webmgmt:
+  interface: 127.0.0.1@5001
+
+max-workers: 64
+
+rate-limiting:
+  rate-limit: 100
\ No newline at end of file
diff --git a/python/knot_resolver/client/commands/migrate.py b/python/knot_resolver/client/commands/migrate.py
new file mode 100644 (file)
index 0000000..06e377a
--- /dev/null
@@ -0,0 +1,158 @@
+import argparse
+import copy
+import sys
+from typing import Any, Dict, List, Optional, Tuple, Type
+
+from knot_resolver.client.command import Command, CommandArgs, CompWords, comp_get_words, register_command
+from knot_resolver.utils.modeling.exceptions import DataParsingError
+from knot_resolver.utils.modeling.parsing import DataFormat, try_to_parse
+
+
+def _remove(config: Dict[str, Any], path: str) -> Optional[Any]:
+    keys = path.split("/")
+    last = keys[-1]
+
+    current = config
+    for key in keys[1:-1]:
+        if key in current:
+            current = current[key]
+        else:
+            return None
+    if isinstance (current, dict) and last in current:
+        val = copy.copy(current[last])
+        del current[last]
+        print(f"removed {path}")
+        return val
+    return None
+
+
+def _add(config: Dict[str, Any], path: str, val: Any, rewrite: bool = False) -> None:
+    keys = path.split("/")
+    last = keys[-1]
+
+    current = config
+    for key in keys[1:-1]:
+        if key not in current:
+            current[key] = {}
+        elif key in current and not isinstance(current[key], dict):
+            current[key] = {}
+        current = current[key]
+
+    if rewrite or last not in current:
+        current[last] = val
+        print(f"added {path}")
+
+
+def _rename(config: Dict[str, Any], path: str, new_path: str) -> None:
+    val: Optional[Any] = _remove(config, path)
+    if val:
+        _add(config, new_path, val)
+
+
+@register_command
+class MigrateCommand(Command):
+    def __init__(self, namespace: argparse.Namespace) -> None:
+        super().__init__(namespace)
+        self.input_file: str = namespace.input_file
+        self.output_file: Optional[str] = namespace.output_file
+        self.output_format: DataFormat = namespace.output_format
+
+    @staticmethod
+    def register_args_subparser(
+        subparser: "argparse._SubParsersAction[argparse.ArgumentParser]",
+    ) -> Tuple[argparse.ArgumentParser, "Type[Command]"]:
+        migrate = subparser.add_parser("migrate", help="Migrates JSON or YAML configuration to the newer version.")
+
+        migrate.set_defaults(output_format=DataFormat.YAML)
+        output_formats = migrate.add_mutually_exclusive_group()
+        output_formats.add_argument(
+            "--json",
+            help="Get migrated configuration data in JSON format.",
+            const=DataFormat.JSON,
+            action="store_const",
+            dest="output_format",
+        )
+        output_formats.add_argument(
+            "--yaml",
+            help="Get migrated configuration data in YAML format, default.",
+            const=DataFormat.YAML,
+            action="store_const",
+            dest="output_format",
+        )
+
+        migrate.add_argument(
+            "input_file",
+            type=str,
+            help="File with configuration in YAML or JSON format.",
+        )
+        migrate.add_argument(
+            "output_file",
+            type=str,
+            nargs="?",
+            help="Optional, output file for migrated configuration in desired output format. If not specified, migrated configuration is printed.",
+            default=None,
+        )
+        return migrate, MigrateCommand
+
+    @staticmethod
+    def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords:
+        return comp_get_words(args, parser)
+
+    def run(self, args: CommandArgs) -> None:
+        with open(self.input_file, "r") as f:
+            data = f.read()
+
+        try:
+            parsed = try_to_parse(data)
+        except DataParsingError as e:
+            print(e, file=sys.stderr)
+            sys.exit(1)
+
+        new = parsed.copy()
+
+        # REMOVE
+        _remove(new, "/dnssec/refresh-time")
+        _remove(new, "/dnssec/hold-down-time")
+        _remove(new, "/dnssec/time-skew-detection")
+        _remove(new, "/local-data/root-fallback-addresses")
+        _remove(new, "/local-data/root-fallback-addresses-files")
+        _remove(new, "/logging/debugging")
+        _remove(new, "/max-workers")
+        _remove(new, "/network/tls/auto-discovery")
+        _remove(new, "/webmgmt")
+
+        # RENAME/MOVE
+        dns64_key = "dns64"
+        if dns64_key in new:
+            if new[dns64_key] is False:
+                _add(new, "/dns64/enabled", False, rewrite=True)
+            else:
+                _add(new, "/dns64/enabled", True, rewrite=True)
+        _rename(new, "/dns64/rev-ttl", "/dns64/reverse-ttl")
+        dnssec_key = "dnssec"
+        if dnssec_key in new:
+            if new[dnssec_key] is False:
+                _add(new, "/dnssec/enabled", False, rewrite=True)
+            else:
+                # by default the DNSSEC is enabled
+                pass
+        _rename(new, "/dnssec/keep-removed", "/dnssec/trust-anchors-keep-removed")
+        _rename(new, "/dnssec/trust-anchor-sentinel", "/dnssec/sentinel")
+        _rename(new, "/dnssec/trust-anchor-signal-query", "/dnssec/signal-query")
+        _rename(new, "/logging/dnssec-bogus", "/dnssec/log-bogus")
+        _rename(new, "/network/tls/files-watchdog", "/network/tls/watchdog")
+        rate_limiting_key = "rate-limiting"
+        if rate_limiting_key in new:
+            _add(new, "/rate-limiting/enabled", True)
+
+        # remove empty dicts
+        new = {k: v for k, v in new.items() if v}
+
+        dumped = self.output_format.dict_dump(new)
+        if self.output_file:
+            with open(self.output_file, "w") as f:
+                f.write(dumped)
+        else:
+            print("\nNew migrated configuration:")
+            print("---")
+            print(dumped)
index 76da1209a099e19f78776a2c10d91aa10dca4b5d..f5033f174913af47aca9fedc69571fd1ab8d2280 100755 (executable)
@@ -41,6 +41,12 @@ python -m knot_resolver.client schema | diff - doc/_static/config.schema.json
 check_rv $?
 echo
 
+# check etc/config/config.migrate.yaml
+echo -e "${yellow}Checking etc/config/config.migrate.yaml${reset}"
+python -m knot_resolver.client migrate etc/config/config.migrate.yaml > /dev/null
+check_rv $?
+echo
+
 # fancy messages at the end :)
 fancy_message