]> mj.ucw.cz Git - pynsc.git/commitdiff
Daemon configuration can have multiple files
authorMartin Mareš <mj@ucw.cz>
Sat, 22 Nov 2025 20:16:58 +0000 (21:16 +0100)
committerMartin Mareš <mj@ucw.cz>
Sat, 22 Nov 2025 20:16:58 +0000 (21:16 +0100)
For BIND, we now support separate config files for each zone.
This is useful when BIND is run with multiple views, each of them
including a subset of zones.

nsconfig/cli.py
nsconfig/daemon/__init__.py
nsconfig/daemon/bind.py

index 4712c0830466e716428025ddca56be289397603c..ee8b516600d6891ddd844b5ff417bef68257d2cd 100644 (file)
@@ -11,7 +11,9 @@ from nsconfig.core import Nsc, NscZonePrimary, NscZoneSecondary, NscZoneAlias
 
 def do_test(nsc: Nsc, args: Namespace) -> None:
     test_dir = Path(args.output)
-    test_dir.mkdir(exist_ok=True)
+    zone_dir = test_dir / 'zone'
+    zone_dir.mkdir(parents=True, exist_ok=True)
+
     for z in nsc.get_zones():
         print(f'Zone:        {z.name}')
         print(f'Type:        {z.zone_type.name}')
@@ -23,7 +25,7 @@ def do_test(nsc: Nsc, args: Namespace) -> None:
             print(f'Old hash:    {z.prev_state.hash}')
             print(f'New serial:  {z.state.serial}')
             print(f'New hash:    {z.state.hash}')
-            out_file = test_dir / z.safe_name
+            out_file = zone_dir / z.safe_name
             print(f'Dumping to:  {out_file}')
             with open(out_file, 'w') as f:
                 z.dump(file=f)
@@ -34,10 +36,12 @@ def do_test(nsc: Nsc, args: Namespace) -> None:
         print()
 
     if nsc.daemon:
-        conf_file = test_dir / 'daemon.conf'
-        print(f'Dumping daemon config to {conf_file}')
-        with open(conf_file, 'w') as f:
-            nsc.daemon.dump_config(file=f)
+        config_dir = test_dir / 'config'
+        for file, lines in nsc.daemon.create_config():
+            cfg = config_dir / file
+            cfg.parent.mkdir(parents=True, exist_ok=True)
+            print(f'Dumping {cfg}')
+            cfg.write_text("\n".join(lines) + "\n")
 
 
 def do_status(nsc: Nsc, args: Namespace) -> None:
index 9215d2b5045885cbb02732416f5a2ac33b520dc2..d9bf2c7f27b28624a56c34ba53c833e88fa72dff 100644 (file)
@@ -3,13 +3,16 @@
 
 from io import StringIO
 from pathlib import Path
-from typing import TextIO
 import subprocess
 import sys
+from typing import List, Tuple
 
 from nsconfig.core import Nsc, NscZone
 
 
+DaemonConfig = List[Tuple[str, List[str]]]
+
+
 class NscDaemon:
     nsc: Nsc
 
@@ -19,8 +22,8 @@ class NscDaemon:
     def setup(self, nsc: Nsc) -> None:
         self.nsc = nsc
 
-    def dump_config(self, file: TextIO = sys.stdout) -> None:
-        pass
+    def create_config(self) -> DaemonConfig:
+        return []
 
     def write_config(self) -> None:
         pass
@@ -39,21 +42,27 @@ class NscDaemon:
         if new_contents == old_new_contents:
             return False
         else:
+            path.parent.mkdir(parents=True, exist_ok=True)
             new_path = Path(str(path) + '.new')
             with open(new_path, 'w') as f:
                 f.write(new_contents)
             new_path.replace(path)
             return True
 
-    def _write_config(self, config_path: Path) -> bool:
-        string_stream = StringIO()
-        self.dump_config(string_stream)
-        if self._install_config(config_path, string_stream.getvalue()):
+    def _write_config(self, config_dir_path: Path) -> bool:
+        configs = self.create_config()
+        changed = False
+        for file, lines in configs:
+            contents = "\n".join(lines) + "\n"
+            if self._install_config(config_dir_path / file, contents):
+                changed = True
+
+        if changed:
             print('Wrote new daemon configuration')
