import argparse
import datetime
import daemon
+import filecmp
import functools
import ipaddress
import logging
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 __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)
+ if self.write_dhcp_leases(leases):
+ log.debug("Reloading Unbound...")
- # 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 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"]
log.critical("Could not run %s, error code: %s: %s" % (
" ".join(command), e.returncode, e.output))
- raise
+ raise e
if __name__ == "__main__":
bridge = UnboundDHCPLeasesBridge(args.dhcp_leases, args.fix_leases,
args.unbound_leases, args.hosts)
- ctx = daemon.DaemonContext(detach_process=args.daemon, stderr=sys.stderr)
- ctx.signal_map = {
- signal.SIGHUP : bridge.update_dhcp_leases,
- signal.SIGTERM : bridge.terminate,
- }
-
- with ctx:
+ 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)
bridge.run()