]> git.ipfire.org Git - oddments/ddns.git/blobdiff - src/ddns/providers.py
Merge branch 'database'
[oddments/ddns.git] / src / ddns / providers.py
index a5385a9c6b1bb25c12d11dd5950d0aac74bff917..c6ef5c0d7c0cd0a22e4f43d8ead72615e35d6e25 100644 (file)
@@ -19,6 +19,7 @@
 #                                                                             #
 ###############################################################################
 
 #                                                                             #
 ###############################################################################
 
+import datetime
 import logging
 import subprocess
 import urllib2
 import logging
 import subprocess
 import urllib2
@@ -57,6 +58,10 @@ class DDNSProvider(object):
 
        DEFAULT_SETTINGS = {}
 
 
        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):
        # 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)
 
        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.
        def get(self, key, default=None):
                """
                        Get a setting from the settings dictionary.
@@ -127,22 +136,67 @@ class DDNSProvider(object):
                if force:
                        logger.debug(_("Updating %s forced") % self.hostname)
 
                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.
                        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 })
 
                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):
 
        def update(self):
+               for protocol in self.protocols:
+                       if self.have_address(protocol):
+                               self.update_protocol(protocol)
+                       else:
+                               self.remove_protocol(protocol)
+
+       def update_protocol(self, proto):
                raise NotImplementedError
 
                raise NotImplementedError
 
-       def is_uptodate(self, protos):
+       def remove_protocol(self, proto):
+               logger.warning(_("%(hostname)s current resolves to an IP address"
+                       " of the %(proto)s protocol which could not be removed by ddns") % \
+                       { "hostname" : self.hostname, "proto" : proto })
+
+               # Maybe this will raise NotImplementedError at some time
+               #raise NotImplementedError
+
+       @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
                """
                        Returns True if this host is already up to date
                        and does not need to change the IP address on the
@@ -159,9 +213,39 @@ class DDNSProvider(object):
                                continue
 
                        if not current_address in addresses:
                                continue
 
                        if not current_address in addresses:
-                               return False
+                               return True
 
 
-               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
+
+               # 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):
                """
 
        def send_request(self, *args, **kwargs):
                """
@@ -176,6 +260,18 @@ class DDNSProvider(object):
                """
                return self.core.system.get_address(proto) or default
 
                """
                return self.core.system.get_address(proto) or default
 
+       def have_address(self, proto):
+               """
+                       Returns True if an IP address for the given protocol
+                       is known and usable.
+               """
+               address = self.get_address(proto)
+
+               if address:
+                       return True
+
+               return False
+
 
 class DDNSProtocolDynDNS2(object):
        """
 
 class DDNSProtocolDynDNS2(object):
        """
@@ -189,19 +285,22 @@ class DDNSProtocolDynDNS2(object):
        # http://dyn.com/support/developers/api/perform-update/
        # http://dyn.com/support/developers/api/return-codes/
 
        # http://dyn.com/support/developers/api/perform-update/
        # http://dyn.com/support/developers/api/return-codes/
 
-       def _prepare_request_data(self):
+       def prepare_request_data(self, proto):
                data = {
                        "hostname" : self.hostname,
                data = {
                        "hostname" : self.hostname,
-                       "myip"     : self.get_address("ipv4"),
+                       "myip"     : self.get_address(proto),
                }
 
                return data
 
                }
 
                return data
 
-       def update(self):
-               data = self._prepare_request_data()
+       def update_protocol(self, proto):
+               data = self.prepare_request_data(proto)
+
+               return self.send_request(data)
 
 
+       def send_request(self, data):
                # Send update to the server.
                # Send update to the server.
-               response = self.send_request(self.url, data=data,
+               response = DDNSProvider.send_request(self, self.url, data=data,
                        username=self.username, password=self.password)
 
                # Get the full response message.
                        username=self.username, password=self.password)
 
                # Get the full response message.
@@ -371,10 +470,10 @@ class DDNSProviderDHS(DDNSProvider):
 
        url = "http://members.dhs.org/nic/hosts"
 
 
        url = "http://members.dhs.org/nic/hosts"
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                        "domain"       : self.hostname,
                data = {
                        "domain"       : self.hostname,
-                       "ip"           : self.get_address("ipv4"),
+                       "ip"           : self.get_address(proto),
                        "hostcmd"      : "edit",
                        "hostcmdstage" : "2",
                        "type"         : "4",
                        "hostcmd"      : "edit",
                        "hostcmdstage" : "2",
                        "type"         : "4",
@@ -403,10 +502,10 @@ class DDNSProviderDNSpark(DDNSProvider):
 
        url = "https://control.dnspark.com/api/dynamic/update.php"
 
 
        url = "https://control.dnspark.com/api/dynamic/update.php"
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                        "domain" : self.hostname,
                data = {
                        "domain" : self.hostname,
-                       "ip"     : self.get_address("ipv4"),
+                       "ip"     : self.get_address(proto),
                }
 
                # Send update to the server.
                }
 
                # Send update to the server.
