]> git.ipfire.org Git - ddns.git/blobdiff - src/ddns/providers.py
DynU: Fix typo in function call
[ddns.git] / src / ddns / providers.py
index de87b7c9bf32725da548b69d8ea592a5d044f097..0bd21b83939c11d75af83c35f993537275b43e0b 100644 (file)
@@ -19,7 +19,9 @@
 #                                                                             #
 ###############################################################################
 
+import datetime
 import logging
+import os
 import subprocess
 import urllib2
 import xml.dom.minidom
@@ -57,6 +59,14 @@ class DDNSProvider(object):
 
        DEFAULT_SETTINGS = {}
 
+       # holdoff time - Number of days no update is performed unless
+       # the IP address has changed.
+       holdoff_days = 30
+
+       # True if the provider is able to remove records, too.
+       # Required to remove AAAA records if IPv6 is absent again.
+       can_remove_records = True
+
        # Automatically register all providers.
        class __metaclass__(type):
                def __init__(provider, name, bases, dict):
@@ -75,6 +85,14 @@ class DDNSProvider(object):
 
                        _providers[provider.handle] = provider
 
+       @staticmethod
+       def supported():
+               """
+                       Should be overwritten to check if the system the code is running
+                       on has all the required tools to support this provider.
+               """
+               return True
+
        def __init__(self, core, **settings):
                self.core = core
 
@@ -89,6 +107,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,18 +149,65 @@ 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(_("%s is already up to date") % self.hostname)
+               # 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:
+                       if self.have_address(protocol):
+                               self.update_protocol(protocol)
+                       elif self.can_remove_records:
+                               self.remove_protocol(protocol)
+
+       def update_protocol(self, proto):
                raise NotImplementedError
 
-       def is_uptodate(self, protos):
+       def remove_protocol(self, proto):
+               if not self.can_remove_records:
+                       raise RuntimeError, "can_remove_records is enabled, but remove_protocol() not implemented"
+
+               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
@@ -146,18 +215,53 @@ class DDNSProvider(object):
                """
                for proto in protos:
                        addresses = self.core.system.resolve(self.hostname, proto)
-
                        current_address = self.get_address(proto)
 
-                       # If no addresses for the given protocol exist, we
-                       # are fine...
-                       if current_address is None and not addresses:
+                       # Handle if the system has not got any IP address from a protocol
+                       # (i.e. had full dual-stack connectivity which it has not any more)
+                       if current_address is None:
+                               # If addresses still exists in the DNS system and if this provider
+                               # is able to remove records, we will do that.
+                               if addresses and self.can_remove_records:
+                                       return True
+
+                               # Otherwise, we cannot go on...
                                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):
                """
@@ -172,6 +276,18 @@ class DDNSProvider(object):
                """
                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):
        """
@@ -185,19 +301,25 @@ class DDNSProtocolDynDNS2(object):
        # http://dyn.com/support/developers/api/perform-update/
        # http://dyn.com/support/developers/api/return-codes/
 
-       def _prepare_request_data(self):
+       # The DynDNS protocol version 2 does not allow to remove records
+       can_remove_records = False
+
+       def prepare_request_data(self, proto):
                data = {
                        "hostname" : self.hostname,
-                       "myip"     : self.get_address("ipv4"),
+                       "myip"     : self.get_address(proto),
                }
 
                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.
-               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.
@@ -210,7 +332,7 @@ class DDNSProtocolDynDNS2(object):
                # Handle error codes.
                if output == "badauth":
                        raise DDNSAuthenticationError
-               elif output == "aduse":
+               elif output == "abuse":
                        raise DDNSAbuseError
                elif output == "notfqdn":
                        raise DDNSRequestError(_("No valid FQDN was given."))
@@ -220,6 +342,8 @@ class DDNSProtocolDynDNS2(object):
                        raise DDNSInternalServerError
                elif output == "dnserr":
                        raise DDNSInternalServerError(_("DNS error encountered."))
