Merge pull request #1434 from YunoHost/enh-domains

Ability to "list" domain as a tree structure, + add a new domain_info endpoint
This commit is contained in:
Alexandre Aubin 2022-10-07 15:25:51 +02:00 committed by GitHub
commit ef742124ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 148 additions and 59 deletions

View file

@ -443,6 +443,19 @@ domain:
--exclude-subdomains: --exclude-subdomains:
help: Filter out domains that are obviously subdomains of other declared domains help: Filter out domains that are obviously subdomains of other declared domains
action: store_true action: store_true
--tree:
help: Display domains as a tree
action: store_true
### domain_info()
info:
action_help: Get domain aggredated data
api: GET /domains/<domain>
arguments:
domain:
help: Domain to check
extra:
pattern: *pattern_domain
### domain_add() ### domain_add()
add: add:

View file

@ -5,12 +5,13 @@ i18n = "domain_config"
# Other things we may want to implement in the future: # Other things we may want to implement in the future:
# #
# - maindomain handling # - maindomain handling
# - default app
# - autoredirect www in nginx conf # - autoredirect www in nginx conf
# - ? # - ?
# #
[feature] [feature]
name = "Features"
[feature.app] [feature.app]
[feature.app.default_app] [feature.app.default_app]
type = "app" type = "app"
@ -46,6 +47,7 @@ i18n = "domain_config"
default = 0 default = 0
[dns] [dns]
name = "DNS"
[dns.registrar] [dns.registrar]
optional = true optional = true
@ -61,6 +63,7 @@ i18n = "domain_config"
[cert] [cert]
name = "Certificate"
[cert.status] [cert.status]
name = "Status" name = "Status"
@ -90,13 +93,13 @@ i18n = "domain_config"
[cert.cert.acme_eligible_explain] [cert.cert.acme_eligible_explain]
type = "alert" type = "alert"
style = "warning" style = "warning"
visible = "acme_eligible == false" visible = "acme_eligible == false || acme_elligible == null"
[cert.cert.cert_no_checks] [cert.cert.cert_no_checks]
ask = "Ignore diagnosis checks" ask = "Ignore diagnosis checks"
type = "boolean" type = "boolean"
default = false default = false
visible = "acme_eligible == false" visible = "acme_eligible == false || acme_elligible == null"
[cert.cert.cert_install] [cert.cert.cert_install]
type = "button" type = "button"

View file