@@ -451,9 +550,9 @@ class DDNSProviderDtDNS(DDNSProvider):
 
        url = "https://www.dtdns.com/api/autodns.cfm"
 
 
        url = "https://www.dtdns.com/api/autodns.cfm"
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                data = {
-                       "ip" : self.get_address("ipv4"),
+                       "ip" : self.get_address(proto),
                        "id" : self.hostname,
                        "pw" : self.password
                }
                        "id" : self.hostname,
                        "pw" : self.password
                }
@@ -519,8 +618,10 @@ class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
 
        url = "https://api.dynu.com/nic/update"
 
 
        url = "https://api.dynu.com/nic/update"
 
-       def _prepare_request_data(self):
-               data = DDNSProtocolDynDNS2._prepare_request_data(self)
+       # DynU sends the IPv6 and IPv4 address in one request
+
+       def update(self):
+               data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
 
                # This one supports IPv6
                myipv6 = self.get_address("ipv6")
 
                # This one supports IPv6
                myipv6 = self.get_address("ipv6")
@@ -529,7 +630,7 @@ class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
                if myipv6:
                        data["myipv6"] = myipv6
 
                if myipv6:
                        data["myipv6"] = myipv6
 
-               return data
+               self._send_request(data)
 
 
 class DDNSProviderEasyDNS(DDNSProtocolDynDNS2, DDNSProvider):
 
 
 class DDNSProviderEasyDNS(DDNSProtocolDynDNS2, DDNSProvider):
@@ -568,9 +669,9 @@ class DDNSProviderDynsNet(DDNSProvider):
 
        url = "http://www.dyns.net/postscript011.php"
 
 
        url = "http://www.dyns.net/postscript011.php"
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                data = {
-                       "ip"       : self.get_address("ipv4"),
+                       "ip"       : self.get_address(proto),
                        "host"     : self.hostname,
                        "username" : self.username,
                        "password" : self.password,
                        "host"     : self.hostname,
                        "username" : self.username,
                        "password" : self.password,
@@ -604,6 +705,7 @@ class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
        handle    = "enom.com"
        name      = "eNom Inc."
        website   = "http://www.enom.com/"
        handle    = "enom.com"
        name      = "eNom Inc."
        website   = "http://www.enom.com/"
+       protocols = ("ipv4",)
 
        # There are very detailed information about how to send an update request and
        # the respone codes.
 
        # There are very detailed information about how to send an update request and
        # the respone codes.
@@ -611,11 +713,11 @@ class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
 
        url = "https://dynamic.name-services.com/interface.asp"
 
 
        url = "https://dynamic.name-services.com/interface.asp"
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                        "command"        : "setdnshost",
                        "responsetype"   : "xml",
                data = {
                        "command"        : "setdnshost",
                        "responsetype"   : "xml",
-                       "address"        : self.get_address("ipv4"),
+                       "address"        : self.get_address(proto),
                        "domainpassword" : self.password,
                        "zone"           : self.hostname
                }
                        "domainpassword" : self.password,
                        "zone"           : self.hostname
                }
@@ -652,9 +754,9 @@ class DDNSProviderEntryDNS(DDNSProvider):
        # here: https://entrydns.net/help
        url = "https://entrydns.net/records/modify"
 
        # here: https://entrydns.net/help
        url = "https://entrydns.net/records/modify"
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                data = {
-                       "ip" : self.get_address("ipv4")
+                       "ip" : self.get_address(proto),
                }
 
                # Add auth token to the update url.
                }
 
                # Add auth token to the update url.
@@ -691,15 +793,9 @@ class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
        # page. All used values have been collected by testing.
        url = "https://freedns.afraid.org/dynamic/update.php"
 
        # page. All used values have been collected by testing.
        url = "https://freedns.afraid.org/dynamic/update.php"
 
-       @property
-       def proto(self):
-               return self.get("proto")
-
-       def update(self):
-               address = self.get_address(self.proto)
-
+       def update_protocol(self, proto):
                data = {
                data = {
-                       "address" : address,
+                       "address" : self.get_address(proto),
                }
 
                # Add auth token to the update url.
                }
 
                # Add auth token to the update url.
