mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
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:
commit
ef742124ea
4 changed files with 148 additions and 59 deletions
|
@ -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:
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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(
|
||||||
|
|
175
src/domain.py
175
src/domain.py
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue