]>
git.ipfire.org Git - people/ms/ddns.git/blob - src/ddns/providers.py
2 ###############################################################################
4 # ddns - A dynamic DNS client for IPFire #
5 # Copyright (C) 2012 IPFire development team #
7 # This program is free software: you can redistribute it and/or modify #
8 # it under the terms of the GNU General Public License as published by #
9 # the Free Software Foundation, either version 3 of the License, or #
10 # (at your option) any later version. #
12 # This program is distributed in the hope that it will be useful, #
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15 # GNU General Public License for more details. #
17 # You should have received a copy of the GNU General Public License #
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. #
20 ###############################################################################
25 import xml
.dom
.minidom
29 # Import all possible exception types.
32 logger
= logging
.getLogger("ddns.providers")
39 Returns a dict with all automatically registered providers.
41 return _providers
.copy()
43 class DDNSProvider(object):
44 # A short string that uniquely identifies
48 # The full name of the provider.
51 # A weburl to the homepage of the provider.
52 # (Where to register a new account?)
55 # A list of supported protocols.
56 protocols
= ("ipv6", "ipv4")
60 # Automatically register all providers.
61 class __metaclass__(type):
62 def __init__(provider
, name
, bases
, dict):
63 type.__init
__(provider
, name
, bases
, dict)
65 # The main class from which is inherited is not registered
67 if name
== "DDNSProvider":
70 if not all((provider
.handle
, provider
.name
, provider
.website
)):
71 raise DDNSError(_("Provider is not properly configured"))
73 assert not _providers
.has_key(provider
.handle
), \
74 "Provider '%s' has already been registered" % provider
.handle
76 _providers
[provider
.handle
] = provider
78 def __init__(self
, core
, **settings
):
81 # Copy a set of default settings and
82 # update them by those from the configuration file.
83 self
.settings
= self
.DEFAULT_SETTINGS
.copy()
84 self
.settings
.update(settings
)
87 return "<DDNS Provider %s (%s)>" % (self
.name
, self
.handle
)
89 def __cmp__(self
, other
):
90 return cmp(self
.hostname
, other
.hostname
)
92 def get(self
, key
, default
=None):
94 Get a setting from the settings dictionary.
96 return self
.settings
.get(key
, default
)
101 Fast access to the hostname.
103 return self
.get("hostname")
108 Fast access to the username.
110 return self
.get("username")
115 Fast access to the password.
117 return self
.get("password")
122 Fast access to the token.
124 return self
.get("token")
126 def __call__(self
, force
=False):
128 logger
.debug(_("Updating %s forced") % self
.hostname
)
130 # Check if we actually need to update this host.
131 elif self
.is_uptodate(self
.protocols
):
132 logger
.debug(_("%s is already up to date") % self
.hostname
)
135 # Execute the update.
139 raise NotImplementedError
141 def is_uptodate(self
, protos
):
143 Returns True if this host is already up to date
144 and does not need to change the IP address on the
148 addresses
= self
.core
.system
.resolve(self
.hostname
, proto
)
150 current_address
= self
.get_address(proto
)
152 # If no addresses for the given protocol exist, we
154 if current_address
is None and not addresses
:
157 if not current_address
in addresses
:
162 def send_request(self
, *args
, **kwargs
):
164 Proxy connection to the send request
167 return self
.core
.system
.send_request(*args
, **kwargs
)
169 def get_address(self
, proto
, default
=None):
171 Proxy method to get the current IP address.
173 return self
.core
.system
.get_address(proto
) or default
176 class DDNSProtocolDynDNS2(object):
178 This is an abstract class that implements the DynDNS updater
179 protocol version 2. As this is a popular way to update dynamic
180 DNS records, this class is supposed make the provider classes
184 # Information about the format of the request is to be found
185 # http://dyn.com/support/developers/api/perform-update/
186 # http://dyn.com/support/developers/api/return-codes/
188 def _prepare_request_data(self
):
190 "hostname" : self
.hostname
,
191 "myip" : self
.get_address("ipv4"),
197 data
= self
._prepare
_request
_data
()
199 # Send update to the server.
200 response
= self
.send_request(self
.url
, data
=data
,
201 username
=self
.username
, password
=self
.password
)
203 # Get the full response message.
204 output
= response
.read()
206 # Handle success messages.
207 if output
.startswith("good") or output
.startswith("nochg"):
210 # Handle error codes.
211 if output
== "badauth":
212 raise DDNSAuthenticationError
213 elif output
== "aduse":
215 elif output
== "notfqdn":
216 raise DDNSRequestError(_("No valid FQDN was given."))
217 elif output
== "nohost":
218 raise DDNSRequestError(_("Specified host does not exist."))
219 elif output
== "911":
220 raise DDNSInternalServerError
221 elif output
== "dnserr":
222 raise DDNSInternalServerError(_("DNS error encountered."))
224 # If we got here, some other update error happened.
225 raise DDNSUpdateError(_("Server response: %s") % output
)
228 class DDNSProviderAllInkl(DDNSProvider
):
229 handle
= "all-inkl.com"
230 name
= "All-inkl.com"
231 website
= "http://all-inkl.com/"
232 protocols
= ("ipv4",)
234 # There are only information provided by the vendor how to
235 # perform an update on a FRITZ Box. Grab requried informations
237 # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
239 url
= "http://dyndns.kasserver.com"
242 # There is no additional data required so we directly can
244 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
)
246 # Get the full response message.
247 output
= response
.read()
249 # Handle success messages.
250 if output
.startswith("good") or output
.startswith("nochg"):
253 # If we got here, some other update error happened.
254 raise DDNSUpdateError
257 class DDNSProviderBindNsupdate(DDNSProvider
):
259 name
= "BIND nsupdate utility"
260 website
= "http://en.wikipedia.org/wiki/Nsupdate"
265 scriptlet
= self
.__make
_scriptlet
()
267 # -v enables TCP hence we transfer keys and other data that may
268 # exceed the size of one packet.
269 # -t sets the timeout
270 command
= ["nsupdate", "-v", "-t", "60"]
272 p
= subprocess
.Popen(command
, shell
=True,
273 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
275 stdout
, stderr
= p
.communicate(scriptlet
)
277 if p
.returncode
== 0:
280 raise DDNSError("nsupdate terminated with error code: %s\n %s" % (p
.returncode
, stderr
))
282 def __make_scriptlet(self
):
285 # Set a different server the update is sent to.
286 server
= self
.get("server", None)
288 scriptlet
.append("server %s" % server
)
290 key
= self
.get("key", None)
292 secret
= self
.get("secret")
294 scriptlet
.append("key %s %s" % (key
, secret
))
296 ttl
= self
.get("ttl", self
.DEFAULT_TTL
)
298 # Perform an update for each supported protocol.
299 for rrtype
, proto
in (("AAAA", "ipv6"), ("A", "ipv4")):
300 address
= self
.get_address(proto
)
304 scriptlet
.append("update delete %s. %s" % (self
.hostname
, rrtype
))
305 scriptlet
.append("update add %s. %s %s %s" % \
306 (self
.hostname
, ttl
, rrtype
, address
))
308 # Send the actions to the server.
309 scriptlet
.append("send")
310 scriptlet
.append("quit")
312 logger
.debug(_("Scriptlet:"))
313 for line
in scriptlet
:
314 # Masquerade the line with the secret key.
315 if line
.startswith("key"):
316 line
= "key **** ****"
318 logger
.debug(" %s" % line
)
320 return "\n".join(scriptlet
)
323 class DDNSProviderDHS(DDNSProvider
):
325 name
= "DHS International"
326 website
= "http://dhs.org/"
327 protocols
= ("ipv4",)
329 # No information about the used update api provided on webpage,
330 # grabed from source code of ez-ipudate.
332 url
= "http://members.dhs.org/nic/hosts"
336 "domain" : self
.hostname
,
337 "ip" : self
.get_address("ipv4"),
339 "hostcmdstage" : "2",
343 # Send update to the server.
344 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
347 # Handle success messages.
348 if response
.code
== 200:
351 # If we got here, some other update error happened.
352 raise DDNSUpdateError
355 class DDNSProviderDNSpark(DDNSProvider
):
356 handle
= "dnspark.com"
358 website
= "http://dnspark.com/"
359 protocols
= ("ipv4",)
361 # Informations to the used api can be found here:
362 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
364 url
= "https://control.dnspark.com/api/dynamic/update.php"
368 "domain" : self
.hostname
,
369 "ip" : self
.get_address("ipv4"),
372 # Send update to the server.
373 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
376 # Get the full response message.
377 output
= response
.read()
379 # Handle success messages.
380 if output
.startswith("ok") or output
.startswith("nochange"):
383 # Handle error codes.
384 if output
== "unauth":
385 raise DDNSAuthenticationError
386 elif output
== "abuse":
388 elif output
== "blocked":
389 raise DDNSBlockedError
390 elif output
== "nofqdn":
391 raise DDNSRequestError(_("No valid FQDN was given."))
392 elif output
== "nohost":
393 raise DDNSRequestError(_("Invalid hostname specified."))
394 elif output
== "notdyn":
395 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
396 elif output
== "invalid":
397 raise DDNSRequestError(_("Invalid IP address has been sent."))
399 # If we got here, some other update error happened.
400 raise DDNSUpdateError
403 class DDNSProviderDtDNS(DDNSProvider
):
406 website
= "http://dtdns.com/"
407 protocols
= ("ipv4",)
409 # Information about the format of the HTTPS request is to be found
410 # http://www.dtdns.com/dtsite/updatespec
412 url
= "https://www.dtdns.com/api/autodns.cfm"
416 "ip" : self
.get_address("ipv4"),
417 "id" : self
.hostname
,
421 # Send update to the server.
422 response
= self
.send_request(self
.url
, data
=data
)
424 # Get the full response message.
425 output
= response
.read()
427 # Remove all leading and trailing whitespace.
428 output
= output
.strip()
430 # Handle success messages.
431 if "now points to" in output
:
434 # Handle error codes.
435 if output
== "No hostname to update was supplied.":
436 raise DDNSRequestError(_("No hostname specified."))
438 elif output
== "The hostname you supplied is not valid.":
439 raise DDNSRequestError(_("Invalid hostname specified."))
441 elif output
== "The password you supplied is not valid.":
442 raise DDNSAuthenticationError
444 elif output
== "Administration has disabled this account.":
445 raise DDNSRequestError(_("Account has been disabled."))
447 elif output
== "Illegal character in IP.":
448 raise DDNSRequestError(_("Invalid IP address has been sent."))
450 elif output
== "Too many failed requests.":
451 raise DDNSRequestError(_("Too many failed requests."))
453 # If we got here, some other update error happened.
454 raise DDNSUpdateError
457 class DDNSProviderDynDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
458 handle
= "dyndns.org"
460 website
= "http://dyn.com/dns/"
461 protocols
= ("ipv4",)
463 # Information about the format of the request is to be found
464 # http://http://dyn.com/support/developers/api/perform-update/
465 # http://dyn.com/support/developers/api/return-codes/
467 url
= "https://members.dyndns.org/nic/update"
470 class DDNSProviderDynU(DDNSProtocolDynDNS2
, DDNSProvider
):
473 website
= "http://dynu.com/"
474 protocols
= ("ipv6", "ipv4",)
476 # Detailed information about the request and response codes
477 # are available on the providers webpage.
478 # http://dynu.com/Default.aspx?page=dnsapi
480 url
= "https://api.dynu.com/nic/update"
482 def _prepare_request_data(self
):
483 data
= DDNSProviderDynDNS
._prepare
_request
_data
(self
)
485 # This one supports IPv6
487 "myipv6" : self
.get_address("ipv6"),
493 class DDNSProviderEasyDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
494 handle
= "easydns.com"
496 website
= "http://www.easydns.com/"
497 protocols
= ("ipv4",)
499 # There is only some basic documentation provided by the vendor,
500 # also searching the web gain very poor results.
501 # http://mediawiki.easydns.com/index.php/Dynamic_DNS
503 url
= "http://api.cp.easydns.com/dyn/tomato.php"
506 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider
):
507 handle
= "freedns.afraid.org"
508 name
= "freedns.afraid.org"
509 website
= "http://freedns.afraid.org/"
511 # No information about the request or response could be found on the vendor
512 # page. All used values have been collected by testing.
513 url
= "https://freedns.afraid.org/dynamic/update.php"
517 return self
.get("proto")
520 address
= self
.get_address(self
.proto
)
526 # Add auth token to the update url.
527 url
= "%s?%s" % (self
.url
, self
.token
)
529 # Send update to the server.
530 response
= self
.send_request(url
, data
=data
)
532 if output
.startswith("Updated") or "has not changed" in output
:
535 # Handle error codes.
536 if output
== "ERROR: Unable to locate this record":
537 raise DDNSAuthenticationError
538 elif "is an invalid IP address" in output
:
539 raise DDNSRequestError(_("Invalid IP address has been sent."))
541 # If we got here, some other update error happened.
542 raise DDNSUpdateError
545 class DDNSProviderLightningWireLabs(DDNSProvider
):
546 handle
= "dns.lightningwirelabs.com"
547 name
= "Lightning Wire Labs DNS Service"
548 website
= "http://dns.lightningwirelabs.com/"
550 # Information about the format of the HTTPS request is to be found
551 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
553 url
= "https://dns.lightningwirelabs.com/update"
557 "hostname" : self
.hostname
,
558 "address6" : self
.get_address("ipv6", "-"),
559 "address4" : self
.get_address("ipv4", "-"),
562 # Check if a token has been set.
564 data
["token"] = self
.token
566 # Check for username and password.
567 elif self
.username
and self
.password
:
569 "username" : self
.username
,
570 "password" : self
.password
,
573 # Raise an error if no auth details are given.
575 raise DDNSConfigurationError
577 # Send update to the server.
578 response
= self
.send_request(self
.url
, data
=data
)
580 # Handle success messages.
581 if response
.code
== 200:
584 # If we got here, some other update error happened.
585 raise DDNSUpdateError
588 class DDNSProviderNamecheap(DDNSProvider
):
589 handle
= "namecheap.com"
591 website
= "http://namecheap.com"
592 protocols
= ("ipv4",)
594 # Information about the format of the HTTP request is to be found
595 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
596 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
598 url
= "https://dynamicdns.park-your-domain.com/update"
600 def parse_xml(self
, document
, content
):
601 # Send input to the parser.
602 xmldoc
= xml
.dom
.minidom
.parseString(document
)
604 # Get XML elements by the given content.
605 element
= xmldoc
.getElementsByTagName(content
)
607 # If no element has been found, we directly can return None.
611 # Only get the first child from an element, even there are more than one.
612 firstchild
= element
[0].firstChild
614 # Get the value of the child.
615 value
= firstchild
.nodeValue
621 # Namecheap requires the hostname splitted into a host and domain part.
622 host
, domain
= self
.hostname
.split(".", 1)
625 "ip" : self
.get_address("ipv4"),
626 "password" : self
.password
,
631 # Send update to the server.
632 response
= self
.send_request(self
.url
, data
=data
)
634 # Get the full response message.
635 output
= response
.read()
637 # Handle success messages.
638 if self
.parse_xml(output
, "IP") == self
.get_address("ipv4"):
641 # Handle error codes.
642 errorcode
= self
.parse_xml(output
, "ResponseNumber")
644 if errorcode
== "304156":
645 raise DDNSAuthenticationError
646 elif errorcode
== "316153":
647 raise DDNSRequestError(_("Domain not found."))
648 elif errorcode
== "316154":
649 raise DDNSRequestError(_("Domain not active."))
650 elif errorcode
in ("380098", "380099"):
651 raise DDNSInternalServerError
653 # If we got here, some other update error happened.
654 raise DDNSUpdateError
657 class DDNSProviderNOIP(DDNSProtocolDynDNS2
, DDNSProvider
):
660 website
= "http://www.no-ip.com/"
661 protocols
= ("ipv4",)
663 # Information about the format of the HTTP request is to be found
664 # here: http://www.no-ip.com/integrate/request and
665 # here: http://www.no-ip.com/integrate/response
667 url
= "http://dynupdate.no-ip.com/nic/update"
669 def _prepare_request_data(self
):
671 "hostname" : self
.hostname
,
672 "address" : self
.get_address("ipv4"),
678 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2
, DDNSProvider
):
679 handle
= "nsupdate.info"
680 name
= "nsupdate.info"
681 website
= "http://www.nsupdate.info/"
682 protocols
= ("ipv6", "ipv4",)
684 # Information about the format of the HTTP request can be found
685 # after login on the provider user intrface and here:
686 # http://nsupdateinfo.readthedocs.org/en/latest/user.html
688 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
689 # and for the password a so called secret.
692 return self
.get("hostname")
696 return self
.get("secret")
700 return self
.get("proto")
704 # The update URL is different by the used protocol.
705 if self
.proto
== "ipv4":
706 return "https://ipv4.nsupdate.info/nic/update"
707 elif self
.proto
== "ipv6":
708 return "https://ipv6.nsupdate.info/nic/update"
710 raise DDNSUpdateError(_("Invalid protocol has been given"))
712 def _prepare_request_data(self
):
714 "myip" : self
.get_address(self
.proto
),
720 class DDNSProviderOVH(DDNSProtocolDynDNS2
, DDNSProvider
):
723 website
= "http://www.ovh.com/"
724 protocols
= ("ipv4",)
726 # OVH only provides very limited information about how to
727 # update a DynDNS host. They only provide the update url
728 # on the their german subpage.
730 # http://hilfe.ovh.de/DomainDynHost
732 url
= "https://www.ovh.com/nic/update"
734 def _prepare_request_data(self
):
735 data
= DDNSProtocolDynDNS2
._prepare_request_data(self
)
743 class DDNSProviderRegfish(DDNSProvider
):
744 handle
= "regfish.com"
745 name
= "Regfish GmbH"
746 website
= "http://www.regfish.com/"
748 # A full documentation to the providers api can be found here
749 # but is only available in german.
750 # https://www.regfish.de/domains/dyndns/dokumentation
752 url
= "https://dyndns.regfish.de/"
756 "fqdn" : self
.hostname
,
759 # Check if we update an IPv6 address.
760 address6
= self
.get_address("ipv6")
762 data
["ipv6"] = address6
764 # Check if we update an IPv4 address.
765 address4
= self
.get_address("ipv4")
767 data
["ipv4"] = address4
769 # Raise an error if none address is given.
770 if not data
.has_key("ipv6") and not data
.has_key("ipv4"):
771 raise DDNSConfigurationError
773 # Check if a token has been set.
775 data
["token"] = self
.token
777 # Raise an error if no token and no useranem and password
779 elif not self
.username
and not self
.password
:
780 raise DDNSConfigurationError(_("No Auth details specified."))
782 # HTTP Basic Auth is only allowed if no token is used.
784 # Send update to the server.
785 response
= self
.send_request(self
.url
, data
=data
)
787 # Send update to the server.
788 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
791 # Get the full response message.
792 output
= response
.read()
794 # Handle success messages.
795 if "100" in output
or "101" in output
:
798 # Handle error codes.
799 if "401" or "402" in output
:
800 raise DDNSAuthenticationError
801 elif "408" in output
:
802 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
803 elif "409" in output
:
804 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
805 elif "412" in output
:
806 raise DDNSRequestError(_("No valid FQDN was given."))
807 elif "414" in output
:
808 raise DDNSInternalServerError
810 # If we got here, some other update error happened.
811 raise DDNSUpdateError
814 class DDNSProviderSelfhost(DDNSProtocolDynDNS2
, DDNSProvider
):
815 handle
= "selfhost.de"
817 website
= "http://www.selfhost.de/"
818 protocols
= ("ipv4",)
820 url
= "https://carol.selfhost.de/nic/update"
822 def _prepare_request_data(self
):
823 data
= DDNSProviderDynDNS
._prepare
_request
_data
(self
)
831 class DDNSProviderSPDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
834 website
= "http://spdns.org/"
835 protocols
= ("ipv4",)
837 # Detailed information about request and response codes are provided
838 # by the vendor. They are using almost the same mechanism and status
839 # codes as dyndns.org so we can inherit all those stuff.
841 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
842 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
844 url
= "https://update.spdns.de/nic/update"
847 class DDNSProviderStrato(DDNSProtocolDynDNS2
, DDNSProvider
):
848 handle
= "strato.com"
850 website
= "http:/www.strato.com/"
851 protocols
= ("ipv4",)
853 # Information about the request and response can be obtained here:
854 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
856 url
= "https://dyndns.strato.com/nic/update"
859 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
862 website
= "http://www.twodns.de"
863 protocols
= ("ipv4",)
865 # Detailed information about the request can be found here
866 # http://twodns.de/en/faqs
867 # http://twodns.de/en/api
869 url
= "https://update.twodns.de/update"
871 def _prepare_request_data(self
):
873 "ip" : self
.get_address("ipv4"),
874 "hostname" : self
.hostname
880 class DDNSProviderUdmedia(DDNSProtocolDynDNS2
, DDNSProvider
):
881 handle
= "udmedia.de"
882 name
= "Udmedia GmbH"
883 website
= "http://www.udmedia.de"
884 protocols
= ("ipv4",)
886 # Information about the request can be found here
887 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
889 url
= "https://www.udmedia.de/nic/update"
892 class DDNSProviderVariomedia(DDNSProtocolDynDNS2
, DDNSProvider
):
893 handle
= "variomedia.de"
895 website
= "http://www.variomedia.de/"
896 protocols
= ("ipv6", "ipv4",)
898 # Detailed information about the request can be found here
899 # https://dyndns.variomedia.de/
901 url
= "https://dyndns.variomedia.de/nic/update"
905 return self
.get("proto")
907 def _prepare_request_data(self
):
909 "hostname" : self
.hostname
,
910 "myip" : self
.get_address(self
.proto
)
916 class DDNSProviderZoneedit(DDNSProtocolDynDNS2
, DDNSProvider
):
917 handle
= "zoneedit.com"
919 website
= "http://www.zoneedit.com"
920 protocols
= ("ipv4",)
922 # Detailed information about the request and the response codes can be
924 # http://www.zoneedit.com/doc/api/other.html
925 # http://www.zoneedit.com/faq.html
927 url
= "https://dynamic.zoneedit.com/auth/dynamic.html"
931 return self
.get("proto")
935 "dnsto" : self
.get_address(self
.proto
),
936 "host" : self
.hostname
939 # Send update to the server.
940 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
943 # Get the full response message.
944 output
= response
.read()
946 # Handle success messages.
947 if output
.startswith("<SUCCESS"):
950 # Handle error codes.
951 if output
.startswith("invalid login"):
952 raise DDNSAuthenticationError
953 elif output
.startswith("<ERROR CODE=\"704\""):
954 raise DDNSRequestError(_("No valid FQDN was given."))
955 elif output
.startswith("<ERROR CODE=\"702\""):
956 raise DDNSInternalServerError
958 # If we got here, some other update error happened.
959 raise DDNSUpdateError