def dyndns_update( operation_logger, dyn_host="dyndns.yunohost.org", domain=None, key=None, ipv4=None, ipv6=None, force=False, dry_run=False, ): """ Update IP on DynDNS platform Keyword argument: domain -- Full domain to update dyn_host -- Dynette DNS server to inform key -- Public DNS key ipv4 -- IP address to send ipv6 -- IPv6 address to send """ # Get old ipv4/v6 old_ipv4, old_ipv6 = (None, None) # (default values) # If domain is not given, try to guess it from keys available... if domain is None: (domain, key) = _guess_current_dyndns_domain(dyn_host) if domain is None: raise YunohostValidationError('dyndns_no_domain_registered') # If key is not given, pick the first file we find with the domain given else: if key is None: keys = glob.glob( "/etc/yunohost/dyndns/K{0}.+*.private".format(domain)) if not keys: raise YunohostValidationError("dyndns_key_not_found") key = keys[0] # Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me' host = domain.split(".")[1:] host = ".".join(host) logger.debug("Building zone update file ...") lines = [ "server %s" % dyn_host, "zone %s" % host, ] def resolve_domain(domain, rdtype): # FIXME make this work for IPv6-only hosts too.. ok, result = dig(dyn_host, "A") dyn_host_ip = result[0] if ok == "ok" and len(result) else None if not dyn_host_ip: raise YunohostError("Failed to resolve %s" % dyn_host) ok, result = dig(domain, rdtype, resolvers=[dyn_host_ip]) if ok == "ok": return result[0] if len(result) else None elif result[0] == "Timeout": logger.debug( "Timed-out while trying to resolve %s record for %s using %s" % (rdtype, domain, dyn_host)) else: return None logger.debug("Falling back to external resolvers") ok, result = dig(domain, rdtype, resolvers="force_external") if ok == "ok": return result[0] if len(result) else None elif result[0] == "Timeout": logger.debug( "Timed-out while trying to resolve %s record for %s using external resolvers : %s" % (rdtype, domain, result)) else: return None raise YunohostError("Failed to resolve %s for %s" % (rdtype, domain), raw_msg=True) old_ipv4 = resolve_domain(domain, "A") old_ipv6 = resolve_domain(domain, "AAAA") # Get current IPv4 and IPv6 ipv4_ = get_public_ip() ipv6_ = get_public_ip(6) if ipv4 is None: ipv4 = ipv4_ if ipv6 is None: ipv6 = ipv6_ logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6)) logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6)) # no need to update if (not force and not dry_run) and (old_ipv4 == ipv4 and old_ipv6 == ipv6): logger.info("No updated needed.") return else: operation_logger.related_to.append(("domain", domain)) operation_logger.start() logger.info("Updated needed, going on...") dns_conf = _build_dns_conf(domain) # Delete custom DNS records, we don't support them (have to explicitly # authorize them on dynette) for category in dns_conf.keys(): if category not in ["basic", "mail", "xmpp", "extra"]: del dns_conf[category] # Delete the old records for all domain/subdomains # every dns_conf.values() is a list of : # [{"name": "...", "ttl": "...", "type": "...", "value": "..."}] for records in dns_conf.values(): for record in records: action = "update delete {name}.{domain}.".format(domain=domain, **record) action = action.replace(" @.", " ") lines.append(action) # Add the new records for all domain/subdomains for records in dns_conf.values(): for record in records: # (For some reason) here we want the format with everytime the # entire, full domain shown explicitly, not just "muc" or "@", it # should be muc.the.domain.tld. or the.domain.tld if record["value"] == "@": record["value"] = domain record["value"] = record["value"].replace(";", r"\;") action = "update add {name}.{domain}. {ttl} {type} {value}".format( domain=domain, **record) action = action.replace(" @.", " ") lines.append(action) lines += ["show", "send"] # Write the actions to do to update to a file, to be able to pass it # to nsupdate as argument write_to_file(DYNDNS_ZONE, "\n".join(lines)) logger.debug("Now pushing new conf to DynDNS host...") if not dry_run: try: command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE] subprocess.check_call(command) except subprocess.CalledProcessError: raise YunohostError("dyndns_ip_update_failed") logger.success(m18n.n("dyndns_ip_updated")) else: print(read_file(DYNDNS_ZONE)) print("") print( "Warning: dry run, this is only the generated config, it won't be applied" )
def check_domain(self, domain, is_main_domain, is_subdomain): expected_configuration = _build_dns_conf( domain, include_empty_AAAA_if_no_ipv6=True) categories = ["basic", "mail", "xmpp", "extra"] # For subdomains, we only diagnosis A and AAAA records if is_subdomain: categories = ["basic"] for category in categories: records = expected_configuration[category] discrepancies = [] results = {} for r in records: id_ = r["type"] + ":" + r["name"] r["current"] = self.get_current_record(domain, r["name"], r["type"]) if r["value"] == "@": r["value"] = domain + "." if self.current_record_match_expected(r): results[id_] = "OK" else: if r["current"] is None: results[id_] = "MISSING" discrepancies.append( ("diagnosis_dns_missing_record", r)) else: results[id_] = "WRONG" discrepancies.append(("diagnosis_dns_discrepancy", r)) def its_important(): # Every mail DNS records are important for main domain # For other domain, we only report it as a warning for now... if is_main_domain and category == "mail": return True elif category == "basic": # A bad or missing A record is critical ... # And so is a wrong AAAA record # (However, a missing AAAA record is acceptable) if results["A:@"] != "OK" or results["AAAA:@"] == "WRONG": return True return False if discrepancies: status = "ERROR" if its_important() else "WARNING" summary = "diagnosis_dns_bad_conf" else: status = "SUCCESS" summary = "diagnosis_dns_good_conf" output = dict( meta={ "domain": domain, "category": category }, data=results, status=status, summary=summary, ) if discrepancies: # For ynh-managed domains (nohost.me etc...), tell people to try to "yunohost dyndns update --force" if any( domain.endswith(ynh_dyndns_domain) for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS): output["details"] = [ "diagnosis_dns_try_dyndns_update_force" ] # Otherwise point to the documentation else: output["details"] = ["diagnosis_dns_point_to_doc"] output["details"] += discrepancies yield output