@@ -768,6 +864,25 @@ class DDNSProviderLightningWireLabs(DDNSProvider):
                raise DDNSUpdateError
 
 
                raise DDNSUpdateError
 
 
+class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "myonlineportal.net"
+       name      = "myonlineportal.net"
+       website   = "https:/myonlineportal.net/"
+
+       # Information about the request and response can be obtained here:
+       # https://myonlineportal.net/howto_dyndns
+
+       url = "https://myonlineportal.net/updateddns"
+
+       def prepare_request_data(self, proto):
+               data = {
+                       "hostname" : self.hostname,
+                       "ip"     : self.get_address(proto),
+               }
+
+               return data
+
+
 class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
        handle    = "namecheap.com"
        name      = "Namecheap"
 class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
        handle    = "namecheap.com"
        name      = "Namecheap"
@@ -780,12 +895,12 @@ class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
 
        url = "https://dynamicdns.park-your-domain.com/update"
 
 
        url = "https://dynamicdns.park-your-domain.com/update"
 
-       def update(self):
+       def update_protocol(self, proto):
                # Namecheap requires the hostname splitted into a host and domain part.
                host, domain = self.hostname.split(".", 1)
 
                data = {
                # Namecheap requires the hostname splitted into a host and domain part.
                host, domain = self.hostname.split(".", 1)
 
                data = {
-                       "ip"       : self.get_address("ipv4"),
+                       "ip"       : self.get_address(proto),
                        "password" : self.password,
                        "host"     : host,
                        "domain"   : domain
                        "password" : self.password,
                        "host"     : host,
                        "domain"   : domain
@@ -798,7 +913,7 @@ class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
                output = response.read()
 
                # Handle success messages.
                output = response.read()
 
                # Handle success messages.
-               if self.get_xml_tag_value(output, "IP") == self.get_address("ipv4"):
+               if self.get_xml_tag_value(output, "IP") == address:
                        return
 
                # Handle error codes.
                        return
 
                # Handle error codes.
@@ -829,10 +944,12 @@ class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
 
        url = "http://dynupdate.no-ip.com/nic/update"
 
 
        url = "http://dynupdate.no-ip.com/nic/update"
 
-       def _prepare_request_data(self):
+       def prepare_request_data(self, proto):
+               assert proto == "ipv4"
+
                data = {
                        "hostname" : self.hostname,
                data = {
                        "hostname" : self.hostname,
-                       "address"  : self.get_address("ipv4"),
+                       "address"  : self.get_address(proto),
                }
 
                return data
                }
 
                return data
@@ -856,11 +973,7 @@ class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
 
        @property
        def password(self):
 
        @property
        def password(self):
-               return self.get("secret")
-
-       @property
-       def proto(self):
-               return self.get("proto")
+               return self.token or self.get("secret")
 
        @property
        def url(self):
 
        @property
        def url(self):
@@ -872,9 +985,9 @@ class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
                else:
                        raise DDNSUpdateError(_("Invalid protocol has been given"))
 
                else:
                        raise DDNSUpdateError(_("Invalid protocol has been given"))
 
-       def _prepare_request_data(self):
+       def prepare_request_data(self, proto):
                data = {
                data = {
-                       "myip" : self.get_address(self.proto),
+                       "myip" : self.get_address(proto),
                }
 
                return data
                }
 
                return data
@@ -891,14 +1004,10 @@ class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
 
        url = "https://updates.opendns.com/nic/update"
 
 
        url = "https://updates.opendns.com/nic/update"
 
-       @property
-       def proto(self):
-               return self.get("proto")
-
-       def _prepare_request_data(self):
+       def prepare_request_data(self, proto):
                data = {
                        "hostname" : self.hostname,
                data = {
                        "hostname" : self.hostname,
-                       "myip"     : self.get_address(self.proto)
+                       "myip"     : self.get_address(proto),
                }
 
                return data
                }
 
                return data
@@ -918,8 +1027,8 @@ class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
 
        url = "https://www.ovh.com/nic/update"
 
 
        url = "https://www.ovh.com/nic/update"
 
-       def _prepare_request_data(self):
-               data = DDNSProtocolDynDNS2._prepare_request_data(self)
+       def prepare_request_data(self, proto):
+               data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
                data.update({
                        "system" : "dyndns",
                })
                data.update({
                        "system" : "dyndns",
                })
@@ -1006,8 +1115,8 @@ class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
 
        url = "https://carol.selfhost.de/nic/update"
 
 
        url = "https://carol.selfhost.de/nic/update"
 
-       def _prepare_request_data(self):
-               data = DDNSProtocolDynDNS2._prepare_request_data(self)
+       def prepare_request_data(self, proto):
+               data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
                data.update({
                        "hostname" : "1",
                })
                data.update({
                        "hostname" : "1",
                })
@@ -1019,7 +1128,6 @@ class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
        handle    = "spdns.org"
        name      = "SPDNS"
        website   = "http://spdns.org/"
        handle    = "spdns.org"
        name      = "SPDNS"
        website   = "http://spdns.org/"
-       protocols = ("ipv4",)
 
        # Detailed information about request and response codes are provided
        # by the vendor. They are using almost the same mechanism and status
 
        # Detailed information about request and response codes are provided
        # by the vendor. They are using almost the same mechanism and status
@@ -1063,9 +1171,11 @@ class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
 
        url = "https://update.twodns.de/update"
 
 
        url = "https://update.twodns.de/update"
 
-       def _prepare_request_data(self):
+       def prepare_request_data(self, proto):
+               assert proto == "ipv4"
+
                data = {
                data = {
-                       "ip" : self.get_address("ipv4"),
+                       "ip"       : self.get_address(proto),
                        "hostname" : self.hostname
                }
 
                        "hostname" : self.hostname
                }
 
@@ -1095,14 +1205,10 @@ class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
 
        url = "https://dyndns.variomedia.de/nic/update"
 
 
        url = "https://dyndns.variomedia.de/nic/update"
 
-       @property
-       def proto(self):
-               return self.get("proto")
-
-       def _prepare_request_data(self):
+       def prepare_request_data(self, proto):
                data = {
                        "hostname" : self.hostname,
                data = {
                        "hostname" : self.hostname,
-                       "myip"     : self.get_address(self.proto)
+                       "myip"     : self.get_address(proto),
                }
 
                return data
                }
 
                return data
@@ -1121,13 +1227,9 @@ class DDNSProviderZoneedit(DDNSProtocolDynDNS2, DDNSProvider):
 
        url = "https://dynamic.zoneedit.com/auth/dynamic.html"
 
 
        url = "https://dynamic.zoneedit.com/auth/dynamic.html"
 
-       @property
-       def proto(self):
-               return self.get("proto")
-
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                data = {
-                       "dnsto" : self.get_address(self.proto),
+                       "dnsto" : self.get_address(proto),
                        "host"  : self.hostname
                }
 
                        "host"  : self.hostname
                }
 
@@ -1158,7 +1260,7 @@ class DDNSProviderZZZZ(DDNSProvider):
        handle    = "zzzz.io"
        name      = "zzzz"
        website   = "https://zzzz.io"
        handle    = "zzzz.io"
        name      = "zzzz"
        website   = "https://zzzz.io"
-       protocols = ("ipv4",)
+       protocols = ("ipv6", "ipv4",)
 
        # Detailed information about the update request can be found here:
        # https://zzzz.io/faq/
 
        # Detailed information about the update request can be found here:
        # https://zzzz.io/faq/
@@ -1168,12 +1270,15 @@ class DDNSProviderZZZZ(DDNSProvider):
 
        url = "https://zzzz.io/api/v1/update"
 
 
        url = "https://zzzz.io/api/v1/update"
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                data = {
-                       "ip"    : self.get_address("ipv4"),
+                       "ip"    : self.get_address(proto),
                        "token" : self.token,
                }
 
                        "token" : self.token,
                }
 
+               if proto == "ipv6":
+                       data["type"] = "aaaa"
+
                # zzzz uses the host from the full hostname as part
                # of the update url.
                host, domain = self.hostname.split(".", 1)
                # zzzz uses the host from the full hostname as part
                # of the update url.
                host, domain = self.hostname.split(".", 1)
@@ -1186,11 +1291,8 @@ class DDNSProviderZZZZ(DDNSProvider):
                        response = self.send_request(url, data=data)
 
                # Handle error codes.
                        response = self.send_request(url, data=data)
 
                # Handle error codes.
-               except urllib2.HTTPError, e:
-                       if e.code == 404:
-                               raise DDNSRequestError(_("Invalid hostname specified."))
-
-                       raise
+               except DDNSNotFound:
+                       raise DDNSRequestError(_("Invalid hostname specified"))
 
                # Handle success messages.
                if response.code == 200:
 
                # Handle success messages.
                if response.code == 200: