This commit is contained in:
Alexandre Aubin 2021-09-04 18:39:39 +02:00
parent cc8247acfd
commit 0a52430186
4 changed files with 208 additions and 159 deletions

View file

@ -6,6 +6,7 @@ from yunohost.app import app_list
from yunohost.diagnosis import Diagnoser from yunohost.diagnosis import Diagnoser
class AppDiagnoser(Diagnoser): class AppDiagnoser(Diagnoser):
id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
@ -30,13 +31,17 @@ class AppDiagnoser(Diagnoser):
if not app["issues"]: if not app["issues"]:
continue continue
level = "ERROR" if any(issue[0] == "error" for issue in app["issues"]) else "WARNING" level = (
"ERROR"
if any(issue[0] == "error" for issue in app["issues"])
else "WARNING"
)
yield dict( yield dict(
meta={"test": "apps", "app": app["name"]}, meta={"test": "apps", "app": app["name"]},
status=level, status=level,
summary="diagnosis_apps_issue", summary="diagnosis_apps_issue",
details=[issue[1] for issue in app["issues"]] details=[issue[1] for issue in app["issues"]],
) )
def issues(self, app): def issues(self, app):
@ -45,14 +50,19 @@ class AppDiagnoser(Diagnoser):
if not app.get("from_catalog") or app["from_catalog"].get("state") != "working": if not app.get("from_catalog") or app["from_catalog"].get("state") != "working":
yield ("error", "diagnosis_apps_not_in_app_catalog") yield ("error", "diagnosis_apps_not_in_app_catalog")
elif not isinstance(app["from_catalog"].get("level"), int) or app["from_catalog"]["level"] == 0: elif (
not isinstance(app["from_catalog"].get("level"), int)
or app["from_catalog"]["level"] == 0
):
yield ("error", "diagnosis_apps_broken") yield ("error", "diagnosis_apps_broken")
elif app["from_catalog"]["level"] <= 4: elif app["from_catalog"]["level"] <= 4:
yield ("warning", "diagnosis_apps_bad_quality") yield ("warning", "diagnosis_apps_bad_quality")
# Check for super old, deprecated practices # Check for super old, deprecated practices
yunohost_version_req = app["manifest"].get("requirements", {}).get("yunohost", "").strip(">= ") yunohost_version_req = (
app["manifest"].get("requirements", {}).get("yunohost", "").strip(">= ")
)
if yunohost_version_req.startswith("2."): if yunohost_version_req.startswith("2."):
yield ("error", "diagnosis_apps_outdated_ynh_requirement") yield ("error", "diagnosis_apps_outdated_ynh_requirement")
@ -64,11 +74,21 @@ class AppDiagnoser(Diagnoser):
"yunohost tools port-available", "yunohost tools port-available",
] ]
for deprecated_helper in deprecated_helpers: for deprecated_helper in deprecated_helpers:
if os.system(f"grep -nr -q '{deprecated_helper}' {app['setting_path']}/scripts/") == 0: if (
os.system(
f"grep -nr -q '{deprecated_helper}' {app['setting_path']}/scripts/"
)
== 0
):
yield ("error", "diagnosis_apps_deprecated_practices") yield ("error", "diagnosis_apps_deprecated_practices")
old_arg_regex = r'^domain=\${?[0-9]' old_arg_regex = r"^domain=\${?[0-9]"
if os.system(f"grep -q '{old_arg_regex}' {app['setting_path']}/scripts/install") == 0: if (
os.system(
f"grep -q '{old_arg_regex}' {app['setting_path']}/scripts/install"
)
== 0
):
yield ("error", "diagnosis_apps_deprecated_practices") yield ("error", "diagnosis_apps_deprecated_practices")

View file

