~ working push_config. Tested with Gandi. To be improved!

This commit is contained in:
Paco 2021-04-28 00:26:19 +02:00
parent ddeea564f1
commit eeab7cd103
2 changed files with 136 additions and 37 deletions

View file

@ -447,6 +447,16 @@ domain:
help: Subscribe to the DynDNS service help: Subscribe to the DynDNS service
action: store_true action: store_true
### domain_push_config()
push_config:
action_help: Push DNS records to registrar
api: GET /domains/push
arguments:
domain:
help: Domain name to add
extra:
pattern: *pattern_domain
### domain_remove() ### domain_remove()
remove: remove:
action_help: Delete domains action_help: Delete domains

View file

@ -27,6 +27,10 @@ import os
import re import re
import sys import sys
import yaml import yaml
import functools
from lexicon.config import ConfigResolver
from lexicon.client import Client
from moulinette import m18n, msettings, msignals from moulinette import m18n, msettings, msignals
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
@ -103,7 +107,6 @@ def domain_add(operation_logger, domain, dyndns=False):
from yunohost.hook import hook_callback from yunohost.hook import hook_callback
from yunohost.app import app_ssowatconf from yunohost.app import app_ssowatconf
from yunohost.utils.ldap import _get_ldap_interface from yunohost.utils.ldap import _get_ldap_interface
from yunohost.certificate import _certificate_install_selfsigned
if domain.startswith("xmpp-upload."): if domain.startswith("xmpp-upload."):
raise YunohostValidationError("domain_cannot_add_xmpp_upload") raise YunohostValidationError("domain_cannot_add_xmpp_upload")
@ -126,7 +129,7 @@ def domain_add(operation_logger, domain, dyndns=False):
# Do not allow to subscribe to multiple dyndns domains... # Do not allow to subscribe to multiple dyndns domains...
if _guess_current_dyndns_domain("dyndns.yunohost.org") != (None, None): if _guess_current_dyndns_domain("dyndns.yunohost.org") != (None, None):
raise YunohostValidationError("domain_dyndns_already_subscribed") raise YunohostValidationError('domain_dyndns_already_subscribed')
# Check that this domain can effectively be provided by # Check that this domain can effectively be provided by
# dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st) # dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st)
@ -137,13 +140,14 @@ def domain_add(operation_logger, domain, dyndns=False):
if dyndns: if dyndns:
from yunohost.dyndns import dyndns_subscribe from yunohost.dyndns import dyndns_subscribe
# Actually subscribe # Actually subscribe
dyndns_subscribe(domain=domain) dyndns_subscribe(domain=domain)
_certificate_install_selfsigned([domain], False)
try: try:
import yunohost.certificate
yunohost.certificate._certificate_install_selfsigned([domain], False)
attr_dict = { attr_dict = {
"objectClass": ["mailDomain", "top"], "objectClass": ["mailDomain", "top"],
"virtualdomain": domain, "virtualdomain": domain,
@ -170,13 +174,13 @@ def domain_add(operation_logger, domain, dyndns=False):
regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd"]) regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd"])
app_ssowatconf() app_ssowatconf()
except Exception as e: except Exception:
# Force domain removal silently # Force domain removal silently
try: try:
domain_remove(domain, force=True) domain_remove(domain, force=True)
except Exception: except Exception:
pass pass
raise e raise
hook_callback("post_domain_add", args=[domain]) hook_callback("post_domain_add", args=[domain])
@ -202,8 +206,8 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False):
# the 'force' here is related to the exception happening in domain_add ... # the 'force' here is related to the exception happening in domain_add ...
# we don't want to check the domain exists because the ldap add may have # we don't want to check the domain exists because the ldap add may have
# failed # failed
if not force and domain not in domain_list()["domains"]: if not force and domain not in domain_list()['domains']:
raise YunohostValidationError("domain_name_unknown", domain=domain) raise YunohostValidationError('domain_name_unknown', domain=domain)
# Check domain is not the main domain # Check domain is not the main domain
if domain == _get_maindomain(): if domain == _get_maindomain():
@ -217,9 +221,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False):
other_domains="\n * " + ("\n * ".join(other_domains)), other_domains="\n * " + ("\n * ".join(other_domains)),
) )
else: else:
raise YunohostValidationError( raise YunohostValidationError("domain_cannot_remove_main_add_new_one", domain=domain)
"domain_cannot_remove_main_add_new_one", domain=domain
)
# Check if apps are installed on the domain # Check if apps are installed on the domain
apps_on_that_domain = [] apps_on_that_domain = []
@ -228,37 +230,21 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False):
settings = _get_app_settings(app) settings = _get_app_settings(app)
label = app_info(app)["name"] label = app_info(app)["name"]
if settings.get("domain") == domain: if settings.get("domain") == domain:
apps_on_that_domain.append( apps_on_that_domain.append((app, " - %s \"%s\" on https://%s%s" % (app, label, domain, settings["path"]) if "path" in settings else app))
(
app,
' - %s "%s" on https://%s%s'
% (app, label, domain, settings["path"])
if "path" in settings
else app,
)
)
if apps_on_that_domain: if apps_on_that_domain:
if remove_apps: if remove_apps:
if msettings.get("interface") == "cli" and not force: if msettings.get('interface') == "cli" and not force:
answer = msignals.prompt( answer = msignals.prompt(m18n.n('domain_remove_confirm_apps_removal',
m18n.n(
"domain_remove_confirm_apps_removal",
apps="\n".join([x[1] for x in apps_on_that_domain]), apps="\n".join([x[1] for x in apps_on_that_domain]),
answers="y/N", answers='y/N'), color="yellow")
),
color="yellow",
)
if answer.upper() != "Y": if answer.upper() != "Y":
raise YunohostError("aborting") raise YunohostError("aborting")
for app, _ in apps_on_that_domain: for app, _ in apps_on_that_domain:
app_remove(app) app_remove(app)
else: else:
raise YunohostValidationError( raise YunohostValidationError('domain_uninstall_app_first', apps="\n".join([x[1] for x in apps_on_that_domain]))
"domain_uninstall_app_first",
apps="\n".join([x[1] for x in apps_on_that_domain]),
)
operation_logger.start() operation_logger.start()
@ -271,7 +257,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False):
os.system("rm -rf /etc/yunohost/certs/%s" % domain) os.system("rm -rf /etc/yunohost/certs/%s" % domain)
# Delete dyndns keys for this domain (if any) # Delete dyndns keys for this domain (if any)
os.system("rm -rf /etc/yunohost/dyndns/K%s.+*" % domain) os.system('rm -rf /etc/yunohost/dyndns/K%s.+*' % domain)
# Sometime we have weird issues with the regenconf where some files # Sometime we have weird issues with the regenconf where some files
# appears as manually modified even though they weren't touched ... # appears as manually modified even though they weren't touched ...
@ -558,8 +544,9 @@ def _build_dns_conf(domains):
if ipv6: if ipv6:
extra.append(["*", ttl, "AAAA", ipv6]) extra.append(["*", ttl, "AAAA", ipv6])
elif include_empty_AAAA_if_no_ipv6: # TODO
extra.append(["*", ttl, "AAAA", None]) # elif include_empty_AAAA_if_no_ipv6:
# extra.append(["*", ttl, "AAAA", None])
extra.append([name, ttl, "CAA", '128 issue "letsencrypt.org"']) extra.append([name, ttl, "CAA", '128 issue "letsencrypt.org"'])
@ -838,3 +825,105 @@ def _set_domain_settings(domain, domain_settings):
with open(DOMAIN_SETTINGS_PATH, 'w') as file: with open(DOMAIN_SETTINGS_PATH, 'w') as file:
yaml.dump(domains, file, default_flow_style=False) yaml.dump(domains, file, default_flow_style=False)
def domain_push_config(domain):
"""
Send DNS records to the previously-configured registrar of the domain.
"""
# Generate the records
if domain not in domain_list()["domains"]:
raise YunohostValidationError("domain_name_unknown", domain=domain)
domains_settings = _get_domain_settings(domain, True)
dns_conf = _build_dns_conf(domains_settings)
# Flatten the DNS conf
flatten_dns_conf = []
for key in dns_conf:
list_of_records = dns_conf[key]
for record in list_of_records:
# FIXME Lexicon does not support CAA records
# See https://github.com/AnalogJ/lexicon/issues/282 and https://github.com/AnalogJ/lexicon/pull/371
# They say it's trivial to implement it!
# And yet, it is still not done/merged
if record["type"] != "CAA":
# Add .domain.tdl to the name entry
record["name"] = "{}.{}".format(record["name"], domain)
flatten_dns_conf.append(record)
# Get provider info
# TODO
provider = {
"name": "gandi",
"options": {
"api_protocol": "rest",
"auth_token": "vhcIALuRJKtoZiZyxfDYWLom"
}
}
# Construct the base data structure to use lexicon's API.
base_config = {
"provider_name": provider["name"],
"domain": domain, # domain name
}
base_config[provider["name"]] = provider["options"]
# Get types present in the generated records
types = set()
for record in flatten_dns_conf:
types.add(record["type"])
# Fetch all types present in the generated records
distant_records = {}
for key in types:
record_config = {
"action": "list",
"type": key,
}
final_lexicon = ConfigResolver().with_dict(dict_object=base_config).with_dict(dict_object=record_config)
# print('final_lexicon:', final_lexicon);
client = Client(final_lexicon)
distant_records[key] = client.execute()
for key in types:
for distant_record in distant_records[key]:
print('distant_record:', distant_record);
for local_record in flatten_dns_conf:
print('local_record:', local_record);
# Push the records
for record in flatten_dns_conf:
# For each record, first check if one record exists for the same (type, name) couple
it_exists = False
# TODO do not push if local and distant records are exactly the same ?
# is_the_same_record = False
for distant_record in distant_records[record["type"]]:
if distant_record["type"] == record["type"] and distant_record["name"] == record["name"]:
it_exists = True
# previous TODO
# if distant_record["ttl"] = ... and distant_record["name"] ...
# is_the_same_record = True
# Finally, push the new record or update the existing one
record_config = {
"action": "update" if it_exists else "create", # create, list, update, delete
"type": record["type"], # specify a type for record filtering, case sensitive in some cases.
"name": record["name"],
"content": record["value"],
# FIXME Delte TTL, doesn't work with Gandi.
# See https://github.com/AnalogJ/lexicon/issues/726 (similar issue)
# But I think there is another issue with Gandi. Or I'm misusing the API...
# "ttl": record["ttl"],
}
final_lexicon = ConfigResolver().with_dict(dict_object=base_config).with_dict(dict_object=record_config)
client = Client(final_lexicon)
print('pushed_record:', record_config, "", end=' ')
results = client.execute()
print('results:', results);
# print("Failed" if results == False else "Ok")
# def domain_config_fetch(domain, key, value):