]> git.ipfire.org Git - ipfire-2.x.git/blobdiff - config/unbound/unbound-dhcp-leases-bridge
kernel: reset asix88179 twice like in older kernels
[ipfire-2.x.git] / config / unbound / unbound-dhcp-leases-bridge
index a2df5f10144151632fc80ce040a7fb424d6bea0f..7f89f620a1400c5542e2d9585ffa4b658d2a115e 100644 (file)
@@ -22,6 +22,8 @@
 import argparse
 import datetime
 import daemon
+import filecmp
+import functools
 import ipaddress
 import logging
 import logging.handlers
@@ -30,27 +32,38 @@ import re
 import signal
 import stat
 import subprocess
+import sys
 import tempfile
+import time
 
 import inotify.adapters
 
 LOCAL_TTL = 60
 
-def setup_logging(loglevel=logging.INFO):
-       log = logging.getLogger("dhcp")
+log = logging.getLogger("dhcp")
+log.setLevel(logging.DEBUG)
+
+def setup_logging(daemon=True, loglevel=logging.INFO):
        log.setLevel(loglevel)
 
+       # Log to syslog by default
        handler = logging.handlers.SysLogHandler(address="/dev/log", facility="daemon")
-       handler.setLevel(loglevel)
+       log.addHandler(handler)
 
+       # Format everything
        formatter = logging.Formatter("%(name)s[%(process)d]: %(message)s")
        handler.setFormatter(formatter)
 
-       log.addHandler(handler)
+       handler.setLevel(loglevel)
 
-       return log
+       # If we are running in foreground, we should write everything to the console, too
+       if not daemon:
+               handler = logging.StreamHandler()
+               log.addHandler(handler)
 
-log = logging.getLogger("dhcp")
+               handler.setLevel(loglevel)
+
+       return log
 
 def ip_address_to_reverse_pointer(address):
        parts = address.split(".")
@@ -72,6 +85,12 @@ class UnboundDHCPLeasesBridge(object):
                self.fix_leases_file = fix_leases_file
                self.hosts_file = hosts_file
 
+               self.watches = {
+                       self.leases_file     : inotify.constants.IN_MODIFY,
+                       self.fix_leases_file : 0,
+                       self.hosts_file      : 0,
+               }
+
                self.unbound = UnboundConfigWriter(unbound_leases_file)
                self.running = False
 
@@ -79,37 +98,54 @@ class UnboundDHCPLeasesBridge(object):
                log.info("Unbound DHCP Leases Bridge started on %s" % self.leases_file)
                self.running = True
 
-               # Initial setup
-               self.hosts = self.read_static_hosts()
-               self.update_dhcp_leases()
+               i = inotify.adapters.Inotify()
 
-               i = inotify.adapters.Inotify([
-                       self.leases_file,
-                       self.fix_leases_file,
-                       self.hosts_file,
-               ])
+               # Add watches for the directories of every relevant file
+               for f, mask in self.watches.items():
+                       i.add_watch(
+                               os.path.dirname(f),
+                               mask | inotify.constants.IN_CLOSE_WRITE | inotify.constants.IN_MOVED_TO,
+                       )
 
-               for event in i.event_gen():
-                       # End if we are requested to terminate
-                       if not self.running:
-                               break
+               # Enabled so that we update hosts and leases on startup
+               update_hosts = update_leases = True
 
-                       if event is None:
-                               continue
+               while self.running:
+                       log.debug("Wakeup of main loop")
+
+                       # Process the entire inotify queue and identify what we need to do
+                       for event in i.event_gen():
+                               # Nothing to do
+                               if event is None:
+                                       break
+
+                               # Decode the event
+                               header, type_names, path, filename = event
+
+                               file = os.path.join(path, filename)
 
-                       header, type_names, watch_path, filename = event
+                               log.debug("inotify event received for %s: %s", file, " ".join(type_names))
 
-                       # Update leases after leases file has been modified
-                       if "IN_MODIFY" in type_names:
-                               # Reload hosts
-                               if watch_path == self.hosts_file:
-                                       self.hosts = self.read_static_hosts()
+                               # Did the hosts file change?
+                               if self.hosts_file == file:
+                                       update_hosts = True
 
+                               # We will need to update the leases on any change
+                               update_leases = True
+
+                       # Update hosts (if needed)
+                       if update_hosts:
+                               self.hosts = self.read_static_hosts()
+
+                       # Update leases (if needed)
+                       if update_leases:
                                self.update_dhcp_leases()
 
-                       # If the file is deleted, we re-add the watcher
-                       if "IN_IGNORED" in type_names:
-                               i.add_watch(watch_path)
+                       # Reset
+                       update_hosts = update_leases = False
+
+                       # Wait a moment before we start the next iteration
+                       time.sleep(5)
 
                log.info("Unbound DHCP Leases Bridge terminated")
 