+               elif output == "badagent":
+                       raise DDNSBlockedError
 
                # If we got here, some other update error happened.
                raise DDNSUpdateError(_("Server response: %s") % output)
@@ -266,6 +390,7 @@ class DDNSProviderAllInkl(DDNSProvider):
        # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
 
        url = "http://dyndns.kasserver.com"
+       can_remove_records = False
 
        def update(self):
                # There is no additional data required so we directly can
@@ -290,6 +415,19 @@ class DDNSProviderBindNsupdate(DDNSProvider):
 
        DEFAULT_TTL = 60
 
+       @staticmethod
+       def supported():
+               # Search if the nsupdate utility is available
+               paths = os.environ.get("PATH")
+
+               for path in paths.split(":"):
+                       executable = os.path.join(path, "nsupdate")
+
+                       if os.path.exists(executable):
+                               return True
+
+               return False
+
        def update(self):
                scriptlet = self.__make_scriptlet()
 
@@ -316,6 +454,11 @@ class DDNSProviderBindNsupdate(DDNSProvider):
                if server:
                        scriptlet.append("server %s" % server)
 
+               # Set the DNS zone the host should be added to.
+               zone = self.get("zone", None)
+               if zone:
+                       scriptlet.append("zone %s" % zone)
+
                key = self.get("key", None)
                if key:
                        secret = self.get("secret")
@@ -359,11 +502,12 @@ class DDNSProviderDHS(DDNSProvider):
        # grabed from source code of ez-ipudate.
 
        url = "http://members.dhs.org/nic/hosts"
+       can_remove_records = False
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                        "domain"       : self.hostname,
-                       "ip"           : self.get_address("ipv4"),
+                       "ip"           : self.get_address(proto),
                        "hostcmd"      : "edit",
                        "hostcmdstage" : "2",
                        "type"         : "4",
@@ -391,11 +535,12 @@ class DDNSProviderDNSpark(DDNSProvider):
        # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
 
        url = "https://control.dnspark.com/api/dynamic/update.php"
+       can_remove_records = False
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                        "domain" : self.hostname,
-                       "ip"     : self.get_address("ipv4"),
+                       "ip"     : self.get_address(proto),
                }
 
                # Send update to the server.
@@ -439,10 +584,11 @@ class DDNSProviderDtDNS(DDNSProvider):
        # http://www.dtdns.com/dtsite/updatespec
 
        url = "https://www.dtdns.com/api/autodns.cfm"
+       can_remove_records = False
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
-                       "ip" : self.get_address("ipv4"),
+                       "ip" : self.get_address(proto),
                        "id" : self.hostname,
                        "pw" : self.password
                }
@@ -508,15 +654,19 @@ class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
 
        url = "https://api.dynu.com/nic/update"
 
-       def _prepare_request_data(self):
-               data = DDNSProviderDynDNS._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
-               data.update({
-                       "myipv6"   : self.get_address("ipv6"),
-               })
+               myipv6 = self.get_address("ipv6")
 
-               return data
+               # Add update information if we have an IPv6 address.
+               if myipv6:
+                       data["myipv6"] = myipv6
+
+               self.send_request(data)
 
 
 class DDNSProviderEasyDNS(DDNSProtocolDynDNS2, DDNSProvider):
@@ -532,22 +682,80 @@ class DDNSProviderEasyDNS(DDNSProtocolDynDNS2, DDNSProvider):
        url = "http://api.cp.easydns.com/dyn/tomato.php"
 
 