-            return True
         else:
             print('Daemon configuration not changed')
-            return False
+
+        return changed
 
     def _run_command(self, argv, **kwargs) -> None:
         res = subprocess.run(argv, **kwargs)
index cf879ac0ee8bc1cfcb235099816a6a6de8ebb554..d0ac1616a3ecf5b14cc7b7811b4c7605285b791f 100644 (file)
@@ -2,46 +2,72 @@
 # (c) 2024 Martin Mareš <mj@ucw.cz>
 
 from pathlib import Path
-import sys
-from typing import TextIO
+from typing import List
 
 from nsconfig.core import NscZone, NscZonePrimary, NscZoneSecondary, NscZoneAlias
-from nsconfig.daemon import NscDaemon
+from nsconfig.daemon import NscDaemon, DaemonConfig
 
 
 class NscDaemonBind(NscDaemon):
-    config_path: Path
+    config_dir_path: Path
+    config_file: str
     control_command: str
+    split_config: bool
     need_full_reload: bool
 
     def __init__(self,
+                 config_directory: str = 'config',
                  config_file: str = 'named.conf.nsc',
+                 split_config: bool = False,
                  control_command: str = 'rndc') -> None:
         super().__init__()
-        self.config_path = Path(config_file)
+        self.config_dir_path = Path(config_directory)
+        self.config_file = config_file
+        self.split_config = split_config
         self.control_command = control_command
         self.need_full_reload = False
 
-    def dump_config(self, file: TextIO = sys.stdout) -> None:
-        file.write('# Domains managed by NSC\n')
-        file.write('# This file was automatically generated by NSC, please do not edit manually.\n\n')
-        for z in self.nsc.get_zones():
-            file.write(f'zone "{z.name}" in {{\n')  # broken editor: }}
-            if isinstance(z, NscZonePrimary) or isinstance(z, NscZoneAlias):
-                file.write('\ttype master;\n')
-                file.write(f'\tfile "{z.zone_file}";\n')
-            elif isinstance(z, NscZoneSecondary):
-                file.write('\ttype slave;\n')
-                file.write(f'\tfile "{z.secondary_file}";\n')
-                file.write(f'\tmasters {{ {z.primary_server}; }};\n')
-            else:
-                raise NotImplementedError()
-            for opt in z.config.daemon_options:
-                file.write(f'\t{opt}\n')
-            file.write('}\n\n')
+    def create_config(self) -> DaemonConfig:
+        WARNING = '# This file was automatically generated by NSC, please do not edit manually.'
+        if self.split_config:
+            configs = []
+            for z in self.nsc.get_zones():
+                cf = []
+                cf.append(f'# Zone configuration for {z.name}')
+                cf.append(WARNING)
+                cf.append("")
+                cf.extend(self._zone_config(z))
+                configs.append((f'zone/{z.safe_name}', cf))
+            return configs
+        else:
+            cf = []
+            cf.append('# Domains managed by NSC')
+            cf.append(WARNING)
+            cf.append("")
+            for z in self.nsc.get_zones():
+                cf.extend(self._zone_config(z))
+                cf.append("")
+            return [(self.config_file, cf)]
+
+    def _zone_config(self, z: NscZone) -> List[str]:
+        cf = []
+        cf.append(f'zone "{z.name}" in {{')  # broken editor: }}
+        if isinstance(z, NscZonePrimary) or isinstance(z, NscZoneAlias):
+            cf.append('\ttype master;')
+            cf.append(f'\tfile "{z.zone_file}";')
+        elif isinstance(z, NscZoneSecondary):
+            cf.append('\ttype slave;')
+            cf.append(f'\tfile "{z.secondary_file}";')
+            cf.append(f'\tmasters {{ {z.primary_server}; }};')
+        else:
+            raise NotImplementedError()
+        for opt in z.config.daemon_options:
+            cf.append(f'\t{opt}')
+        cf.append('}')
+        return cf
 
     def write_config(self) -> None:
-        if self._write_config(self.config_path):
+        if self._write_config(self.config_dir_path):
             self.need_full_reload = True
 
     def reload_zone(self, z: NscZone) -> None: