mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
autodns: Improve the push system to save managed dns record hashes, similar to the regenconf mecanism
This commit is contained in:
parent
bc39788da9
commit
0a404f6d56
3 changed files with 85 additions and 32 deletions
|
@ -641,8 +641,8 @@ domain:
|
|||
full: --dry-run
|
||||
help: Only display what's to be pushed
|
||||
action: store_true
|
||||
--autoremove:
|
||||
help: Also autoremove records which are stale or not part of the recommended configuration
|
||||
--force:
|
||||
help: Also update/remove records which were not originally set by Yunohost, or which have been manually modified
|
||||
action: store_true
|
||||
--purge:
|
||||
help: Delete all records
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
import os
|
||||
import re
|
||||
import time
|
||||
import hashlib
|
||||
|
||||
from difflib import SequenceMatcher
|
||||
from collections import OrderedDict
|
||||
|
||||
|
@ -33,7 +35,7 @@ from moulinette import m18n, Moulinette
|
|||
from moulinette.utils.log import getActionLogger
|
||||
from moulinette.utils.filesystem import read_file, write_to_file, read_toml
|
||||
|
||||
from yunohost.domain import domain_list, _assert_domain_exists, domain_config_get
|
||||
from yunohost.domain import domain_list, _assert_domain_exists, domain_config_get, _get_domain_settings, _set_domain_settings
|
||||
from yunohost.utils.dns import dig, YNH_DYNDNS_DOMAINS
|
||||
from yunohost.utils.error import YunohostValidationError
|
||||
from yunohost.utils.network import get_public_ip
|
||||
|
@ -516,7 +518,7 @@ def _get_registrar_config_section(domain):
|
|||
|
||||
|
||||
@is_unit_operation()
|
||||
def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=False, purge=False):
|
||||
def domain_registrar_push(operation_logger, domain, dry_run=False, force=False, purge=False):
|
||||
"""
|
||||
Send DNS records to the previously-configured registrar of the domain.
|
||||
"""
|
||||
|
@ -533,6 +535,8 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
if not registrar or registrar in ["None", "yunohost"]:
|
||||
raise YunohostValidationError("registrar_push_not_applicable", domain=domain)
|
||||
|
||||
base_dns_zone = _get_dns_zone_for_domain(domain)
|
||||
|
||||
registrar_credentials = {
|
||||
k.split('.')[-1]: v["value"]
|
||||
for k, v in settings.items()
|
||||
|
@ -549,13 +553,13 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
for record in records:
|
||||
|
||||
# Make sure the name is a FQDN
|
||||
name = f"{record['name']}.{domain}" if record["name"] != "@" else f"{domain}"
|
||||
name = f"{record['name']}.{base_dns_zone}" if record["name"] != "@" else base_dns_zone
|
||||
type_ = record["type"]
|
||||
content = record["value"]
|
||||
|
||||
# Make sure the content is also a FQDN (with trailing . ?)
|
||||
if content == "@" and record["type"] == "CNAME":
|
||||
content = domain + "."
|
||||
content = base_dns_zone + "."
|
||||
|
||||
wanted_records.append({
|
||||
"name": name,
|
||||
|
@ -572,29 +576,31 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
|
||||
if purge:
|
||||
wanted_records = []
|
||||
autoremove = True
|
||||
force = True
|
||||
|
||||
# Construct the base data structure to use lexicon's API.
|
||||
|
||||
base_config = {
|
||||
"provider_name": registrar,
|
||||
"domain": domain,
|
||||
"domain": base_dns_zone,
|
||||
registrar: registrar_credentials
|
||||
}
|
||||
|
||||
# Ugly hack to be able to fetch all record types at once:
|
||||
# we initialize a LexiconClient with type: dummytype,
|
||||
# we initialize a LexiconClient with a dummy type "all"
|
||||
# (which lexicon doesnt actually understands)
|
||||
# then trigger ourselves the authentication + list_records
|
||||
# instead of calling .execute()
|
||||
query = (
|
||||
LexiconConfigResolver()
|
||||
.with_dict(dict_object=base_config)
|
||||
.with_dict(dict_object={"action": "list", "type": "dummytype"})
|
||||
.with_dict(dict_object={"action": "list", "type": "all"})
|
||||
)
|
||||
# current_records.extend(
|
||||
client = LexiconClient(query)
|
||||
client.provider.authenticate()
|
||||
current_records = client.provider.list_records()
|
||||
managed_dns_records_hashes = _get_managed_dns_records_hashes(domain)
|
||||
|
||||
# Keep only records for relevant types: A, AAAA, MX, TXT, CNAME, SRV
|
||||
relevant_types = ["A", "AAAA", "MX", "TXT", "CNAME", "SRV"]
|
||||
|
@ -602,7 +608,7 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
|
||||
# Ignore records which are for a higher-level domain
|
||||
# i.e. we don't care about the records for domain.tld when pushing yuno.domain.tld
|
||||
current_records = [r for r in current_records if r['name'].endswith(f'.{domain}')]
|
||||
current_records = [r for r in current_records if r['name'].endswith(f'.{domain}') or r['name'] == domain]
|
||||
|
||||
for record in current_records:
|
||||
|
||||
|
@ -610,7 +616,10 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
record["name"] = record["name"].strip("@").strip(".")
|
||||
|
||||
# Some API return '@' in content and we shall convert it to absolute/fqdn
|
||||
record["content"] = record["content"].replace('@.', domain + ".").replace('@', domain + ".")
|
||||
record["content"] = record["content"].replace('@.', base_dns_zone + ".").replace('@', base_dns_zone + ".")
|
||||
|
||||
# Check if this record was previously set by YunoHost
|
||||
record["managed_by_yunohost"] = _hash_dns_record(record) in managed_dns_records_hashes
|
||||
|
||||
# Step 0 : Get the list of unique (type, name)
|
||||
# And compare the current and wanted records
|
||||
|
@ -624,7 +633,7 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
# (MX, .domain.tld) 10 domain.tld [10 mx1.ovh.net, 20 mx2.ovh.net]
|
||||
# (TXT, .domain.tld) "v=spf1 ..." ["v=spf1", "foobar"]
|
||||
# (SRV, .domain.tld) 0 5 5269 domain.tld
|
||||
changes = {"delete": [], "update": [], "create": []}
|
||||
changes = {"delete": [], "update": [], "create": [], "unchanged": []}
|
||||
type_and_names = set([(r["type"], r["name"]) for r in current_records + wanted_records])
|
||||
comparison = {type_and_name: {"current": [], "wanted": []} for type_and_name in type_and_names}
|
||||
|
||||
|
@ -652,16 +661,15 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
#
|
||||
if len(current) == 0 and len(wanted) == 0:
|
||||
# No diff, nothing to do
|
||||
changes["unchanged"].extend(records["current"])
|
||||
continue
|
||||
|
||||
elif len(wanted) == 0:
|
||||
for r in current:
|
||||
changes["delete"].append(r)
|
||||
changes["delete"].extend(current)
|
||||
continue
|
||||
|
||||
elif len(current) == 0:
|
||||
for r in current:
|
||||
changes["create"].append(r)
|
||||
changes["create"].extend(wanted)
|
||||
continue
|
||||
|
||||
#
|
||||
|
@ -699,8 +707,8 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
def human_readable_record(action, record):
|
||||
name = record["name"]
|
||||
name = name.strip(".")
|
||||
name = name.replace('.' + domain, "")
|
||||
name = name.replace(domain, "@")
|
||||
name = name.replace('.' + base_dns_zone, "")
|
||||
name = name.replace(base_dns_zone, "@")
|
||||
name = name[:20]
|
||||
t = record["type"]
|
||||
if action in ["create", "update"]:
|
||||
|
@ -710,10 +718,15 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
new_content = record.get("old_content", "(None)")[:30]
|
||||
old_content = record.get("content", "(None)")[:30]
|
||||
|
||||
return f'{name:>20} [{t:^5}] {old_content:^30} -> {new_content:^30}'
|
||||
if not force and action in ["update", "delete"]:
|
||||
ignored = "" if record["managed_by_yunohost"] else "(ignored, won't be changed by Yunohost unless forced)"
|
||||
else:
|
||||
ignored = ""
|
||||
|
||||
return f'{name:>20} [{t:^5}] {old_content:^30} -> {new_content:^30} {ignored}'
|
||||
|
||||
if dry_run:
|
||||
out = {"delete": [], "create": [], "update": [], "ignored": []}
|
||||
out = {"delete": [], "create": [], "update": []}
|
||||
for action in ["delete", "create", "update"]:
|
||||
for record in changes[action]:
|
||||
out[action].append(human_readable_record(action, record))
|
||||
|
@ -722,13 +735,17 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
|
||||
operation_logger.start()
|
||||
|
||||
new_managed_dns_records_hashes = [_hash_dns_record(r) for r in changes["unchanged"]]
|
||||
|
||||
# Push the records
|
||||
for action in ["delete", "create", "update"]:
|
||||
if action == "delete" and not autoremove:
|
||||
continue
|
||||
|
||||
for record in changes[action]:
|
||||
|
||||
if not force and action in ["update", "delete"] and not record["managed_by_yunohost"]:
|
||||
# Don't overwrite manually-set or manually-modified records
|
||||
continue
|
||||
|
||||
record["action"] = action
|
||||
|
||||
# Apparently Lexicon yields us some 'id' during fetch
|
||||
|
@ -737,11 +754,10 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
record["identifier"] = record["id"]
|
||||
del record["id"]
|
||||
|
||||
if "old_content" in record:
|
||||
del record["old_content"]
|
||||
logger.info(action + " : " + human_readable_record(action, record))
|
||||
|
||||
if registrar == "godaddy":
|
||||
if record["name"] == domain:
|
||||
if record["name"] == base_dns_zone:
|
||||
record["name"] = "@." + record["name"]
|
||||
if record["type"] in ["MX", "SRV"]:
|
||||
logger.warning(f"Pushing {record['type']} records is not properly supported by Lexicon/Godaddy.")
|
||||
|
@ -753,8 +769,6 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
if registrar == "gandi":
|
||||
del record["ttl"]
|
||||
|
||||
logger.info(action + " : " + human_readable_record(action, record))
|
||||
|
||||
query = (
|
||||
LexiconConfigResolver()
|
||||
.with_dict(dict_object=base_config)
|
||||
|
@ -767,8 +781,29 @@ def domain_registrar_push(operation_logger, domain, dry_run=False, autoremove=Fa
|
|||
logger.error(f"Failed to {action} record {record['type']}/{record['name']} : {e}")
|
||||
else:
|
||||
if result:
|
||||
new_managed_dns_records_hashes.append(_hash_dns_record(record))
|
||||
logger.success("Done!")
|
||||
else:
|
||||
logger.error("Uhoh!?")
|
||||
|
||||
# FIXME : implement a system to properly report what worked and what did not at the end of the command..
|
||||
_set_managed_dns_records_hashes(domain, new_managed_dns_records_hashes)
|
||||
|
||||
# FIXME : implement a system to properly report what worked and what did not at the end of the command..
|
||||
|
||||
|
||||
def _get_managed_dns_records_hashes(domain: str) -> list:
|
||||
return _get_domain_settings(domain).get("managed_dns_records_hashes", [])
|
||||
|
||||
|
||||
def _set_managed_dns_records_hashes(domain: str, hashes: list) -> None:
|
||||
settings = _get_domain_settings(domain)
|
||||
settings["managed_dns_records_hashes"] = hashes or []
|
||||
_set_domain_settings(domain, settings)
|
||||
|
||||
|
||||
def _hash_dns_record(record: dict) -> int:
|
||||
|
||||
fields = ["name", "type", "content"]
|
||||
record_ = {f: record.get(f) for f in fields}
|
||||
|
||||
return hash(frozenset(record_.items()))
|
||||
|
|
|
@ -28,7 +28,7 @@ import os
|
|||
from moulinette import m18n, Moulinette
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.utils.log import getActionLogger
|
||||
from moulinette.utils.filesystem import write_to_file
|
||||
from moulinette.utils.filesystem import write_to_file, read_yaml, write_to_yaml
|
||||
|
||||
from yunohost.app import (
|
||||
app_ssowatconf,
|
||||
|
@ -449,6 +449,24 @@ class DomainConfigPanel(ConfigPanel):
|
|||
# FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ...
|
||||
self.values["registrar"] = self.registar_id
|
||||
|
||||
|
||||
def _get_domain_settings(domain: str) -> dict:
|
||||
|
||||
_assert_domain_exists(domain)
|
||||
|
||||
if os.path.exists(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml"):
|
||||
return read_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml") or {}
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def _set_domain_settings(domain: str, settings: dict) -> None:
|
||||
|
||||
_assert_domain_exists(domain)
|
||||
|
||||
write_to_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", settings)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
# Stuff managed in other files
|
||||
|
@ -491,6 +509,6 @@ def domain_dns_suggest(domain):
|
|||
return yunohost.dns.domain_dns_suggest(domain)
|
||||
|
||||
|
||||
def domain_dns_push(domain, dry_run, autoremove, purge):
|
||||
def domain_dns_push(domain, dry_run, force, purge):
|
||||
import yunohost.dns
|
||||
return yunohost.dns.domain_registrar_push(domain, dry_run, autoremove, purge)
|
||||
return yunohost.dns.domain_registrar_push(domain, dry_run, force, purge)
|
||||
|
|
Loading…
Add table
Reference in a new issue