mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
[enh] config panel python method hook
This commit is contained in:
parent
8dbc93c674
commit
9f7fb61b50
3 changed files with 143 additions and 60 deletions
|
@ -1608,14 +1608,9 @@ def app_config_set(
|
|||
|
||||
|
||||
class AppConfigPanel(ConfigPanel):
|
||||
def __init__(self, app):
|
||||
|
||||
# Check app is installed
|
||||
_assert_is_installed(app)
|
||||
|
||||
self.app = app
|
||||
config_path = os.path.join(APPS_SETTING_PATH, app, "config_panel.toml")
|
||||
super().__init__(config_path=config_path)
|
||||
entity_type = "app"
|
||||
save_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/settings.yml")
|
||||
config_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/config_panel.yml")
|
||||
|
||||
def _load_current_values(self):
|
||||
self.values = self._call_config_script("show")
|
||||
|
@ -1639,7 +1634,7 @@ class AppConfigPanel(ConfigPanel):
|
|||
from yunohost.hook import hook_exec
|
||||
|
||||
# Add default config script if needed
|
||||
config_script = os.path.join(APPS_SETTING_PATH, self.app, "scripts", "config")
|
||||
config_script = os.path.join(APPS_SETTING_PATH, self.entity, "scripts", "config")
|
||||
if not os.path.exists(config_script):
|
||||
logger.debug("Adding a default config script")
|
||||
default_script = """#!/bin/bash
|
||||
|
@ -1651,15 +1646,15 @@ ynh_app_config_run $1
|
|||
|
||||
# Call config script to extract current values
|
||||
logger.debug(f"Calling '{action}' action from config script")
|
||||
app_id, app_instance_nb = _parse_app_instance_name(self.app)
|
||||
app_id, app_instance_nb = _parse_app_instance_name(self.entity)
|
||||
settings = _get_app_settings(app_id)
|
||||
env.update(
|
||||
{
|
||||
"app_id": app_id,
|
||||
"app": self.app,
|
||||
"app": self.entity,
|
||||
"app_instance_nb": str(app_instance_nb),
|
||||
"final_path": settings.get("final_path", ""),
|
||||
"YNH_APP_BASEDIR": os.path.join(APPS_SETTING_PATH, self.app),
|
||||
"YNH_APP_BASEDIR": os.path.join(APPS_SETTING_PATH, self.entity),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -452,14 +452,9 @@ def domain_config_set(
|
|||
|
||||
|
||||
class DomainConfigPanel(ConfigPanel):
|
||||
def __init__(self, domain):
|
||||
_assert_domain_exists(domain)
|
||||
self.domain = domain
|
||||
self.save_mode = "diff"
|
||||
super().__init__(
|
||||
config_path=DOMAIN_CONFIG_PATH,
|
||||
save_path=f"{DOMAIN_SETTINGS_DIR}/{domain}.yml",
|
||||
)
|
||||
entity_type = "domain"
|
||||
save_path_tpl = f"{DOMAIN_SETTINGS_PATH}/{entity}.yml")
|
||||
save_mode = "diff"
|
||||
|
||||
def _get_toml(self):
|
||||
from yunohost.dns import _get_registrar_config_section
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import urllib.parse
|
||||
|
@ -27,7 +28,7 @@ import shutil
|
|||
import ast
|
||||
import operator as op
|
||||
from collections import OrderedDict
|
||||
from typing import Optional, Dict, List, Union, Any, Mapping
|
||||
from typing import Optional, Dict, List, Union, Any, Mapping, Callable
|
||||
|
||||
from moulinette.interfaces.cli import colorize
|
||||
from moulinette import Moulinette, m18n
|
||||
|
@ -189,13 +190,45 @@ def evaluate_simple_js_expression(expr, context={}):
|
|||
|
||||
|
||||
class ConfigPanel:
|
||||
def __init__(self, config_path, save_path=None):
|
||||
entity_type = "config"
|
||||
save_path_tpl = None
|
||||
config_path_tpl = "/usr/share/yunohost/other/config_{entity}.toml"
|
||||
|
||||
@classmethod
|
||||
def list(cls):
|
||||
"""
|
||||
List available config panel
|
||||
"""
|
||||
try:
|
||||
entities = [re.match("^" + cls.save_path_tpl.format(entity="(?p<entity>)") + "$", f).group('entity')
|
||||
for f in glob.glob(cls.save_path_tpl.format(entity="*"))
|
||||
if os.path.isfile(f)]
|
||||
except FileNotFoundError:
|
||||
entities = []
|
||||
return entities
|
||||
|
||||
def __init__(self, entity, config_path=None, save_path=None, creation=False):
|
||||
self.entity = entity
|
||||
self.config_path = config_path
|
||||
if not config_path:
|
||||
self.config_path = self.config_path_tpl.format(entity=entity)
|
||||
self.save_path = save_path
|
||||
if not save_path and self.save_path_tpl:
|
||||
self.save_path = self.save_path_tpl.format(entity=entity)
|
||||
self.config = {}
|
||||
self.values = {}
|
||||
self.new_values = {}
|
||||
|
||||
if self.save_path and not creation and not os.path.exists(self.save_path):
|
||||
raise YunohostError(f"{self.entity_type}_doesnt_exists", name=entity)
|
||||
if self.save_path and creation and os.path.exists(self.save_path):
|
||||
raise YunohostError(f"{self.entity_type}_already_exists", name=entity)
|
||||
|
||||
# Search for hooks in the config panel
|
||||
self.hooks = {func: getattr(self, func)
|
||||
for func in dir(self)
|
||||
if callable(getattr(self, func)) and re.match("^(validate|post_ask)__", func)}
|
||||
|
||||
def get(self, key="", mode="classic"):
|
||||
self.filter_key = key or ""
|
||||
|
||||
|
@ -274,19 +307,12 @@ class ConfigPanel:
|
|||
|
||||
# Import and parse pre-answered options
|
||||
logger.debug("Import and parse pre-answered options")
|
||||
args = urllib.parse.parse_qs(args or "", keep_blank_values=True)
|
||||
self.args = {key: ",".join(value_) for key, value_ in args.items()}
|
||||
|
||||
if args_file:
|
||||
# Import YAML / JSON file but keep --args values
|
||||
self.args = {**read_yaml(args_file), **self.args}
|
||||
|
||||
if value is not None:
|
||||
self.args = {self.filter_key.split(".")[-1]: value}
|
||||
self._parse_pre_answered(args, value, args_file)
|
||||
|
||||
# Read or get values and hydrate the config
|
||||
self._load_current_values()
|
||||
self._hydrate()
|
||||
Question.operation_logger = operation_logger
|
||||
self._ask()
|
||||
|
||||
if operation_logger:
|
||||
|
@ -525,7 +551,12 @@ class ConfigPanel:
|
|||
display_header(f"\n# {name}")
|
||||
|
||||
# Check and ask unanswered questions
|
||||
questions = ask_questions_and_parse_answers(section["options"], self.args)
|
||||
questions = ask_questions_and_parse_answers(
|
||||
section["options"],
|
||||
prefilled_answers=self.args,
|
||||
current_values=self.values,
|
||||
hooks=self.hooks
|
||||
)
|
||||
self.new_values.update(
|
||||
{
|
||||
question.name: question.value
|
||||
|
@ -543,20 +574,42 @@ class ConfigPanel:
|
|||
if "default" in option
|
||||
}
|
||||
|
||||
@property
|
||||
def future_values(self): # TODO put this in ConfigPanel ?
|
||||
return {**self.values, **self.new_values}
|
||||
|
||||
def __getattr__(self, name):
|
||||
if "new_values" in self.__dict__ and name in self.new_values:
|
||||
return self.new_values[name]
|
||||
|
||||
if "values" in self.__dict__ and name in self.values:
|
||||
return self.values[name]
|
||||
|
||||
return self.__dict__[name]
|
||||
|
||||
def _load_current_values(self):
|
||||
"""
|
||||
Retrieve entries in YAML file
|
||||
And set default values if needed
|
||||
"""
|
||||
|
||||
# Retrieve entries in the YAML
|
||||
on_disk_settings = {}
|
||||
if os.path.exists(self.save_path) and os.path.isfile(self.save_path):
|
||||
on_disk_settings = read_yaml(self.save_path) or {}
|
||||
|
||||
# Inject defaults if needed (using the magic .update() ;))
|
||||
self.values = self._get_default_values()
|
||||
self.values.update(on_disk_settings)
|
||||
|
||||
# Retrieve entries in the YAML
|
||||
if os.path.exists(self.save_path) and os.path.isfile(self.save_path):
|
||||
self.values.update(read_yaml(self.save_path) or {})
|
||||
|
||||
def _parse_pre_answered(self, args, value, args_file):
|
||||
args = urllib.parse.parse_qs(args or "", keep_blank_values=True)
|
||||
self.args = {key: ",".join(value_) for key, value_ in args.items()}
|
||||
|
||||
if args_file:
|
||||
# Import YAML / JSON file but keep --args values
|
||||
self.args = {**read_yaml(args_file), **self.args}
|
||||
|
||||
if value is not None:
|
||||
self.args = {self.filter_key.split(".")[-1]: value}
|
||||
|
||||
def _apply(self):
|
||||
logger.info("Saving the new configuration...")
|
||||
|
@ -564,7 +617,7 @@ class ConfigPanel:
|
|||
if not os.path.exists(dir_path):
|
||||
mkdir(dir_path, mode=0o700)
|
||||
|
||||
values_to_save = {**self.values, **self.new_values}
|
||||
values_to_save = self.future_values
|
||||
if self.save_mode == "diff":
|
||||
defaults = self._get_default_values()
|
||||
values_to_save = {
|
||||
|
@ -587,8 +640,8 @@ class ConfigPanel:
|
|||
if services_to_reload:
|
||||
logger.info("Reloading services...")
|
||||
for service in services_to_reload:
|
||||
if hasattr(self, "app"):
|
||||
service = service.replace("__APP__", self.app)
|
||||
if hasattr(self, "entity"):
|
||||
service = service.replace("__APP__", self.entity)
|
||||
service_reload_or_restart(service)
|
||||
|
||||
def _iterate(self, trigger=["option"]):
|
||||
|
@ -607,13 +660,16 @@ class Question(object):
|
|||
hide_user_input_in_prompt = False
|
||||
pattern: Optional[Dict] = None
|
||||
|
||||
def __init__(self, question: Dict[str, Any], context: Mapping[str, Any] = {}):
|
||||
def __init__(self, question: Dict[str, Any],
|
||||
context: Mapping[str, Any] = {},
|
||||
hooks: Dict[str, Callable] = {}):
|
||||
self.name = question["name"]
|
||||
self.context = context
|
||||
self.hooks = hooks
|
||||
self.type = question.get("type", "string")
|
||||
self.default = question.get("default", None)
|
||||
self.optional = question.get("optional", False)
|
||||
self.visible = question.get("visible", None)
|
||||
self.context = context
|
||||
self.choices = question.get("choices", [])
|
||||
self.pattern = question.get("pattern", self.pattern)
|
||||
self.ask = question.get("ask", {"en": self.name})
|
||||
|
@ -623,6 +679,8 @@ class Question(object):
|
|||
self.current_value = question.get("current_value")
|
||||
# .value is the "proposed" value which we got from the user
|
||||
self.value = question.get("value")
|
||||
# Use to return several values in case answer is in mutipart
|
||||
self.values = {}
|
||||
|
||||
# Empty value is parsed as empty string
|
||||
if self.default == "":
|
||||
|
@ -663,8 +721,8 @@ class Question(object):
|
|||
# - we doesn't want to give a specific value
|
||||
# - we want to keep the previous value
|
||||
# - we want the default value
|
||||
self.value = None
|
||||
return self.value
|
||||
self.value = self.values[self.name] = None
|
||||
return self.values
|
||||
|
||||
for i in range(5):
|
||||
# Display question if no value filled or if it's a readonly message
|
||||
|
@ -698,9 +756,18 @@ class Question(object):
|
|||
|
||||
break
|
||||
|
||||
self.value = self._post_parse_value()
|
||||
self.value = self.values[self.name] = self._post_parse_value()
|
||||
|
||||
<<<<<<< HEAD
|
||||
return self.value
|
||||
=======
|
||||
# Search for post actions in hooks
|
||||
post_hook = f"post_ask__{self.name}"
|
||||
if post_hook in self.hooks:
|
||||
self.values.update(self.hooks[post_hook](self))
|
||||
|
||||
return self.values
|
||||
>>>>>>> c0cd8dbf... [enh] config panel python method hook
|
||||
|
||||
def _prevalidate(self):
|
||||
if self.value in [None, ""] and not self.optional:
|
||||
|
@ -864,8 +931,10 @@ class PasswordQuestion(Question):
|
|||
default_value = ""
|
||||
forbidden_chars = "{}"
|
||||
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
super().__init__(question, context)
|
||||
def __init__(self, question,
|
||||
context: Mapping[str, Any] = {},
|
||||
hooks: Dict[str, Callable] = {}):
|
||||
super().__init__(question, context, hooks)
|
||||
self.redact = True
|
||||
if self.default is not None:
|
||||
raise YunohostValidationError(
|
||||
|
@ -983,8 +1052,9 @@ class BooleanQuestion(Question):
|
|||
choices="yes/no",
|
||||
)
|
||||
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
super().__init__(question, context)
|
||||
def __init__(self, question, context: Mapping[str, Any] = {},
|
||||
hooks: Dict[str, Callable] = {}):
|
||||
super().__init__(question, context, hooks)
|
||||
self.yes = question.get("yes", 1)
|
||||
self.no = question.get("no", 0)
|
||||
if self.default is None:
|
||||
|
@ -1004,10 +1074,11 @@ class BooleanQuestion(Question):
|
|||
class DomainQuestion(Question):
|
||||
argument_type = "domain"
|
||||
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
def __init__(self, question, context: Mapping[str, Any] = {},
|
||||
hooks: Dict[str, Callable] = {}):
|
||||
from yunohost.domain import domain_list, _get_maindomain
|
||||
|
||||
super().__init__(question, context)
|
||||
super().__init__(question, context, hooks)
|
||||
|
||||
if self.default is None:
|
||||
self.default = _get_maindomain()
|
||||
|
@ -1030,11 +1101,12 @@ class DomainQuestion(Question):
|
|||
class UserQuestion(Question):
|
||||
argument_type = "user"
|
||||
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
def __init__(self, question, context: Mapping[str, Any] = {},
|
||||
hooks: Dict[str, Callable] = {}):
|
||||
from yunohost.user import user_list, user_info
|
||||
from yunohost.domain import _get_maindomain
|
||||
|
||||
super().__init__(question, context)
|
||||
super().__init__(question, context, hooks)
|
||||
self.choices = list(user_list()["users"].keys())
|
||||
|
||||
if not self.choices:
|
||||
|
@ -1056,8 +1128,9 @@ class NumberQuestion(Question):
|
|||
argument_type = "number"
|
||||
default_value = None
|
||||
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
super().__init__(question, context)
|
||||
def __init__(self, question, context: Mapping[str, Any] = {},
|
||||
hooks: Dict[str, Callable] = {}):
|
||||
super().__init__(question, context, hooks)
|
||||
self.min = question.get("min", None)
|
||||
self.max = question.get("max", None)
|
||||
self.step = question.get("step", None)
|
||||
|
@ -1108,8 +1181,9 @@ class DisplayTextQuestion(Question):
|
|||
argument_type = "display_text"
|
||||
readonly = True
|
||||
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
super().__init__(question, context)
|
||||
def __init__(self, question, context: Mapping[str, Any] = {},
|
||||
hooks: Dict[str, Callable] = {}):
|
||||
super().__init__(question, context, hooks)
|
||||
|
||||
self.optional = True
|
||||
self.style = question.get(
|
||||
|
@ -1143,8 +1217,9 @@ class FileQuestion(Question):
|
|||
if os.path.exists(upload_dir):
|
||||
shutil.rmtree(upload_dir)
|
||||
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
super().__init__(question, context)
|
||||
def __init__(self, question, context: Mapping[str, Any] = {},
|
||||
hooks: Dict[str, Callable] = {}):
|
||||
super().__init__(question, context, hooks)
|
||||
self.accept = question.get("accept", "")
|
||||
|
||||
def _prevalidate(self):
|
||||
|
@ -1214,7 +1289,14 @@ ARGUMENTS_TYPE_PARSERS = {
|
|||
|
||||
|
||||
def ask_questions_and_parse_answers(
|
||||
<<<<<<< HEAD
|
||||
raw_questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {}
|
||||
=======
|
||||
raw_questions: Dict,
|
||||
prefilled_answers: Union[str, Mapping[str, Any]] = {},
|
||||
current_values: Union[str, Mapping[str, Any]] = {},
|
||||
hooks: Dict[str, Callable[[], None]] = {}
|
||||
>>>>>>> c0cd8dbf... [enh] config panel python method hook
|
||||
) -> List[Question]:
|
||||
"""Parse arguments store in either manifest.json or actions.json or from a
|
||||
config panel against the user answers when they are present.
|
||||
|
@ -1241,13 +1323,24 @@ def ask_questions_and_parse_answers(
|
|||
else:
|
||||
answers = {}
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
context = {**current_values, **answers}
|
||||
>>>>>>> c0cd8dbf... [enh] config panel python method hook
|
||||
out = []
|
||||
|
||||
for raw_question in raw_questions:
|
||||
question_class = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")]
|
||||
raw_question["value"] = answers.get(raw_question["name"])
|
||||
<<<<<<< HEAD
|
||||
question = question_class(raw_question, context=answers)
|
||||
answers[question.name] = question.ask_if_needed()
|
||||
=======
|
||||
question = question_class(raw_question, context=context, hooks=hooks)
|
||||
new_values = question.ask_if_needed()
|
||||
answers.update(new_values)
|
||||
context.update(new_values)
|
||||
>>>>>>> c0cd8dbf... [enh] config panel python method hook
|
||||
out.append(question)
|
||||
|
||||
return out
|
||||
|
|
Loading…
Add table
Reference in a new issue