+class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "domopoli.de"
+       name      = "domopoli.de"
+       website   = "http://domopoli.de/"
+       protocols = ("ipv4",)
+
+       # https://www.domopoli.de/?page=howto#DynDns_start
+
+       url = "http://dyndns.domopoli.de/nic/update"
+
+
+class DDNSProviderDynsNet(DDNSProvider):
+       handle    = "dyns.net"
+       name      = "DyNS"
+       website   = "http://www.dyns.net/"
+       protocols = ("ipv4",)
+       can_remove_records = False
+
+       # There is very detailed informatio about how to send the update request and
+       # the possible response codes. (Currently we are using the v1.1 proto)
+       # http://www.dyns.net/documentation/technical/protocol/
+
+       url = "http://www.dyns.net/postscript011.php"
+
+       def update_protocol(self, proto):
+               data = {
+                       "ip"       : self.get_address(proto),
+                       "host"     : self.hostname,
+                       "username" : self.username,
+                       "password" : self.password,
+               }
+
+               # Send update to the server.
+               response = self.send_request(self.url, data=data)
+
+               # Get the full response message.
+               output = response.read()
+
+               # Handle success messages.
+               if output.startswith("200"):
+                       return
+
+               # Handle error codes.
+               if output.startswith("400"):
+                       raise DDNSRequestError(_("Malformed request has been sent."))
+               elif output.startswith("401"):
+                       raise DDNSAuthenticationError
+               elif output.startswith("402"):
+                       raise DDNSRequestError(_("Too frequent update requests have been sent."))
+               elif output.startswith("403"):
+                       raise DDNSInternalServerError
+
+               # If we got here, some other update error happened.
+               raise DDNSUpdateError(_("Server response: %s") % output) 
+
+
 class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
        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.
        # http://www.enom.com/APICommandCatalog/
 
        url = "https://dynamic.name-services.com/interface.asp"
+       can_remove_records = False
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                        "command"        : "setdnshost",
                        "responsetype"   : "xml",
-                       "address"        : self.get_address("ipv4"),
+                       "address"        : self.get_address(proto),
                        "domainpassword" : self.password,
                        "zone"           : self.hostname
                }
@@ -583,10 +791,11 @@ class DDNSProviderEntryDNS(DDNSProvider):
        # Some very tiny details about their so called "Simple API" can be found
        # here: https://entrydns.net/help
        url = "https://entrydns.net/records/modify"
+       can_remove_records = False
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
-                       "ip" : self.get_address("ipv4")
+                       "ip" : self.get_address(proto),
                }
 
                # Add auth token to the update url.
@@ -594,7 +803,7 @@ class DDNSProviderEntryDNS(DDNSProvider):
 
                # Send update to the server.
                try:
-                       response = self.send_request(url, method="PUT", data=data)
+                       response = self.send_request(url, data=data)
 
                # Handle error codes
                except urllib2.HTTPError, e:
@@ -622,16 +831,11 @@ class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
        # No information about the request or response could be found on the vendor
        # page. All used values have been collected by testing.
        url = "https://freedns.afraid.org/dynamic/update.php"
+       can_remove_records = False
 
-       @property
-       def proto(self):
-               return self.get("proto")
-
-       def update(self):
-               address = self.get_address(self.proto)
-
+       def update_protocol(self, proto):
                data = {
-                       "address" : address,
+                       "address" : self.get_address(proto),
                }
 
                # Add auth token to the update url.
@@ -700,6 +904,25 @@ class DDNSProviderLightningWireLabs(DDNSProvider):
                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"
@@ -711,13 +934,14 @@ class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
        # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
 
        url = "https://dynamicdns.park-your-domain.com/update"
+       can_remove_records = False
 
-       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 = {
-                       "ip"       : self.get_address("ipv4"),
+                       "ip"       : self.get_address(proto),
                        "password" : self.password,
                        "host"     : host,
                        "domain"   : domain
@@ -730,7 +954,7 @@ class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
                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.
@@ -761,10 +985,12 @@ class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
 
        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,
-                       "address"  : self.get_address("ipv4"),
+                       "address"  : self.get_address(proto),
                }
 
                return data
@@ -773,13 +999,17 @@ class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
        handle    = "nsupdate.info"
        name      = "nsupdate.info"
-       website   = "http://www.nsupdate.info/"
+       website   = "http://nsupdate.info/"
        protocols = ("ipv6", "ipv4",)
 
        # Information about the format of the HTTP request can be found
-       # after login on the provider user intrface and here:
+       # after login on the provider user interface and here:
        # http://nsupdateinfo.readthedocs.org/en/latest/user.html
 
