Example #1
0
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"
        )
Example #2
0
    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