]> git.ipfire.org Git - oddments/ddns.git/blob - src/ddns/providers.py
Automatically register all provider classes.
[oddments/ddns.git] / src / ddns / providers.py
1 #!/usr/bin/python
2 ###############################################################################
3 # #
4 # ddns - A dynamic DNS client for IPFire #
5 # Copyright (C) 2012 IPFire development team #
6 # #
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. #
11 # #
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. #
16 # #
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/>. #
19 # #
20 ###############################################################################
21
22 import logging
23 import urllib2
24 import xml.dom.minidom
25
26 from i18n import _
27
28 # Import all possible exception types.
29 from .errors import *
30
31 logger = logging.getLogger("ddns.providers")
32 logger.propagate = 1
33
34 _providers = {}
35
36 def get():
37 """
38 Returns a dict with all automatically registered providers.
39 """
40 return _providers.copy()
41
42 class DDNSProvider(object):
43 # A short string that uniquely identifies
44 # this provider.
45 handle = None
46
47 # The full name of the provider.
48 name = None
49
50 # A weburl to the homepage of the provider.
51 # (Where to register a new account?)
52 website = None
53
54 # A list of supported protocols.
55 protocols = ("ipv6", "ipv4")
56
57 DEFAULT_SETTINGS = {}
58
59 # Automatically register all providers.
60 class __metaclass__(type):
61 def __init__(provider, name, bases, dict):
62 type.__init__(provider, name, bases, dict)
63
64 # The main class from which is inherited is not registered
65 # as a provider.
66 if name == "DDNSProvider":
67 return
68
69 if not all((provider.handle, provider.name, provider.website)):
70 raise DDNSError(_("Provider is not properly configured"))
71
72 assert not _providers.has_key(provider.handle), \
73 "Provider '%s' has already been registered" % provider.handle
74
75 _providers[provider.handle] = provider
76
77 def __init__(self, core, **settings):
78 self.core = core
79
80 # Copy a set of default settings and
81 # update them by those from the configuration file.
82 self.settings = self.DEFAULT_SETTINGS.copy()
83 self.settings.update(settings)
84
85 def __repr__(self):
86 return "<DDNS Provider %s (%s)>" % (self.name, self.handle)
87
88 def __cmp__(self, other):
89 return cmp(self.hostname, other.hostname)
90
91 def get(self, key, default=None):
92 """
93 Get a setting from the settings dictionary.
94 """
95 return self.settings.get(key, default)
96
97 @property
98 def hostname(self):
99 """
100 Fast access to the hostname.
101 """
102 return self.get("hostname")
103
104 @property
105 def username(self):
106 """
107 Fast access to the username.
108 """
109 return self.get("username")
110
111 @property
112 def password(self):
113 """
114 Fast access to the password.
115 """
116 return self.get("password")
117
118 @property
119 def token(self):
120 """
121 Fast access to the token.
122 """
123 return self.get("token")
124
125 def __call__(self, force=False):
126 if force:
127 logger.debug(_("Updating %s forced") % self.hostname)
128
129 # Check if we actually need to update this host.
130 elif self.is_uptodate(self.protocols):
131 logger.debug(_("%s is already up to date") % self.hostname)
132 return
133
134 # Execute the update.
135 self.update()
136
137 def update(self):
138 raise NotImplementedError
139
140 def is_uptodate(self, protos):
141 """
142 Returns True if this host is already up to date
143 and does not need to change the IP address on the
144 name server.
145 """
146 for proto in protos:
147 addresses = self.core.system.resolve(self.hostname, proto)
148
149 current_address = self.get_address(proto)
150
151 # If no addresses for the given protocol exist, we
152 # are fine...
153 if current_address is None and not addresses:
154 continue
155
156 if not current_address in addresses:
157 return False
158
159 return True
160
161 def send_request(self, *args, **kwargs):
162 """
163 Proxy connection to the send request
164 method.
165 """
166 return self.core.system.send_request(*args, **kwargs)
167
168 def get_address(self, proto, default=None):
169 """
170 Proxy method to get the current IP address.
171 """
172 return self.core.system.get_address(proto) or default
173
174
175 class DDNSProtocolDynDNS2(object):
176 """
177 This is an abstract class that implements the DynDNS updater
178 protocol version 2. As this is a popular way to update dynamic
179 DNS records, this class is supposed make the provider classes
180 shorter and simpler.
181 """
182
183 # Information about the format of the request is to be found
184 # http://dyn.com/support/developers/api/perform-update/
185 # http://dyn.com/support/developers/api/return-codes/
186
187 def _prepare_request_data(self):
188 data = {
189 "hostname" : self.hostname,
190 "myip" : self.get_address("ipv4"),
191 }
192
193 return data
194
195 def update(self):
196 data = self._prepare_request_data()
197
198 # Send update to the server.
199 response = self.send_request(self.url, data=data,
200 username=self.username, password=self.password)
201
202 # Get the full response message.
203 output = response.read()
204
205 # Handle success messages.
206 if output.startswith("good") or output.startswith("nochg"):
207 return
208
209 # Handle error codes.
210 if output == "badauth":
211 raise DDNSAuthenticationError
212 elif output == "aduse":
213 raise DDNSAbuseError
214 elif output == "notfqdn":
215 raise DDNSRequestError(_("No valid FQDN was given."))
216 elif output == "nohost":
217 raise DDNSRequestError(_("Specified host does not exist."))
218 elif output == "911":
219 raise DDNSInternalServerError
220 elif output == "dnserr":
221 raise DDNSInternalServerError(_("DNS error encountered."))
222
223 # If we got here, some other update error happened.
224 raise DDNSUpdateError(_("Server response: %s") % output)
225
226
227 class DDNSProviderAllInkl(DDNSProvider):
228 handle = "all-inkl.com"
229 name = "All-inkl.com"
230 website = "http://all-inkl.com/"
231 protocols = ("ipv4",)
232
233 # There are only information provided by the vendor how to
234 # perform an update on a FRITZ Box. Grab requried informations
235 # from the net.
236 # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
237
238 url = "http://dyndns.kasserver.com"
239
240 def update(self):
241 # There is no additional data required so we directly can
242 # send our request.
243 response = self.send_request(self.url, username=self.username, password=self.password)
244
245 # Get the full response message.
246 output = response.read()
247
248 # Handle success messages.
249 if output.startswith("good") or output.startswith("nochg"):
250 return
251
252 # If we got here, some other update error happened.
253 raise DDNSUpdateError
254
255
256 class DDNSProviderDHS(DDNSProvider):
257 handle = "dhs.org"
258 name = "DHS International"
259 website = "http://dhs.org/"
260 protocols = ("ipv4",)
261
262 # No information about the used update api provided on webpage,
263 # grabed from source code of ez-ipudate.
264
265 url = "http://members.dhs.org/nic/hosts"
266
267 def update(self):
268 data = {
269 "domain" : self.hostname,
270 "ip" : self.get_address("ipv4"),
271 "hostcmd" : "edit",
272 "hostcmdstage" : "2",
273 "type" : "4",
274 }
275
276 # Send update to the server.
277 response = self.send_request(self.url, username=self.username, password=self.password,
278 data=data)
279
280 # Handle success messages.
281 if response.code == 200:
282 return
283
284 # If we got here, some other update error happened.
285 raise DDNSUpdateError
286
287
288 class DDNSProviderDNSpark(DDNSProvider):
289 handle = "dnspark.com"
290 name = "DNS Park"
291 website = "http://dnspark.com/"
292 protocols = ("ipv4",)
293
294 # Informations to the used api can be found here:
295 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
296
297 url = "https://control.dnspark.com/api/dynamic/update.php"
298
299 def update(self):
300 data = {
301 "domain" : self.hostname,
302 "ip" : self.get_address("ipv4"),
303 }
304
305 # Send update to the server.
306 response = self.send_request(self.url, username=self.username, password=self.password,
307 data=data)
308
309 # Get the full response message.
310 output = response.read()
311
312 # Handle success messages.
313 if output.startswith("ok") or output.startswith("nochange"):
314 return
315
316 # Handle error codes.
317 if output == "unauth":
318 raise DDNSAuthenticationError
319 elif output == "abuse":
320 raise DDNSAbuseError
321 elif output == "blocked":
322 raise DDNSBlockedError
323 elif output == "nofqdn":
324 raise DDNSRequestError(_("No valid FQDN was given."))
325 elif output == "nohost":
326 raise DDNSRequestError(_("Invalid hostname specified."))
327 elif output == "notdyn":
328 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
329 elif output == "invalid":
330 raise DDNSRequestError(_("Invalid IP address has been sent."))
331
332 # If we got here, some other update error happened.
333 raise DDNSUpdateError
334
335
336 class DDNSProviderDtDNS(DDNSProvider):
337 handle = "dtdns.com"
338 name = "DtDNS"
339 website = "http://dtdns.com/"
340 protocols = ("ipv4",)
341
342 # Information about the format of the HTTPS request is to be found
343 # http://www.dtdns.com/dtsite/updatespec
344
345 url = "https://www.dtdns.com/api/autodns.cfm"
346
347 def update(self):
348 data = {
349 "ip" : self.get_address("ipv4"),
350 "id" : self.hostname,
351 "pw" : self.password
352 }
353
354 # Send update to the server.
355 response = self.send_request(self.url, data=data)
356
357 # Get the full response message.
358 output = response.read()
359
360 # Remove all leading and trailing whitespace.
361 output = output.strip()
362
363 # Handle success messages.
364 if "now points to" in output:
365 return
366
367 # Handle error codes.
368 if output == "No hostname to update was supplied.":
369 raise DDNSRequestError(_("No hostname specified."))
370
371 elif output == "The hostname you supplied is not valid.":
372 raise DDNSRequestError(_("Invalid hostname specified."))
373
374 elif output == "The password you supplied is not valid.":
375 raise DDNSAuthenticationError
376
377 elif output == "Administration has disabled this account.":
378 raise DDNSRequestError(_("Account has been disabled."))
379
380 elif output == "Illegal character in IP.":
381 raise DDNSRequestError(_("Invalid IP address has been sent."))
382
383 elif output == "Too many failed requests.":
384 raise DDNSRequestError(_("Too many failed requests."))
385
386 # If we got here, some other update error happened.
387 raise DDNSUpdateError
388
389
390 class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
391 handle = "dyndns.org"
392 name = "Dyn"
393 website = "http://dyn.com/dns/"
394 protocols = ("ipv4",)
395
396 # Information about the format of the request is to be found
397 # http://http://dyn.com/support/developers/api/perform-update/
398 # http://dyn.com/support/developers/api/return-codes/
399
400 url = "https://members.dyndns.org/nic/update"
401
402
403 class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
404 handle = "dynu.com"
405 name = "Dynu"
406 website = "http://dynu.com/"
407 protocols = ("ipv6", "ipv4",)
408
409 # Detailed information about the request and response codes
410 # are available on the providers webpage.
411 # http://dynu.com/Default.aspx?page=dnsapi
412
413 url = "https://api.dynu.com/nic/update"
414
415 def _prepare_request_data(self):
416 data = DDNSProviderDynDNS._prepare_request_data(self)
417
418 # This one supports IPv6
419 data.update({
420 "myipv6" : self.get_address("ipv6"),
421 })
422
423 return data
424
425
426 class DDNSProviderEasyDNS(DDNSProtocolDynDNS2, DDNSProvider):
427 handle = "easydns.com"
428 name = "EasyDNS"
429 website = "http://www.easydns.com/"
430 protocols = ("ipv4",)
431
432 # There is only some basic documentation provided by the vendor,
433 # also searching the web gain very poor results.
434 # http://mediawiki.easydns.com/index.php/Dynamic_DNS
435
436 url = "http://api.cp.easydns.com/dyn/tomato.php"
437
438
439 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
440 handle = "freedns.afraid.org"
441 name = "freedns.afraid.org"
442 website = "http://freedns.afraid.org/"
443
444 # No information about the request or response could be found on the vendor
445 # page. All used values have been collected by testing.
446 url = "https://freedns.afraid.org/dynamic/update.php"
447
448 @property
449 def proto(self):
450 return self.get("proto")
451
452 def update(self):
453 address = self.get_address(self.proto)
454
455 data = {
456 "address" : address,
457 }
458
459 # Add auth token to the update url.
460 url = "%s?%s" % (self.url, self.token)
461
462 # Send update to the server.
463 response = self.send_request(url, data=data)
464
465 if output.startswith("Updated") or "has not changed" in output:
466 return
467
468 # Handle error codes.
469 if output == "ERROR: Unable to locate this record":
470 raise DDNSAuthenticationError
471 elif "is an invalid IP address" in output:
472 raise DDNSRequestError(_("Invalid IP address has been sent."))
473
474 # If we got here, some other update error happened.
475 raise DDNSUpdateError
476
477
478 class DDNSProviderLightningWireLabs(DDNSProvider):
479 handle = "dns.lightningwirelabs.com"
480 name = "Lightning Wire Labs DNS Service"
481 website = "http://dns.lightningwirelabs.com/"
482
483 # Information about the format of the HTTPS request is to be found
484 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
485
486 url = "https://dns.lightningwirelabs.com/update"
487
488 def update(self):
489 data = {
490 "hostname" : self.hostname,
491 "address6" : self.get_address("ipv6", "-"),
492 "address4" : self.get_address("ipv4", "-"),
493 }
494
495 # Check if a token has been set.
496 if self.token:
497 data["token"] = self.token
498
499 # Check for username and password.
500 elif self.username and self.password:
501 data.update({
502 "username" : self.username,
503 "password" : self.password,
504 })
505
506 # Raise an error if no auth details are given.
507 else:
508 raise DDNSConfigurationError
509
510 # Send update to the server.
511 response = self.send_request(self.url, data=data)
512
513 # Handle success messages.
514 if response.code == 200:
515 return
516
517 # If we got here, some other update error happened.
518 raise DDNSUpdateError
519
520
521 class DDNSProviderNamecheap(DDNSProvider):
522 handle = "namecheap.com"
523 name = "Namecheap"
524 website = "http://namecheap.com"
525 protocols = ("ipv4",)
526
527 # Information about the format of the HTTP request is to be found
528 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
529 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
530
531 url = "https://dynamicdns.park-your-domain.com/update"
532
533 def parse_xml(self, document, content):
534 # Send input to the parser.
535 xmldoc = xml.dom.minidom.parseString(document)
536
537 # Get XML elements by the given content.
538 element = xmldoc.getElementsByTagName(content)
539
540 # If no element has been found, we directly can return None.
541 if not element:
542 return None
543
544 # Only get the first child from an element, even there are more than one.
545 firstchild = element[0].firstChild
546
547 # Get the value of the child.
548 value = firstchild.nodeValue
549
550 # Return the value.
551 return value
552
553 def update(self):
554 # Namecheap requires the hostname splitted into a host and domain part.
555 host, domain = self.hostname.split(".", 1)
556
557 data = {
558 "ip" : self.get_address("ipv4"),
559 "password" : self.password,
560 "host" : host,
561 "domain" : domain
562 }
563
564 # Send update to the server.
565 response = self.send_request(self.url, data=data)
566
567 # Get the full response message.
568 output = response.read()
569
570 # Handle success messages.
571 if self.parse_xml(output, "IP") == self.get_address("ipv4"):
572 return
573
574 # Handle error codes.
575 errorcode = self.parse_xml(output, "ResponseNumber")
576
577 if errorcode == "304156":
578 raise DDNSAuthenticationError
579 elif errorcode == "316153":
580 raise DDNSRequestError(_("Domain not found."))
581 elif errorcode == "316154":
582 raise DDNSRequestError(_("Domain not active."))
583 elif errorcode in ("380098", "380099"):
584 raise DDNSInternalServerError
585
586 # If we got here, some other update error happened.
587 raise DDNSUpdateError
588
589
590 class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
591 handle = "no-ip.com"
592 name = "No-IP"
593 website = "http://www.no-ip.com/"
594 protocols = ("ipv4",)
595
596 # Information about the format of the HTTP request is to be found
597 # here: http://www.no-ip.com/integrate/request and
598 # here: http://www.no-ip.com/integrate/response
599
600 url = "http://dynupdate.no-ip.com/nic/update"
601
602 def _prepare_request_data(self):
603 data = {
604 "hostname" : self.hostname,
605 "address" : self.get_address("ipv4"),
606 }
607
608 return data
609
610
611 class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
612 handle = "ovh.com"
613 name = "OVH"
614 website = "http://www.ovh.com/"
615 protocols = ("ipv4",)
616
617 # OVH only provides very limited information about how to
618 # update a DynDNS host. They only provide the update url
619 # on the their german subpage.
620 #
621 # http://hilfe.ovh.de/DomainDynHost
622
623 url = "https://www.ovh.com/nic/update"
624
625 def _prepare_request_data(self):
626 data = DDNSProtocolDynDNS2._prepare_request_data(self)
627 data.update({
628 "system" : "dyndns",
629 })
630
631 return data
632
633
634 class DDNSProviderRegfish(DDNSProvider):
635 handle = "regfish.com"
636 name = "Regfish GmbH"
637 website = "http://www.regfish.com/"
638
639 # A full documentation to the providers api can be found here
640 # but is only available in german.
641 # https://www.regfish.de/domains/dyndns/dokumentation
642
643 url = "https://dyndns.regfish.de/"
644
645 def update(self):
646 data = {
647 "fqdn" : self.hostname,
648 }
649
650 # Check if we update an IPv6 address.
651 address6 = self.get_address("ipv6")
652 if address6:
653 data["ipv6"] = address6
654
655 # Check if we update an IPv4 address.
656 address4 = self.get_address("ipv4")
657 if address4:
658 data["ipv4"] = address4
659
660 # Raise an error if none address is given.
661 if not data.has_key("ipv6") and not data.has_key("ipv4"):
662 raise DDNSConfigurationError
663
664 # Check if a token has been set.
665 if self.token:
666 data["token"] = self.token
667
668 # Raise an error if no token and no useranem and password
669 # are given.
670 elif not self.username and not self.password:
671 raise DDNSConfigurationError(_("No Auth details specified."))
672
673 # HTTP Basic Auth is only allowed if no token is used.
674 if self.token:
675 # Send update to the server.
676 response = self.send_request(self.url, data=data)
677 else:
678 # Send update to the server.
679 response = self.send_request(self.url, username=self.username, password=self.password,
680 data=data)
681
682 # Get the full response message.
683 output = response.read()
684
685 # Handle success messages.
686 if "100" in output or "101" in output:
687 return
688
689 # Handle error codes.
690 if "401" or "402" in output:
691 raise DDNSAuthenticationError
692 elif "408" in output:
693 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
694 elif "409" in output:
695 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
696 elif "412" in output:
697 raise DDNSRequestError(_("No valid FQDN was given."))
698 elif "414" in output:
699 raise DDNSInternalServerError
700
701 # If we got here, some other update error happened.
702 raise DDNSUpdateError
703
704
705 class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
706 handle = "selfhost.de"
707 name = "Selfhost.de"
708 website = "http://www.selfhost.de/"
709 protocols = ("ipv4",)
710
711 url = "https://carol.selfhost.de/nic/update"
712
713 def _prepare_request_data(self):
714 data = DDNSProviderDynDNS._prepare_request_data(self)
715 data.update({
716 "hostname" : "1",
717 })
718
719 return data
720
721
722 class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
723 handle = "spdns.org"
724 name = "SPDNS"
725 website = "http://spdns.org/"
726 protocols = ("ipv4",)
727
728 # Detailed information about request and response codes are provided
729 # by the vendor. They are using almost the same mechanism and status
730 # codes as dyndns.org so we can inherit all those stuff.
731 #
732 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
733 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
734
735 url = "https://update.spdns.de/nic/update"
736
737
738 class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
739 handle = "strato.com"
740 name = "Strato AG"
741 website = "http:/www.strato.com/"
742 protocols = ("ipv4",)
743
744 # Information about the request and response can be obtained here:
745 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
746
747 url = "https://dyndns.strato.com/nic/update"
748
749
750 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
751 handle = "twodns.de"
752 name = "TwoDNS"
753 website = "http://www.twodns.de"
754 protocols = ("ipv4",)
755
756 # Detailed information about the request can be found here
757 # http://twodns.de/en/faqs
758 # http://twodns.de/en/api
759
760 url = "https://update.twodns.de/update"
761
762 def _prepare_request_data(self):
763 data = {
764 "ip" : self.get_address("ipv4"),
765 "hostname" : self.hostname
766 }
767
768 return data
769
770
771 class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
772 handle = "udmedia.de"
773 name = "Udmedia GmbH"
774 website = "http://www.udmedia.de"
775 protocols = ("ipv4",)
776
777 # Information about the request can be found here
778 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
779
780 url = "https://www.udmedia.de/nic/update"
781
782
783 class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
784 handle = "variomedia.de"
785 name = "Variomedia"
786 website = "http://www.variomedia.de/"
787 protocols = ("ipv6", "ipv4",)
788
789 # Detailed information about the request can be found here
790 # https://dyndns.variomedia.de/
791
792 url = "https://dyndns.variomedia.de/nic/update"
793
794 @property
795 def proto(self):
796 return self.get("proto")
797
798 def _prepare_request_data(self):
799 data = {
800 "hostname" : self.hostname,
801 "myip" : self.get_address(self.proto)
802 }
803
804 return data
805
806
807 class DDNSProviderZoneedit(DDNSProtocolDynDNS2, DDNSProvider):
808 handle = "zoneedit.com"
809 name = "Zoneedit"
810 website = "http://www.zoneedit.com"
811 protocols = ("ipv4",)
812
813 # Detailed information about the request and the response codes can be
814 # obtained here:
815 # http://www.zoneedit.com/doc/api/other.html
816 # http://www.zoneedit.com/faq.html
817
818 url = "https://dynamic.zoneedit.com/auth/dynamic.html"
819
820 @property
821 def proto(self):
822 return self.get("proto")
823
824 def update(self):
825 data = {
826 "dnsto" : self.get_address(self.proto),
827 "host" : self.hostname
828 }
829
830 # Send update to the server.
831 response = self.send_request(self.url, username=self.username, password=self.password,
832 data=data)
833
834 # Get the full response message.
835 output = response.read()
836
837 # Handle success messages.
838 if output.startswith("<SUCCESS"):
839 return
840
841 # Handle error codes.
842 if output.startswith("invalid login"):
843 raise DDNSAuthenticationError
844 elif output.startswith("<ERROR CODE=\"704\""):
845 raise DDNSRequestError(_("No valid FQDN was given."))
846 elif output.startswith("<ERROR CODE=\"702\""):
847 raise DDNSInternalServerError
848
849 # If we got here, some other update error happened.
850 raise DDNSUpdateError