@ -99,8 +99,12 @@ def certificate_status(domains, full=False):
try: try:
_check_domain_is_ready_for_ACME(domain) _check_domain_is_ready_for_ACME(domain)
status["ACME_eligible"] = True status["ACME_eligible"] = True
except Exception: except Exception as e:
status["ACME_eligible"] = False if e.key == 'certmanager_domain_not_diagnosed_yet':
status["ACME_eligible"] = None # = unknown status
else:
status["ACME_eligible"] = False
del status["domain"] del status["domain"]
certificates[domain] = status certificates[domain] = status
@ -794,7 +798,7 @@ def _check_domain_is_ready_for_ACME(domain):
or {} or {}
) )
parent_domain = _get_parent_domain_of(domain) parent_domain = _get_parent_domain_of(domain, return_self=True)
dnsrecords = ( dnsrecords = (
Diagnoser.get_cached_report( Diagnoser.get_cached_report(

View file

@ -24,7 +24,9 @@
Manage domains Manage domains
""" """
import os import os
from typing import Dict, Any import time
from typing import List
from collections import OrderedDict
from moulinette import m18n, Moulinette from moulinette import m18n, Moulinette
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
@ -47,58 +49,131 @@ logger = getActionLogger("yunohost.domain")
DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains"
# Lazy dev caching to avoid re-query ldap every time we need the domain list # Lazy dev caching to avoid re-query ldap every time we need the domain list
domain_list_cache: Dict[str, Any] = {} # The cache automatically expire every 15 seconds, to prevent desync between
# yunohost CLI and API which run in different processes
domain_list_cache: List[str] = []
domain_list_cache_timestamp = 0
main_domain_cache: str = None
main_domain_cache_timestamp = 0
DOMAIN_CACHE_DURATION = 15
def domain_list(exclude_subdomains=False): def _get_maindomain():
global main_domain_cache
global main_domain_cache_timestamp
if not main_domain_cache or abs(main_domain_cache_timestamp - time.time()) > DOMAIN_CACHE_DURATION:
with open("/etc/yunohost/current_host", "r") as f:
main_domain_cache = f.readline().rstrip()
main_domain_cache_timestamp = time.time()
return main_domain_cache
def _get_domains(exclude_subdomains=False):
global domain_list_cache
global domain_list_cache_timestamp
if not domain_list_cache or abs(domain_list_cache_timestamp - time.time()) > DOMAIN_CACHE_DURATION:
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
result = [
entry["virtualdomain"][0]
for entry in ldap.search("ou=domains", "virtualdomain=*", ["virtualdomain"])
]
def cmp_domain(domain):
# Keep the main part of the domain and the extension together
# eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this']
domain = domain.split(".")
domain[-1] = domain[-2] + domain.pop()
return list(reversed(domain))
domain_list_cache = sorted(result, key=cmp_domain)
domain_list_cache_timestamp = time.time()
if exclude_subdomains:
return [
domain
for domain in domain_list_cache
if not _get_parent_domain_of(domain, return_self=False)
]
return domain_list_cache
def domain_list(exclude_subdomains=False, tree=False):
""" """
List domains List domains
Keyword argument: Keyword argument:
exclude_subdomains -- Filter out domains that are subdomains of other declared domains exclude_subdomains -- Filter out domains that are subdomains of other declared domains
tree -- Display domains as a hierarchy tree
""" """
global domain_list_cache
if not exclude_subdomains and domain_list_cache:
return domain_list_cache
from yunohost.utils.ldap import _get_ldap_interface domains = _get_domains(exclude_subdomains)
main = _get_maindomain()
ldap = _get_ldap_interface() if not tree:
result = [ return {"domains": domains, "main": main}
entry["virtualdomain"][0]
for entry in ldap.search("ou=domains", "virtualdomain=*", ["virtualdomain"])
]
result_list = [] if tree and exclude_subdomains:
for domain in result: return {
if exclude_subdomains: "domains": OrderedDict({domain: {} for domain in domains}),
parent_domain = domain.split(".", 1)[1] "main": main,
if parent_domain in result: }
continue
result_list.append(domain) def get_parent_dict(tree, child):
# If parent exists it should be the last added (see `_get_domains` ordering)
possible_parent = next(reversed(tree)) if tree else None
if possible_parent and child.endswith(f".{possible_parent}"):
return get_parent_dict(tree[possible_parent], child)
return tree
def cmp_domain(domain): result = OrderedDict()
# Keep the main part of the domain and the extension together for domain in domains:
# eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this'] parent = get_parent_dict(result, domain)
domain = domain.split(".") parent[domain] = OrderedDict()
domain[-1] = domain[-2] + domain.pop()
domain = list(reversed(domain))
return domain
result_list = sorted(result_list, key=cmp_domain) return {"domains": result, "main": main}
# Don't cache answer if using exclude_subdomains
if exclude_subdomains:
return {"domains": result_list, "main": _get_maindomain()}
domain_list_cache = {"domains": result_list, "main": _get_maindomain()} def domain_info(domain):
return domain_list_cache """
Print aggregate data for a specific domain
Keyword argument:
domain -- Domain to be checked
"""
from yunohost.app import app_info
from yunohost.dns import _get_registar_settings
_assert_domain_exists(domain)
registrar, _ = _get_registar_settings(domain)
certificate = domain_cert_status([domain], full=True)["certificates"][domain]
apps = []
for app in _installed_apps():
settings = _get_app_settings(app)
if settings.get("domain") == domain:
apps.append(
{"name": app_info(app)["name"], "id": app, "path": settings["path"]}
)
return {
"certificate": certificate,
"registrar": registrar,
"apps": apps,
"main": _get_maindomain() == domain,
"topest_parent": _get_parent_domain_of(domain, return_self=True, topest=True),
# TODO : add parent / child domains ?
}
def _assert_domain_exists(domain): def _assert_domain_exists(domain):
if domain not in domain_list()["domains"]: if domain not in _get_domains():
raise YunohostValidationError("domain_unknown", domain=domain) raise YunohostValidationError("domain_unknown", domain=domain)
@ -107,26 +182,26 @@ def _list_subdomains_of(parent_domain):
_assert_domain_exists(parent_domain) _assert_domain_exists(parent_domain)
out = [] out = []
for domain in domain_list()["domains"]: for domain in _get_domains():
if domain.endswith(f".{parent_domain}"): if domain.endswith(f".{parent_domain}"):
out.append(domain) out.append(domain)
return out return out
def _get_parent_domain_of(domain): def _get_parent_domain_of(domain, return_self=True, topest=False):
_assert_domain_exists(domain) _assert_domain_exists(domain)
if "." not in domain: domains = _get_domains(exclude_subdomains=topest)
return domain
parent_domain = domain.split(".", 1)[-1] domain_ = domain
if parent_domain not in domain_list()["domains"]: while "." in domain_:
return domain # Domain is its own parent domain_ = domain_.split(".", 1)[1]
if domain_ in domains:
return domain_
else: return domain if return_self else None
return _get_parent_domain_of(parent_domain)
@is_unit_operation() @is_unit_operation()
@ -198,7 +273,7 @@ def domain_add(operation_logger, domain, dyndns=False):
raise YunohostError("domain_creation_failed", domain=domain, error=e) raise YunohostError("domain_creation_failed", domain=domain, error=e)
finally: finally:
global domain_list_cache global domain_list_cache
domain_list_cache = {} domain_list_cache = []
# Don't regen these conf if we're still in postinstall # Don't regen these conf if we're still in postinstall
if os.path.exists("/etc/yunohost/installed"): if os.path.exists("/etc/yunohost/installed"):
@ -255,7 +330,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False):
# Check domain is not the main domain # Check domain is not the main domain
if domain == _get_maindomain(): if domain == _get_maindomain():
other_domains = domain_list()["domains"] other_domains = _get_domains()
other_domains.remove(domain) other_domains.remove(domain)
if other_domains: if other_domains:
@ -316,7 +391,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False):
raise YunohostError("domain_deletion_failed", domain=domain, error=e) raise YunohostError("domain_deletion_failed", domain=domain, error=e)
finally: finally:
global domain_list_cache global domain_list_cache
domain_list_cache = {} domain_list_cache = []
stuff_to_delete = [ stuff_to_delete = [
f"/etc/yunohost/certs/{domain}", f"/etc/yunohost/certs/{domain}",
@ -380,8 +455,8 @@ def domain_main_domain(operation_logger, new_main_domain=None):
# Apply changes to ssl certs # Apply changes to ssl certs
try: try:
write_to_file("/etc/yunohost/current_host", new_main_domain) write_to_file("/etc/yunohost/current_host", new_main_domain)
global domain_list_cache global main_domain_cache
domain_list_cache = {} main_domain_cache = new_main_domain
_set_hostname(new_main_domain) _set_hostname(new_main_domain)
except Exception as e: except Exception as e:
logger.warning(str(e), exc_info=1) logger.warning(str(e), exc_info=1)
@ -409,12 +484,6 @@ def domain_url_available(domain, path):
return len(_get_conflicting_apps(domain, path)) == 0 return len(_get_conflicting_apps(domain, path)) == 0
def _get_maindomain():
with open("/etc/yunohost/current_host", "r") as f:
maindomain = f.readline().rstrip()
return maindomain
def domain_config_get(domain, key="", full=False, export=False): def domain_config_get(domain, key="", full=False, export=False):
""" """
Display a domain configuration Display a domain configuration