+       # TODO nsupdate.info can actually do this, but the functionality
+       # has not been implemented here, yet.
+       can_remove_records = False
+
        # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
        # and for the password a so called secret.
        @property
@@ -788,11 +1018,7 @@ class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
 
        @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):
@@ -804,9 +1030,9 @@ class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
                else:
                        raise DDNSUpdateError(_("Invalid protocol has been given"))
 
-       def _prepare_request_data(self):
+       def prepare_request_data(self, proto):
                data = {
-                       "myip" : self.get_address(self.proto),
+                       "myip" : self.get_address(proto),
                }
 
                return data
@@ -823,14 +1049,10 @@ class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
 
        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,
-                       "myip"     : self.get_address(self.proto)
+                       "myip"     : self.get_address(proto),
                }
 
                return data
@@ -850,8 +1072,8 @@ class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
 
        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",
                })
@@ -869,6 +1091,7 @@ class DDNSProviderRegfish(DDNSProvider):
        # https://www.regfish.de/domains/dyndns/dokumentation
 
        url = "https://dyndns.regfish.de/"
+       can_remove_records = False
 
        def update(self):
                data = {
@@ -938,8 +1161,8 @@ class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
 
        url = "https://carol.selfhost.de/nic/update"
 
-       def _prepare_request_data(self):
-               data = DDNSProviderDynDNS._prepare_request_data(self)
+       def prepare_request_data(self, proto):
+               data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
                data.update({
                        "hostname" : "1",
                })
@@ -951,7 +1174,6 @@ class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
        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
@@ -962,6 +1184,14 @@ class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
 
        url = "https://update.spdns.de/nic/update"
 
+       @property
+       def username(self):
+               return self.get("username") or self.hostname
+
+       @property
+       def password(self):
+               return self.get("username") or self.token
+
 
 class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
        handle    = "strato.com"
@@ -987,9 +1217,11 @@ class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
 
        url = "https://update.twodns.de/update"
 
-       def _prepare_request_data(self):
+       def prepare_request_data(self, proto):
+               assert proto == "ipv4"
+
                data = {
-                       "ip" : self.get_address("ipv4"),
+                       "ip"       : self.get_address(proto),
                        "hostname" : self.hostname
                }
 
@@ -1019,14 +1251,10 @@ class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
 
        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,
-                       "myip"     : self.get_address(self.proto)
+                       "myip"     : self.get_address(proto),
                }
 
                return data
@@ -1045,13 +1273,9 @@ class DDNSProviderZoneedit(DDNSProtocolDynDNS2, DDNSProvider):
 
        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 = {
-                       "dnsto" : self.get_address(self.proto),
+                       "dnsto" : self.get_address(proto),
                        "host"  : self.hostname
                }
 
@@ -1076,3 +1300,50 @@ class DDNSProviderZoneedit(DDNSProtocolDynDNS2, DDNSProvider):
 
                # If we got here, some other update error happened.
                raise DDNSUpdateError
+
+
+class DDNSProviderZZZZ(DDNSProvider):
+       handle    = "zzzz.io"
+       name      = "zzzz"
+       website   = "https://zzzz.io"
+       protocols = ("ipv6", "ipv4",)
+
+       # Detailed information about the update request can be found here:
+       # https://zzzz.io/faq/
+
+       # Details about the possible response codes have been provided in the bugtracker:
+       # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
+
+       url = "https://zzzz.io/api/v1/update"
+       can_remove_records = False
+
+       def update_protocol(self, proto):
+               data = {
+                       "ip"    : self.get_address(proto),
+                       "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)
+
+               # Add host value to the update url.
+               url = "%s/%s" % (self.url, host)
+
+               # Send update to the server.
+               try:
+                       response = self.send_request(url, data=data)
+
+               # Handle error codes.
+               except DDNSNotFound:
+                       raise DDNSRequestError(_("Invalid hostname specified"))
+
+               # Handle success messages.
+               if response.code == 200:
+                       return
+
+               # If we got here, some other update error happened.
+               raise DDNSUpdateError