@ -55,7 +55,11 @@ from moulinette.utils.filesystem import (
from yunohost.service import service_status, _run_service_command from yunohost.service import service_status, _run_service_command
from yunohost.utils import packages, config from yunohost.utils import packages, config
from yunohost.utils.config import ConfigPanel, parse_args_in_yunohost_format, YunoHostArgumentFormatParser from yunohost.utils.config import (
ConfigPanel,
parse_args_in_yunohost_format,
YunoHostArgumentFormatParser,
)
from yunohost.utils.i18n import _value_for_locale from yunohost.utils.i18n import _value_for_locale
from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.filesystem import free_space_in_directory from yunohost.utils.filesystem import free_space_in_directory
@ -1756,7 +1760,7 @@ def app_action_run(operation_logger, app, action, args=None):
return logger.success("Action successed!") return logger.success("Action successed!")
def app_config_get(app, key='', mode='classic'): def app_config_get(app, key="", mode="classic"):
""" """
Display an app configuration in classic, full or export mode Display an app configuration in classic, full or export mode
""" """
@ -1765,7 +1769,9 @@ def app_config_get(app, key='', mode='classic'):
@is_unit_operation() @is_unit_operation()
def app_config_set(operation_logger, app, key=None, value=None, args=None, args_file=None): def app_config_set(
operation_logger, app, key=None, value=None, args=None, args_file=None
):
""" """
Apply a new app configuration Apply a new app configuration
""" """
@ -1780,6 +1786,7 @@ def app_config_set(operation_logger, app, key=None, value=None, args=None, args_
operation_logger.success() operation_logger.success()
return result return result
class AppConfigPanel(ConfigPanel): class AppConfigPanel(ConfigPanel):
def __init__(self, app): def __init__(self, app):
@ -1791,10 +1798,10 @@ class AppConfigPanel(ConfigPanel):
super().__init__(config_path=config_path) super().__init__(config_path=config_path)
def _load_current_values(self): def _load_current_values(self):
self.values = self._call_config_script('show') self.values = self._call_config_script("show")
def _apply(self): def _apply(self):
self.errors = self._call_config_script('apply', self.new_values) self.errors = self._call_config_script("apply", self.new_values)
def _call_config_script(self, action, env={}): def _call_config_script(self, action, env={}):
from yunohost.hook import hook_exec from yunohost.hook import hook_exec
@ -1814,22 +1821,23 @@ ynh_app_config_run $1
# Call config script to extract current values # Call config script to extract current values
logger.debug(f"Calling '{action}' action from config script") 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.app)
env.update({ env.update(
"app_id": app_id, {
"app": self.app, "app_id": app_id,
"app_instance_nb": str(app_instance_nb), "app": self.app,
}) "app_instance_nb": str(app_instance_nb),
}
ret, values = hook_exec(
config_script, args=[action], env=env
) )
ret, values = hook_exec(config_script, args=[action], env=env)
if ret != 0: if ret != 0:
if action == 'show': if action == "show":
raise YunohostError("app_config_unable_to_read_values") raise YunohostError("app_config_unable_to_read_values")
else: else:
raise YunohostError("app_config_unable_to_apply_values_correctly") raise YunohostError("app_config_unable_to_apply_values_correctly")
return values return values
def _get_all_installed_apps_id(): def _get_all_installed_apps_id():
""" """
Return something like: Return something like:
@ -2455,9 +2463,6 @@ def _parse_args_for_action(action, args={}):
return parse_args_in_yunohost_format(args, action_args) return parse_args_in_yunohost_format(args, action_args)
def _validate_and_normalize_webpath(args_dict, app_folder): def _validate_and_normalize_webpath(args_dict, app_folder):
# If there's only one "domain" and "path", validate that domain/path # If there's only one "domain" and "path", validate that domain/path

View file

@ -45,8 +45,8 @@ from yunohost.utils.error import YunohostError, YunohostValidationError
logger = getActionLogger("yunohost.config") logger = getActionLogger("yunohost.config")
CONFIG_PANEL_VERSION_SUPPORTED = 1.0 CONFIG_PANEL_VERSION_SUPPORTED = 1.0
class ConfigPanel:
class ConfigPanel:
def __init__(self, config_path, save_path=None): def __init__(self, config_path, save_path=None):
self.config_path = config_path self.config_path = config_path
self.save_path = save_path self.save_path = save_path
@ -54,8 +54,8 @@ class ConfigPanel:
self.values = {} self.values = {}
self.new_values = {} self.new_values = {}
def get(self, key='', mode='classic'): def get(self, key="", mode="classic"):
self.filter_key = key or '' self.filter_key = key or ""
# Read config panel toml # Read config panel toml
self._get_config_panel() self._get_config_panel()
@ -68,12 +68,12 @@ class ConfigPanel:
self._hydrate() self._hydrate()
# Format result in full mode # Format result in full mode
if mode == 'full': if mode == "full":
return self.config return self.config
# In 'classic' mode, we display the current value if key refer to an option # In 'classic' mode, we display the current value if key refer to an option
if self.filter_key.count('.') == 2 and mode == 'classic': if self.filter_key.count(".") == 2 and mode == "classic":
option = self.filter_key.split('.')[-1] option = self.filter_key.split(".")[-1]
return self.values.get(option, None) return self.values.get(option, None)
# Format result in 'classic' or 'export' mode # Format result in 'classic' or 'export' mode
@ -81,17 +81,17 @@ class ConfigPanel:
result = {} result = {}
for panel, section, option in self._iterate(): for panel, section, option in self._iterate():
key = f"{panel['id']}.{section['id']}.{option['id']}" key = f"{panel['id']}.{section['id']}.{option['id']}"
if mode == 'export': if mode == "export":
result[option['id']] = option.get('current_value') result[option["id"]] = option.get("current_value")
else: else:
result[key] = { 'ask': _value_for_locale(option['ask']) } result[key] = {"ask": _value_for_locale(option["ask"])}
if 'current_value' in option: if "current_value" in option:
result[key]['value'] = option['current_value'] result[key]["value"] = option["current_value"]
return result return result
def set(self, key=None, value=None, args=None, args_file=None): def set(self, key=None, value=None, args=None, args_file=None):
self.filter_key = key or '' self.filter_key = key or ""
# Read config panel toml # Read config panel toml
self._get_config_panel() self._get_config_panel()
@ -102,20 +102,20 @@ class ConfigPanel:
if (args is not None or args_file is not None) and value is not None: if (args is not None or args_file is not None) and value is not None:
raise YunohostError("config_args_value") raise YunohostError("config_args_value")
if self.filter_key.count('.') != 2 and not value is None: if self.filter_key.count(".") != 2 and not value is None:
raise YunohostError("config_set_value_on_section") raise YunohostError("config_set_value_on_section")
# Import and parse pre-answered options # Import and parse pre-answered options
logger.debug("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) args = urllib.parse.parse_qs(args or "", keep_blank_values=True)
self.args = { key: ','.join(value_) for key, value_ in args.items() } self.args = {key: ",".join(value_) for key, value_ in args.items()}
if args_file: if args_file:
# Import YAML / JSON file but keep --args values # Import YAML / JSON file but keep --args values
self.args = { **read_yaml(args_file), **self.args } self.args = {**read_yaml(args_file), **self.args}
if value is not None: if value is not None:
self.args = {self.filter_key.split('.')[-1]: value} self.args = {self.filter_key.split(".")[-1]: value}
# Read or get values and hydrate the config # Read or get values and hydrate the config
self._load_current_values() self._load_current_values()
@ -155,10 +155,9 @@ class ConfigPanel:
def _get_toml(self): def _get_toml(self):
return read_toml(self.config_path) return read_toml(self.config_path)
def _get_config_panel(self): def _get_config_panel(self):
# Split filter_key # Split filter_key
filter_key = dict(enumerate(self.filter_key.split('.'))) filter_key = dict(enumerate(self.filter_key.split(".")))
if len(filter_key) > 3: if len(filter_key) > 3:
raise YunohostError("config_too_much_sub_keys") raise YunohostError("config_too_much_sub_keys")
@ -174,20 +173,18 @@ class ConfigPanel:
# Transform toml format into internal format # Transform toml format into internal format
defaults = { defaults = {
'toml': { "toml": {"version": 1.0},
'version': 1.0 "panels": {
}, "name": "",
'panels': { "services": [],
'name': '', "actions": {"apply": {"en": "Apply"}},
'services': [], }, # help
'actions': {'apply': {'en': 'Apply'}} "sections": {
}, # help "name": "",
'sections': { "services": [],
'name': '', "optional": True,
'services': [], }, # visibleIf help
'optional': True "options": {}
}, # visibleIf help
'options': {}
# ask type source help helpLink example style icon placeholder visibleIf # ask type source help helpLink example style icon placeholder visibleIf
# optional choices pattern limit min max step accept redact # optional choices pattern limit min max step accept redact
} }
@ -207,30 +204,36 @@ class ConfigPanel:
# Define the filter_key part to use and the children type # Define the filter_key part to use and the children type
i = list(defaults).index(node_type) i = list(defaults).index(node_type)
search_key = filter_key.get(i) search_key = filter_key.get(i)
subnode_type = list(defaults)[i+1] if node_type != 'options' else None subnode_type = list(defaults)[i + 1] if node_type != "options" else None
for key, value in toml_node.items(): for key, value in toml_node.items():
# Key/value are a child node # Key/value are a child node
if isinstance(value, OrderedDict) and key not in default and subnode_type: if (
isinstance(value, OrderedDict)
and key not in default
and subnode_type
):
# We exclude all nodes not referenced by the filter_key # We exclude all nodes not referenced by the filter_key
if search_key and key != search_key: if search_key and key != search_key:
continue continue
subnode = convert(value, subnode_type) subnode = convert(value, subnode_type)
subnode['id'] = key subnode["id"] = key
if node_type == 'sections': if node_type == "sections":
subnode['name'] = key # legacy subnode["name"] = key # legacy
subnode.setdefault('optional', toml_node.get('optional', True)) subnode.setdefault("optional", toml_node.get("optional", True))
node.setdefault(subnode_type, []).append(subnode) node.setdefault(subnode_type, []).append(subnode)
# Key/value are a property # Key/value are a property
else: else:
# Todo search all i18n keys # Todo search all i18n keys
node[key] = value if key not in ['ask', 'help', 'name'] else { 'en': value } node[key] = (
value if key not in ["ask", "help", "name"] else {"en": value}
)
return node return node
self.config = convert(toml_config_panel, 'toml') self.config = convert(toml_config_panel, "toml")
try: try:
self.config['panels'][0]['sections'][0]['options'][0] self.config["panels"][0]["sections"][0]["options"][0]
except (KeyError, IndexError): except (KeyError, IndexError):
raise YunohostError( raise YunohostError(
"config_empty_or_bad_filter_key", filter_key=self.filter_key "config_empty_or_bad_filter_key", filter_key=self.filter_key
@ -242,36 +245,41 @@ class ConfigPanel:
# Hydrating config panel with current value # Hydrating config panel with current value
logger.debug("Hydrating config with current values") logger.debug("Hydrating config with current values")
for _, _, option in self._iterate(): for _, _, option in self._iterate():
if option['name'] not in self.values: if option["name"] not in self.values:
continue continue
value = self.values[option['name']] value = self.values[option["name"]]
# In general, the value is just a simple value. # In general, the value is just a simple value.
# Sometimes it could be a dict used to overwrite the option itself # Sometimes it could be a dict used to overwrite the option itself
value = value if isinstance(value, dict) else {'current_value': value } value = value if isinstance(value, dict) else {"current_value": value}
option.update(value) option.update(value)
return self.values return self.values
def _ask(self): def _ask(self):
logger.debug("Ask unanswered question and prevalidate data") logger.debug("Ask unanswered question and prevalidate data")
def display_header(message): def display_header(message):
""" CLI panel/section header display """CLI panel/section header display"""
""" if Moulinette.interface.type == "cli" and self.filter_key.count(".") < 2:
if Moulinette.interface.type == 'cli' and self.filter_key.count('.') < 2: Moulinette.display(colorize(message, "purple"))
Moulinette.display(colorize(message, 'purple'))
for panel, section, obj in self._iterate(['panel', 'section']): for panel, section, obj in self._iterate(["panel", "section"]):
if panel == obj: if panel == obj:
name = _value_for_locale(panel['name']) name = _value_for_locale(panel["name"])
display_header(f"\n{'='*40}\n>>>> {name}\n{'='*40}") display_header(f"\n{'='*40}\n>>>> {name}\n{'='*40}")
continue continue
name = _value_for_locale(section['name']) name = _value_for_locale(section["name"])
display_header(f"\n# {name}") display_header(f"\n# {name}")
# Check and ask unanswered questions # Check and ask unanswered questions
self.new_values.update(parse_args_in_yunohost_format( self.new_values.update(
self.args, section['options'] parse_args_in_yunohost_format(self.args, section["options"])
)) )
self.new_values = {key: str(value[0]) for key, value in self.new_values.items() if not value[0] is None} self.new_values = {
key: str(value[0])
for key, value in self.new_values.items()
if not value[0] is None
}
def _apply(self): def _apply(self):
logger.info("Running config script...") logger.info("Running config script...")
@ -281,36 +289,34 @@ class ConfigPanel:
# Save the settings to the .yaml file # Save the settings to the .yaml file
write_to_yaml(self.save_path, self.new_values) write_to_yaml(self.save_path, self.new_values)
def _reload_services(self): def _reload_services(self):
logger.info("Reloading services...") logger.info("Reloading services...")
services_to_reload = set() services_to_reload = set()
for panel, section, obj in self._iterate(['panel', 'section', 'option']): for panel, section, obj in self._iterate(["panel", "section", "option"]):
services_to_reload |= set(obj.get('services', [])) services_to_reload |= set(obj.get("services", []))
services_to_reload = list(services_to_reload) services_to_reload = list(services_to_reload)
services_to_reload.sort(key = 'nginx'.__eq__) services_to_reload.sort(key="nginx".__eq__)
for service in services_to_reload: for service in services_to_reload:
if '__APP__': if "__APP__":
service = service.replace('__APP__', self.app) service = service.replace("__APP__", self.app)
logger.debug(f"Reloading {service}") logger.debug(f"Reloading {service}")
if not _run_service_command('reload-or-restart', service): if not _run_service_command("reload-or-restart", service):
services = _get_services() services = _get_services()
test_conf = services[service].get('test_conf', 'true') test_conf = services[service].get("test_conf", "true")
errors = check_output(f"{test_conf}; exit 0") if test_conf else '' errors = check_output(f"{test_conf}; exit 0") if test_conf else ""
raise YunohostError( raise YunohostError(
"config_failed_service_reload", "config_failed_service_reload", service=service, errors=errors
service=service, errors=errors
) )
def _iterate(self, trigger=['option']): def _iterate(self, trigger=["option"]):
for panel in self.config.get("panels", []): for panel in self.config.get("panels", []):
if 'panel' in trigger: if "panel" in trigger:
yield (panel, None, panel) yield (panel, None, panel)
for section in panel.get("sections", []): for section in panel.get("sections", []):
if 'section' in trigger: if "section" in trigger:
yield (panel, section, section) yield (panel, section, section)
if 'option' in trigger: if "option" in trigger:
for option in section.get("options", []): for option in section.get("options", []):
yield (panel, section, option) yield (panel, section, option)
@ -327,17 +333,17 @@ class YunoHostArgumentFormatParser(object):
parsed_question = Question() parsed_question = Question()
parsed_question.name = question["name"] parsed_question.name = question["name"]
parsed_question.type = question.get("type", 'string') parsed_question.type = question.get("type", "string")
parsed_question.default = question.get("default", None) parsed_question.default = question.get("default", None)
parsed_question.current_value = question.get("current_value") parsed_question.current_value = question.get("current_value")
parsed_question.optional = question.get("optional", False) parsed_question.optional = question.get("optional", False)
parsed_question.choices = question.get("choices", []) parsed_question.choices = question.get("choices", [])
parsed_question.pattern = question.get("pattern") parsed_question.pattern = question.get("pattern")
parsed_question.ask = question.get("ask", {'en': f"{parsed_question.name}"}) parsed_question.ask = question.get("ask", {"en": f"{parsed_question.name}"})
parsed_question.help = question.get("help") parsed_question.help = question.get("help")
parsed_question.helpLink = question.get("helpLink") parsed_question.helpLink = question.get("helpLink")
parsed_question.value = user_answers.get(parsed_question.name) parsed_question.value = user_answers.get(parsed_question.name)
parsed_question.redact = question.get('redact', False) parsed_question.redact = question.get("redact", False)
# Empty value is parsed as empty string # Empty value is parsed as empty string
if parsed_question.default == "": if parsed_question.default == "":
@ -350,7 +356,7 @@ class YunoHostArgumentFormatParser(object):
while True: while True:
# Display question if no value filled or if it's a readonly message # Display question if no value filled or if it's a readonly message
if Moulinette.interface.type== 'cli': if Moulinette.interface.type == "cli":
text_for_user_input_in_cli = self._format_text_for_user_input_in_cli( text_for_user_input_in_cli = self._format_text_for_user_input_in_cli(
question question
) )
@ -368,10 +374,9 @@ class YunoHostArgumentFormatParser(object):
is_password=self.hide_user_input_in_prompt, is_password=self.hide_user_input_in_prompt,
confirm=self.hide_user_input_in_prompt, confirm=self.hide_user_input_in_prompt,
prefill=prefill, prefill=prefill,
is_multiline=(question.type == "text") is_multiline=(question.type == "text"),
) )
# Apply default value # Apply default value
if question.value in [None, ""] and question.default is not None: if question.value in [None, ""] and question.default is not None:
question.value = ( question.value = (
@ -384,9 +389,9 @@ class YunoHostArgumentFormatParser(object):
try: try:
self._prevalidate(question) self._prevalidate(question)
except YunohostValidationError as e: except YunohostValidationError as e:
if Moulinette.interface.type== 'api': if Moulinette.interface.type == "api":
raise raise
Moulinette.display(str(e), 'error') Moulinette.display(str(e), "error")
question.value = None question.value = None
continue continue
break break
@ -398,17 +403,17 @@ class YunoHostArgumentFormatParser(object):
def _prevalidate(self, question): def _prevalidate(self, question):
if question.value in [None, ""] and not question.optional: if question.value in [None, ""] and not question.optional:
raise YunohostValidationError( raise YunohostValidationError("app_argument_required", name=question.name)
"app_argument_required", name=question.name
)
# we have an answer, do some post checks # we have an answer, do some post checks
if question.value is not None: if question.value is not None:
if question.choices and question.value not in question.choices: if question.choices and question.value not in question.choices:
self._raise_invalid_answer(question) self._raise_invalid_answer(question)
if question.pattern and not re.match(question.pattern['regexp'], str(question.value)): if question.pattern and not re.match(
question.pattern["regexp"], str(question.value)
):
raise YunohostValidationError( raise YunohostValidationError(
question.pattern['error'], question.pattern["error"],
name=question.name, name=question.name,
value=question.value, value=question.value,
) )
@ -434,7 +439,7 @@ class YunoHostArgumentFormatParser(object):
text_for_user_input_in_cli += _value_for_locale(question.help) text_for_user_input_in_cli += _value_for_locale(question.help)
if question.helpLink: if question.helpLink:
if not isinstance(question.helpLink, dict): if not isinstance(question.helpLink, dict):
question.helpLink = {'href': question.helpLink} question.helpLink = {"href": question.helpLink}
text_for_user_input_in_cli += f"\n - See {question.helpLink['href']}" text_for_user_input_in_cli += f"\n - See {question.helpLink['href']}"
return text_for_user_input_in_cli return text_for_user_input_in_cli
@ -467,18 +472,18 @@ class StringArgumentParser(YunoHostArgumentFormatParser):
argument_type = "string" argument_type = "string"
default_value = "" default_value = ""
class TagsArgumentParser(YunoHostArgumentFormatParser): class TagsArgumentParser(YunoHostArgumentFormatParser):
argument_type = "tags" argument_type = "tags"
def _prevalidate(self, question): def _prevalidate(self, question):
values = question.value values = question.value
for value in values.split(','): for value in values.split(","):
question.value = value question.value = value
super()._prevalidate(question) super()._prevalidate(question)
question.value = values question.value = values
class PasswordArgumentParser(YunoHostArgumentFormatParser): class PasswordArgumentParser(YunoHostArgumentFormatParser):
hide_user_input_in_prompt = True hide_user_input_in_prompt = True
argument_type = "password" argument_type = "password"
@ -522,9 +527,7 @@ class BooleanArgumentParser(YunoHostArgumentFormatParser):
default_value = False default_value = False
def parse_question(self, question, user_answers): def parse_question(self, question, user_answers):
question = super().parse_question( question = super().parse_question(question, user_answers)
question, user_answers
)
if question.default is None: if question.default is None:
question.default = False question.default = False
@ -616,11 +619,9 @@ class NumberArgumentParser(YunoHostArgumentFormatParser):
default_value = "" default_value = ""
def parse_question(self, question, user_answers): def parse_question(self, question, user_answers):
question_parsed = super().parse_question( question_parsed = super().parse_question(question, user_answers)
question, user_answers question_parsed.min = question.get("min", None)
) question_parsed.max = question.get("max", None)
question_parsed.min = question.get('min', None)
question_parsed.max = question.get('max', None)
if question_parsed.default is None: if question_parsed.default is None:
question_parsed.default = 0 question_parsed.default = 0
@ -628,19 +629,27 @@ class NumberArgumentParser(YunoHostArgumentFormatParser):
def _prevalidate(self, question): def _prevalidate(self, question):
super()._prevalidate(question) super()._prevalidate(question)
if not isinstance(question.value, int) and not (isinstance(question.value, str) and question.value.isdigit()): if not isinstance(question.value, int) and not (
isinstance(question.value, str) and question.value.isdigit()
):
raise YunohostValidationError( raise YunohostValidationError(
"app_argument_invalid", field=question.name, error=m18n.n("invalid_number") "app_argument_invalid",
field=question.name,
error=m18n.n("invalid_number"),
) )
if question.min is not None and int(question.value) < question.min: if question.min is not None and int(question.value) < question.min:
raise YunohostValidationError( raise YunohostValidationError(
"app_argument_invalid", field=question.name, error=m18n.n("invalid_number") "app_argument_invalid",
field=question.name,
error=m18n.n("invalid_number"),
) )
if question.max is not None and int(question.value) > question.max: if question.max is not None and int(question.value) > question.max:
raise YunohostValidationError( raise YunohostValidationError(
"app_argument_invalid", field=question.name, error=m18n.n("invalid_number") "app_argument_invalid",
field=question.name,
error=m18n.n("invalid_number"),
) )
def _post_parse_value(self, question): def _post_parse_value(self, question):
@ -660,29 +669,28 @@ class DisplayTextArgumentParser(YunoHostArgumentFormatParser):
readonly = True readonly = True
def parse_question(self, question, user_answers): def parse_question(self, question, user_answers):
question_parsed = super().parse_question( question_parsed = super().parse_question(question, user_answers)
question, user_answers
)
question_parsed.optional = True question_parsed.optional = True
question_parsed.style = question.get('style', 'info') question_parsed.style = question.get("style", "info")
return question_parsed return question_parsed
def _format_text_for_user_input_in_cli(self, question): def _format_text_for_user_input_in_cli(self, question):
text = question.ask['en'] text = question.ask["en"]
if question.style in ['success', 'info', 'warning', 'danger']: if question.style in ["success", "info", "warning", "danger"]:
color = { color = {
'success': 'green', "success": "green",
'info': 'cyan', "info": "cyan",
'warning': 'yellow', "warning": "yellow",
'danger': 'red' "danger": "red",
} }
return colorize(m18n.g(question.style), color[question.style]) + f" {text}" return colorize(m18n.g(question.style), color[question.style]) + f" {text}"
else: else:
return text return text
class FileArgumentParser(YunoHostArgumentFormatParser): class FileArgumentParser(YunoHostArgumentFormatParser):
argument_type = "file" argument_type = "file"
upload_dirs = [] upload_dirs = []
@ -690,60 +698,77 @@ class FileArgumentParser(YunoHostArgumentFormatParser):
@classmethod @classmethod
def clean_upload_dirs(cls): def clean_upload_dirs(cls):
# Delete files uploaded from API # Delete files uploaded from API
if Moulinette.interface.type== 'api': if Moulinette.interface.type == "api":
for upload_dir in cls.upload_dirs: for upload_dir in cls.upload_dirs:
if os.path.exists(upload_dir): if os.path.exists(upload_dir):
shutil.rmtree(upload_dir) shutil.rmtree(upload_dir)
def parse_question(self, question, user_answers): def parse_question(self, question, user_answers):
question_parsed = super().parse_question( question_parsed = super().parse_question(question, user_answers)
question, user_answers if question.get("accept"):
) question_parsed.accept = question.get("accept").replace(" ", "").split(",")
if question.get('accept'):
question_parsed.accept = question.get('accept').replace(' ', '').split(',')
else: else:
question_parsed.accept = [] question_parsed.accept = []
if Moulinette.interface.type== 'api': if Moulinette.interface.type == "api":
if user_answers.get(f"{question_parsed.name}[name]"): if user_answers.get(f"{question_parsed.name}[name]"):
question_parsed.value = { question_parsed.value = {
'content': question_parsed.value, "content": question_parsed.value,
'filename': user_answers.get(f"{question_parsed.name}[name]", question_parsed.name), "filename": user_answers.get(
f"{question_parsed.name}[name]", question_parsed.name
),
} }
# If path file are the same # If path file are the same
if question_parsed.value and str(question_parsed.value) == question_parsed.current_value: if (
question_parsed.value
and str(question_parsed.value) == question_parsed.current_value
):
question_parsed.value = None question_parsed.value = None
return question_parsed return question_parsed
def _prevalidate(self, question): def _prevalidate(self, question):
super()._prevalidate(question) super()._prevalidate(question)
if isinstance(question.value, str) and question.value and not os.path.exists(question.value): if (
isinstance(question.value, str)
and question.value
and not os.path.exists(question.value)
):
raise YunohostValidationError( raise YunohostValidationError(
"app_argument_invalid", field=question.name, error=m18n.n("invalid_number1") "app_argument_invalid",
field=question.name,
error=m18n.n("invalid_number1"),
) )
if question.value in [None, ''] or not question.accept: if question.value in [None, ""] or not question.accept:
return return
filename = question.value if isinstance(question.value, str) else question.value['filename'] filename = (
if '.' not in filename or '.' + filename.split('.')[-1] not in question.accept: question.value
if isinstance(question.value, str)
else question.value["filename"]
)
if "." not in filename or "." + filename.split(".")[-1] not in question.accept:
raise YunohostValidationError( raise YunohostValidationError(
"app_argument_invalid", field=question.name, error=m18n.n("invalid_number2") "app_argument_invalid",
field=question.name,
error=m18n.n("invalid_number2"),
) )
def _post_parse_value(self, question): def _post_parse_value(self, question):
from base64 import b64decode from base64 import b64decode
# Upload files from API # Upload files from API
# A file arg contains a string with "FILENAME:BASE64_CONTENT" # A file arg contains a string with "FILENAME:BASE64_CONTENT"
if not question.value: if not question.value:
return question.value return question.value
if Moulinette.interface.type== 'api': if Moulinette.interface.type == "api":
upload_dir = tempfile.mkdtemp(prefix='tmp_configpanel_') upload_dir = tempfile.mkdtemp(prefix="tmp_configpanel_")
FileArgumentParser.upload_dirs += [upload_dir] FileArgumentParser.upload_dirs += [upload_dir]
filename = question.value['filename'] filename = question.value["filename"]
logger.debug(f"Save uploaded file {question.value['filename']} from API into {upload_dir}") logger.debug(
f"Save uploaded file {question.value['filename']} from API into {upload_dir}"
)
# Filename is given by user of the API. For security reason, we have replaced # Filename is given by user of the API. For security reason, we have replaced
# os.path.join to avoid the user to be able to rewrite a file in filesystem # os.path.join to avoid the user to be able to rewrite a file in filesystem
@ -755,9 +780,9 @@ class FileArgumentParser(YunoHostArgumentFormatParser):
while os.path.exists(file_path): while os.path.exists(file_path):
file_path = os.path.normpath(upload_dir + "/" + filename + (".%d" % i)) file_path = os.path.normpath(upload_dir + "/" + filename + (".%d" % i))
i += 1 i += 1
content = question.value['content'] content = question.value["content"]
try: try:
with open(file_path, 'wb') as f: with open(file_path, "wb") as f:
f.write(b64decode(content)) f.write(b64decode(content))
except IOError as e: except IOError as e:
raise YunohostError("cannot_write_file", file=file_path, error=str(e)) raise YunohostError("cannot_write_file", file=file_path, error=str(e))
@ -790,6 +815,7 @@ ARGUMENTS_TYPE_PARSERS = {
"file": FileArgumentParser, "file": FileArgumentParser,
} }
def parse_args_in_yunohost_format(user_answers, argument_questions): def parse_args_in_yunohost_format(user_answers, argument_questions):
"""Parse arguments store in either manifest.json or actions.json or from a """Parse arguments store in either manifest.json or actions.json or from a
config panel against the user answers when they are present. config panel against the user answers when they are present.
@ -811,4 +837,3 @@ def parse_args_in_yunohost_format(user_answers, argument_questions):
parsed_answers_dict[question["name"]] = answer parsed_answers_dict[question["name"]] = answer
return parsed_answers_dict return parsed_answers_dict

View file

@ -20,6 +20,7 @@
""" """
from moulinette import Moulinette, m18n from moulinette import Moulinette, m18n
def _value_for_locale(values): def _value_for_locale(values):
""" """
Return proper value for current locale Return proper value for current locale
@ -42,5 +43,3 @@ def _value_for_locale(values):
# Fallback to first value # Fallback to first value
return list(values.values())[0] return list(values.values())[0]