@@ -406,10 +442,14 @@ class Lease(object):
                        if address in subnet:
                                return subnets[subnet]
 
-               # Fall back to localdomain if no match could be found
-               return "localdomain"
+               # Load main settings
+               settings = self.read_settings("/var/ipfire/main/settings")
+
+               # Fall back to the host domain if no match could be found
+               return settings.get("DOMAINNAME", "localdomain")
 
        @staticmethod
+       @functools.cache
        def read_settings(filename):
                settings = {}
 
@@ -475,64 +515,46 @@ class UnboundConfigWriter(object):
        def __init__(self, path):
                self.path = path
 
-               self._cached_leases = []
-
        def update_dhcp_leases(self, leases):
-               # Find any leases that have expired or do not exist any more
-               # but are still in the unbound local data
-               removed_leases = [l for l in self._cached_leases if not l in leases]
-
-               # Find any leases that have been added
-               new_leases = [l for l in leases if l not in self._cached_leases]
-
-               # End here if nothing has changed
-               if not new_leases and not removed_leases:
-                       return
-
                # Write out all leases
-               self.write_dhcp_leases(leases)
-
-               # Update unbound about changes
-               for l in removed_leases:
-                       try:
-                               for name, ttl, type, content in l.rrset:
-                                       log.debug("Removing records for %s" % name)
-                                       self._control("local_data_remove", name)
+               if self.write_dhcp_leases(leases):
+                       log.debug("Reloading Unbound...")
 
-                       # If the lease cannot be removed we will try the next one
-                       except:
-                               continue
+                       # Reload the configuration without dropping the cache
+                       self._control("reload_keep_cache")
 
-                       # If the removal was successful, we will remove it from the cache
-                       else:
-                               self._cached_leases.remove(l)
+       def write_dhcp_leases(self, leases):
+               log.debug("Writing DHCP leases...")
 
-               for l in new_leases:
-                       try:
+               with tempfile.NamedTemporaryFile(mode="w") as f:
+                       for l in sorted(leases, key=lambda x: x.ipaddr):
                                for rr in l.rrset:
-                                       log.debug("Adding new record %s" % " ".join(rr))
-                                       self._control("local_data", *rr)
+                                       f.write("local-data: \"%s\"\n" % " ".join(rr))
 
-                       # If the lease cannot be added we will try the next one
-                       except:
-                               continue
+                       # Flush the file
+                       f.flush()
 
-                       # Add lease to cache when successfully added
-                       else:
-                               self._cached_leases.append(l)
+                       # Compare if the new leases file has changed from the previous version
+                       try:
+                               if filecmp.cmp(f.name, self.path, shallow=False):
+                                       log.debug("The generated leases file has not changed")
 
-       def write_dhcp_leases(self, leases):
-               with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
-                       filename = f.name
+                                       return False
 
-                       for l in leases:
-                               for rr in l.rrset:
-                                       f.write("local-data: \"%s\"\n" % " ".join(rr))
+                               # Remove the old file
+                               os.unlink(self.path)
+
+                       # If the previous file did not exist, just keep falling through
+                       except FileNotFoundError:
+                               pass
 
                        # Make file readable for everyone
                        os.fchmod(f.fileno(), stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP|stat.S_IROTH)
 
-               os.rename(filename, self.path)
+                       # Move the file to its destination
+                       os.link(f.name, self.path)
+
+               return True
 
        def _control(self, *args):
                command = ["unbound-control"]
@@ -546,7 +568,7 @@ class UnboundConfigWriter(object):
                        log.critical("Could not run %s, error code: %s: %s" % (
                                " ".join(command), e.returncode, e.output))
 
-                       raise
+                       raise e
 
 
 if __name__ == "__main__":
@@ -579,16 +601,17 @@ if __name__ == "__main__":
                elif args.verbose >= 2:
                        loglevel = logging.DEBUG
 
-       setup_logging(loglevel)
-
        bridge = UnboundDHCPLeasesBridge(args.dhcp_leases, args.fix_leases,
                args.unbound_leases, args.hosts)
 
-       ctx = daemon.DaemonContext(detach_process=args.daemon)
-       ctx.signal_map = {
-               signal.SIGHUP  : bridge.update_dhcp_leases,
-               signal.SIGTERM : bridge.terminate,
-       }
+       with daemon.DaemonContext(
+               detach_process=args.daemon,
+               stderr=None if args.daemon else sys.stderr,
+               signal_map = {
+                       signal.SIGHUP  : bridge.update_dhcp_leases,
+                       signal.SIGTERM : bridge.terminate,
+               },
+       ) as daemon:
+               setup_logging(daemon=args.daemon, loglevel=loglevel)
 
-       with ctx:
                bridge.run()