From 37e24fbf10596e13a5282207068722e5c3a347e8 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Tue, 9 Sep 2014 10:42:20 +0000 Subject: [PATCH] Create database to track updates that have been performed --- Makefile.am | 1 + src/ddns/__init__.py | 4 ++ src/ddns/database.py | 110 ++++++++++++++++++++++++++++++++++++++++++ src/ddns/providers.py | 85 +++++++++++++++++++++++++++++--- 4 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 src/ddns/database.py diff --git a/Makefile.am b/Makefile.am index fbf4bc1..2ca1c3b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -66,6 +66,7 @@ dist_configs_DATA = \ ddns_PYTHON = \ src/ddns/__init__.py \ src/ddns/__version__.py \ + src/ddns/database.py \ src/ddns/errors.py \ src/ddns/i18n.py \ src/ddns/providers.py \ diff --git a/src/ddns/__init__.py b/src/ddns/__init__.py index fbebc0e..84b8c80 100644 --- a/src/ddns/__init__.py +++ b/src/ddns/__init__.py @@ -28,6 +28,7 @@ from i18n import _ logger = logging.getLogger("ddns.core") logger.propagate = 1 +import database import providers from .errors import * @@ -76,6 +77,9 @@ class DDNSCore(object): # Add the system class. self.system = DDNSSystem(self) + # Open the database. + self.db = database.DDNSDatabase(self, "/var/lib/ddns.db") + def get_provider_names(self): """ Returns a list of names of all registered providers. diff --git a/src/ddns/database.py b/src/ddns/database.py new file mode 100644 index 0000000..9301187 --- /dev/null +++ b/src/ddns/database.py @@ -0,0 +1,110 @@ +#!/usr/bin/python +############################################################################### +# # +# ddns - A dynamic DNS client for IPFire # +# Copyright (C) 2014 IPFire development team # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +############################################################################### + +import datetime +import os.path +import sqlite3 + +# Initialize the logger. +import logging +logger = logging.getLogger("ddns.database") +logger.propagate = 1 + +class DDNSDatabase(object): + def __init__(self, core, path): + self.core = core + + # Open the database file + self._db = self._open_database(path) + + def __del__(self): + self._close_database() + + def _open_database(self, path): + logger.debug("Opening database %s" % path) + + exists = os.path.exists(path) + + conn = sqlite3.connect(path, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES) + conn.isolation_level = None + + if not exists: + logger.debug("Initialising database layout") + c = conn.cursor() + c.executescript(""" + CREATE TABLE updates ( + hostname TEXT NOT NULL, + status TEXT NOT NULL, + message TEXT, + timestamp timestamp NOT NULL + ); + + CREATE TABLE settings ( + k TEXT NOT NULL, + v TEXT NOT NULL + ); + """) + c.execute("INSERT INTO settings(k, v) VALUES(?, ?)", ("version", "1")) + + return conn + + def _close_database(self): + if self._db: + self._db_close() + self._db = None + + def _execute(self, query, *parameters): + c = self._db.cursor() + try: + c.execute(query, parameters) + finally: + c.close() + + def add_update(self, hostname, status, message=None): + self._execute("INSERT INTO updates(hostname, status, message, timestamp) \ + VALUES(?, ?, ?, ?)", hostname, status, message, datetime.datetime.utcnow()) + + def log_success(self, hostname): + logger.debug("Logging successful update for %s" % hostname) + + return self.add_update(hostname, "success") + + def log_failure(self, hostname, exception): + if exception: + message = "%s: %s" % (exception.__class__.__name__, exception.reason) + else: + message = None + + logger.debug("Logging failed update for %s: %s" % (hostname, message or "")) + + return self.add_update(hostname, "failure", message=message) + + def last_update(self, hostname, status="success"): + c = self._db.cursor() + + try: + c.execute("SELECT timestamp FROM updates WHERE hostname = ? AND status = ? \ + ORDER BY timestamp DESC LIMIT 1", (hostname, status)) + + for row in c: + return row[0] + finally: + c.close() diff --git a/src/ddns/providers.py b/src/ddns/providers.py index 648eab6..28bdf41 100644 --- a/src/ddns/providers.py +++ b/src/ddns/providers.py @@ -19,6 +19,7 @@ # # ############################################################################### +import datetime import logging import subprocess import urllib2 @@ -57,6 +58,10 @@ class DDNSProvider(object): DEFAULT_SETTINGS = {} + # holdoff time - Number of days no update is performed unless + # the IP address has changed. + holdoff_days = 30 + # Automatically register all providers. class __metaclass__(type): def __init__(provider, name, bases, dict): @@ -89,6 +94,10 @@ class DDNSProvider(object): def __cmp__(self, other): return cmp(self.hostname, other.hostname) + @property + def db(self): + return self.core.db + def get(self, key, default=None): """ Get a setting from the settings dictionary. @@ -127,17 +136,23 @@ class DDNSProvider(object): if force: logger.debug(_("Updating %s forced") % self.hostname) - # Check if we actually need to update this host. - elif self.is_uptodate(self.protocols): - logger.debug(_("The dynamic host %(hostname)s (%(provider)s) is already up to date") % \ - { "hostname" : self.hostname, "provider" : self.name }) + # Do nothing if no update is required + elif not self.requires_update: return # Execute the update. - self.update() + try: + self.update() + + # In case of any errors, log the failed request and + # raise the exception. + except DDNSError as e: + self.core.db.log_failure(self.hostname, e) + raise logger.info(_("Dynamic DNS update for %(hostname)s (%(provider)s) successful") % \ { "hostname" : self.hostname, "provider" : self.name }) + self.core.db.log_success(self.hostname) def update(self): for protocol in self.protocols: @@ -157,7 +172,31 @@ class DDNSProvider(object): # Maybe this will raise NotImplementedError at some time #raise NotImplementedError - def is_uptodate(self, protos): + @property + def requires_update(self): + # If the IP addresses have changed, an update is required + if self.ip_address_changed(self.protocols): + logger.debug(_("An update for %(hostname)s (%(provider)s)" + " is performed because of an IP address change") % \ + { "hostname" : self.hostname, "provider" : self.name }) + + return True + + # If the holdoff time has expired, an update is required, too + if self.holdoff_time_expired(): + logger.debug(_("An update for %(hostname)s (%(provider)s)" + " is performed because the holdoff time has expired") % \ + { "hostname" : self.hostname, "provider" : self.name }) + + return True + + # Otherwise, we don't need to perform an update + logger.debug(_("No update required for %(hostname)s (%(provider)s)") % \ + { "hostname" : self.hostname, "provider" : self.name }) + + return False + + def ip_address_changed(self, protos): """ Returns True if this host is already up to date and does not need to change the IP address on the @@ -174,9 +213,39 @@ class DDNSProvider(object): continue if not current_address in addresses: - return False + return True + + return False + + def holdoff_time_expired(self): + """ + Returns true if the holdoff time has expired + and the host requires an update + """ + # If no holdoff days is defined, we cannot go on + if not self.holdoff_days: + return False + + # Get the timestamp of the last successfull update + last_update = self.db.last_update(self.hostname) + + # If no timestamp has been recorded, no update has been + # performed. An update should be performed now. + if not last_update: + return True - return True + # Determine when the holdoff time ends + holdoff_end = last_update + datetime.timedelta(days=self.holdoff_days) + + now = datetime.datetime.utcnow() + + if now >= holdoff_end: + logger.debug("The holdoff time has expired for %s" % self.hostname) + return True + else: + logger.debug("Updates for %s are held off until %s" % \ + (self.hostname, holdoff_end)) + return False def send_request(self, *args, **kwargs): """ -- 2.39.2