mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Multiline, file, tags management + prefilled cli
This commit is contained in:
parent
bc725e9768
commit
f1e5309d40
3 changed files with 177 additions and 86 deletions
|
@ -838,8 +838,8 @@ app:
|
|||
arguments:
|
||||
app:
|
||||
help: App name
|
||||
panel:
|
||||
help: Select a specific panel
|
||||
key:
|
||||
help: Select a specific panel, section or a question
|
||||
nargs: '?'
|
||||
-f:
|
||||
full: --full
|
||||
|
|
|
@ -96,51 +96,72 @@ ynh_value_set() {
|
|||
|
||||
_ynh_panel_get() {
|
||||
# From settings
|
||||
local params_sources
|
||||
params_sources=`python3 << EOL
|
||||
local lines
|
||||
lines=`python3 << EOL
|
||||
import toml
|
||||
from collections import OrderedDict
|
||||
with open("../config_panel.toml", "r") as f:
|
||||
file_content = f.read()
|
||||
loaded_toml = toml.loads(file_content, _dict=OrderedDict)
|
||||
|
||||
for panel_name,panel in loaded_toml.items():
|
||||
if isinstance(panel, dict):
|
||||
for section_name, section in panel.items():
|
||||
if isinstance(section, dict):
|
||||
for name, param in section.items():
|
||||
if isinstance(param, dict) and param.get('type', 'string') not in ['success', 'info', 'warning', 'danger', 'display_text', 'markdown']:
|
||||
print("%s=%s" % (name, param.get('source', 'settings')))
|
||||
for panel_name, panel in loaded_toml.items():
|
||||
if not isinstance(panel, dict): continue
|
||||
for section_name, section in panel.items():
|
||||
if not isinstance(section, dict): continue
|
||||
for name, param in section.items():
|
||||
if not isinstance(param, dict):
|
||||
continue
|
||||
print(';'.join([
|
||||
name,
|
||||
param.get('type', 'string'),
|
||||
param.get('source', 'settings' if param.get('type', 'string') != 'file' else '')
|
||||
]))
|
||||
EOL
|
||||
`
|
||||
for param_source in $params_sources
|
||||
for line in $lines
|
||||
do
|
||||
local short_setting="$(echo $param_source | cut -d= -f1)"
|
||||
IFS=';' read short_setting type source <<< "$line"
|
||||
local getter="get__${short_setting}"
|
||||
local source="$(echo $param_source | cut -d= -f2)"
|
||||
sources[${short_setting}]="$source"
|
||||
types[${short_setting}]="$type"
|
||||
file_hash[${short_setting}]=""
|
||||
|
||||
formats[${short_setting}]=""
|
||||
# Get value from getter if exists
|
||||
if type -t $getter 2>/dev/null | grep -q '^function$' 2>/dev/null; then
|
||||
old[$short_setting]="$($getter)"
|
||||
formats[${short_setting}]="yaml"
|
||||
|
||||
elif [[ "$source" == "" ]] ; then
|
||||
old[$short_setting]="YNH_NULL"
|
||||
|
||||
# Get value from app settings or from another file
|
||||
elif [[ "$source" == "settings" ]] || [[ "$source" == *":"* ]] ; then
|
||||
elif [[ "$type" == "file" ]] ; then
|
||||
if [[ "$source" == "settings" ]] ; then
|
||||
ynh_die "File '${short_setting}' can't be stored in settings"
|
||||
fi
|
||||
old[$short_setting]="$(ls $(echo $source | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/) 2> /dev/null || echo YNH_NULL)"
|
||||
file_hash[$short_setting]="true"
|
||||
|
||||
# Get multiline text from settings or from a full file
|
||||
elif [[ "$type" == "text" ]] ; then
|
||||
if [[ "$source" == "settings" ]] ; then
|
||||
old[$short_setting]="$(ynh_app_setting_get $app $short_setting)"
|
||||
elif [[ "$source" == *":"* ]] ; then
|
||||
ynh_die "For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter"
|
||||
else
|
||||
old[$short_setting]="$(cat $(echo $source | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/) 2> /dev/null || echo YNH_NULL)"
|
||||
fi
|
||||
|
||||
# Get value from a kind of key/value file
|
||||
else
|
||||
if [[ "$source" == "settings" ]] ; then
|
||||
source=":/etc/yunohost/apps/$app/settings.yml"
|
||||
fi
|
||||
|
||||
local source_key="$(echo "$source" | cut -d: -f1)"
|
||||
source_key=${source_key:-$short_setting}
|
||||
local source_file="$(echo "$source" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)"
|
||||
old[$short_setting]="$(ynh_value_get --file="${source_file}" --key="${source_key}")"
|
||||
|
||||
# Specific case for files (all content of the file is the source)
|
||||
else
|
||||
|
||||
old[$short_setting]="$(ls $(echo $source | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/) 2> /dev/null || echo YNH_NULL)"
|
||||
file_hash[$short_setting]="true"
|
||||
fi
|
||||
done
|
||||
|
||||
|
@ -152,27 +173,42 @@ _ynh_panel_apply() {
|
|||
do
|
||||
local setter="set__${short_setting}"
|
||||
local source="${sources[$short_setting]}"
|
||||
local type="${types[$short_setting]}"
|
||||
if [ "${changed[$short_setting]}" == "true" ] ; then
|
||||
# Apply setter if exists
|
||||
if type -t $setter 2>/dev/null | grep -q '^function$' 2>/dev/null; then
|
||||
$setter
|
||||
|
||||
# Copy file in right place
|
||||
elif [[ "$source" == "" ]] ; then
|
||||
continue
|
||||
|
||||
# Save in a file
|
||||
elif [[ "$type" == "file" ]] ; then
|
||||
if [[ "$source" == "settings" ]] ; then
|
||||
ynh_die "File '${short_setting}' can't be stored in settings"
|
||||
fi
|
||||
local source_file="$(echo "$source" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)"
|
||||
cp "${!short_setting}" "$source_file"
|
||||
|
||||
# Save value in app settings
|
||||
elif [[ "$source" == "settings" ]] ; then
|
||||
ynh_app_setting_set $app $short_setting "${!short_setting}"
|
||||
|
||||
# Get value from a kind of key/value file
|
||||
elif [[ "$source" == *":"* ]]
|
||||
then
|
||||
# Save multiline text in a file
|
||||
elif [[ "$type" == "text" ]] ; then
|
||||
if [[ "$source" == *":"* ]] ; then
|
||||
ynh_die "For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter"
|
||||
fi
|
||||
local source_file="$(echo "$source" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)"
|
||||
echo "${!short_setting}" > "$source_file"
|
||||
|
||||
# Set value into a kind of key/value file
|
||||
else
|
||||
local source_key="$(echo "$source" | cut -d: -f1)"
|
||||
source_key=${source_key:-$short_setting}
|
||||
local source_file="$(echo "$source" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)"
|
||||
ynh_value_set --file="${source_file}" --key="${source_key}" --value="${!short_setting}"
|
||||
|
||||
# Specific case for files (all content of the file is the source)
|
||||
else
|
||||
local source_file="$(echo "$source" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)"
|
||||
cp "${!short_setting}" "$source_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
@ -182,7 +218,13 @@ _ynh_panel_show() {
|
|||
for short_setting in "${!old[@]}"
|
||||
do
|
||||
if [[ "${old[$short_setting]}" != YNH_NULL ]] ; then
|
||||
ynh_return "${short_setting}: \"${old[$short_setting]}\""
|
||||
if [[ "${formats[$short_setting]}" == "yaml" ]] ; then
|
||||
ynh_return "${short_setting}:"
|
||||
ynh_return "$(echo "${old[$short_setting]}" | sed 's/^/ /g')"
|
||||
else
|
||||
ynh_return "${short_setting}: \"$(echo "${old[$short_setting]}" | sed ':a;N;$!ba;s/\n/\n\n/g')\""
|
||||
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
@ -225,7 +267,8 @@ _ynh_panel_validate() {
|
|||
done
|
||||
if [[ "$is_error" == "true" ]]
|
||||
then
|
||||
ynh_die "Nothing has changed"
|
||||
ynh_print_info "Nothing has changed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run validation if something is changed
|
||||
|
@ -241,7 +284,7 @@ _ynh_panel_validate() {
|
|||
if [ -n "$result" ]
|
||||
then
|
||||
local key="YNH_ERROR_${short_setting}"
|
||||
ynh_return "$key: $result"
|
||||
ynh_return "$key: \"$result\""
|
||||
is_error=true
|
||||
fi
|
||||
done
|
||||
|
@ -274,6 +317,9 @@ ynh_panel_run() {
|
|||
declare -Ag changed=()
|
||||
declare -Ag file_hash=()
|
||||
declare -Ag sources=()
|
||||
declare -Ag types=()
|
||||
declare -Ag formats=()
|
||||
|
||||
case $1 in
|
||||
show)
|
||||
ynh_panel_get
|
||||
|
@ -281,12 +327,12 @@ ynh_panel_run() {
|
|||
;;
|
||||
apply)
|
||||
max_progression=4
|
||||
ynh_script_progression --message="Reading config panel description and current configuration..." --weight=1
|
||||
ynh_script_progression --message="Reading config panel description and current configuration..."
|
||||
ynh_panel_get
|
||||
|
||||
ynh_panel_validate
|
||||
|
||||
ynh_script_progression --message="Applying the new configuration..." --weight=1
|
||||
ynh_script_progression --message="Applying the new configuration..."
|
||||
ynh_panel_apply
|
||||
ynh_script_progression --message="Configuration of $app completed" --last
|
||||
;;
|
||||
|
|
|
@ -35,6 +35,7 @@ import glob
|
|||
import urllib.parse
|
||||
import base64
|
||||
import tempfile
|
||||
import readline
|
||||
from collections import OrderedDict
|
||||
|
||||
from moulinette import msignals, m18n, msettings
|
||||
|
@ -1754,36 +1755,21 @@ def app_action_run(operation_logger, app, action, args=None):
|
|||
# * docstrings
|
||||
# * merge translations on the json once the workflow is in place
|
||||
@is_unit_operation()
|
||||
def app_config_show(operation_logger, app, panel='', full=False):
|
||||
def app_config_show(operation_logger, app, key='', full=False):
|
||||
# logger.warning(m18n.n("experimental_feature"))
|
||||
|
||||
# Check app is installed
|
||||
_assert_is_installed(app)
|
||||
|
||||
panel = panel if panel else ''
|
||||
operation_logger.start()
|
||||
key = key if key else ''
|
||||
|
||||
# Read config panel toml
|
||||
config_panel = _get_app_config_panel(app, filter_key=panel)
|
||||
config_panel = _get_app_hydrated_config_panel(operation_logger,
|
||||
app, filter_key=key)
|
||||
|
||||
if not config_panel:
|
||||
return None
|
||||
|
||||
# Call config script to extract current values
|
||||
parsed_values = _call_config_script(operation_logger, app, 'show')
|
||||
|
||||
# # Check and transform values if needed
|
||||
# options = [option for _, _, option in _get_options_iterator(config_panel)]
|
||||
# args_dict = _parse_args_in_yunohost_format(
|
||||
# parsed_values, options, False
|
||||
# )
|
||||
|
||||
# Hydrate
|
||||
logger.debug("Hydrating config with current value")
|
||||
for _, _, option in _get_options_iterator(config_panel):
|
||||
if option['name'] in parsed_values:
|
||||
option["value"] = parsed_values[option['name']] #args_dict[option["name"]][0]
|
||||
|
||||
# Format result in full or reduce mode
|
||||
if full:
|
||||
operation_logger.success()
|
||||
|
@ -1800,8 +1786,8 @@ def app_config_show(operation_logger, app, panel='', full=False):
|
|||
}
|
||||
if not option.get('optional', False):
|
||||
r_option['ask'] += ' *'
|
||||
if option.get('value', None) is not None:
|
||||
r_option['value'] = option['value']
|
||||
if option.get('current_value', None) is not None:
|
||||
r_option['value'] = option['current_value']
|
||||
|
||||
operation_logger.success()
|
||||
return result
|
||||
|
@ -1812,7 +1798,6 @@ def app_config_get(operation_logger, app, key):
|
|||
# Check app is installed
|
||||
_assert_is_installed(app)
|
||||
|
||||
operation_logger.start()
|
||||
|
||||
# Read config panel toml
|
||||
config_panel = _get_app_config_panel(app, filter_key=key)
|
||||
|
@ -1820,6 +1805,8 @@ def app_config_get(operation_logger, app, key):
|
|||
if not config_panel:
|
||||
raise YunohostError("app_config_no_panel")
|
||||
|
||||
operation_logger.start()
|
||||
|
||||
# Call config script to extract current values
|
||||
parsed_values = _call_config_script(operation_logger, app, 'show')
|
||||
|
||||
|
@ -1851,7 +1838,8 @@ def app_config_set(operation_logger, app, key=None, value=None, args=None):
|
|||
filter_key = key if key else ''
|
||||
|
||||
# Read config panel toml
|
||||
config_panel = _get_app_config_panel(app, filter_key=filter_key)
|
||||
config_panel = _get_app_hydrated_config_panel(operation_logger,
|
||||
app, filter_key=filter_key)
|
||||
|
||||
if not config_panel:
|
||||
raise YunohostError("app_config_no_panel")
|
||||
|
@ -1862,12 +1850,16 @@ def app_config_set(operation_logger, app, key=None, value=None, args=None):
|
|||
operation_logger.start()
|
||||
|
||||
# Prepare pre answered questions
|
||||
args = dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) if args else {}
|
||||
if args:
|
||||
args = { key: ','.join(value) for key, value in urllib.parse.parse_qs(args, keep_blank_values=True).items() }
|
||||
else:
|
||||
args = {}
|
||||
if value is not None:
|
||||
args = {filter_key.split('.')[-1]: value}
|
||||
|
||||
try:
|
||||
logger.debug("Asking unanswered question and prevalidating...")
|
||||
args_dict = {}
|
||||
for panel in config_panel.get("panel", []):
|
||||
if msettings.get('interface') == 'cli' and len(filter_key.split('.')) < 3:
|
||||
msignals.display(colorize("\n" + "=" * 40, 'purple'))
|
||||
|
@ -1878,13 +1870,13 @@ def app_config_set(operation_logger, app, key=None, value=None, args=None):
|
|||
msignals.display(colorize(f"\n# {section['name']}", 'purple'))
|
||||
|
||||
# Check and ask unanswered questions
|
||||
args_dict = _parse_args_in_yunohost_format(
|
||||
args_dict.update(_parse_args_in_yunohost_format(
|
||||
args, section['options']
|
||||
)
|
||||
))
|
||||
|
||||
# Call config script to extract current values
|
||||
logger.info("Running config script...")
|
||||
env = {key: str(value[0]) for key, value in args_dict.items()}
|
||||
env = {key: str(value[0]) for key, value in args_dict.items() if not value[0] is None}
|
||||
|
||||
errors = _call_config_script(operation_logger, app, 'apply', env=env)
|
||||
# Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception
|
||||
|
@ -2246,6 +2238,37 @@ def _get_app_config_panel(app_id, filter_key=''):
|
|||
|
||||
return None
|
||||
|
||||
def _get_app_hydrated_config_panel(operation_logger, app, filter_key=''):
|
||||
|
||||
# Read config panel toml
|
||||
config_panel = _get_app_config_panel(app, filter_key=filter_key)
|
||||
|
||||
if not config_panel:
|
||||
return None
|
||||
|
||||
operation_logger.start()
|
||||
|
||||
# Call config script to extract current values
|
||||
parsed_values = _call_config_script(operation_logger, app, 'show')
|
||||
|
||||
# # Check and transform values if needed
|
||||
# options = [option for _, _, option in _get_options_iterator(config_panel)]
|
||||
# args_dict = _parse_args_in_yunohost_format(
|
||||
# parsed_values, options, False
|
||||
# )
|
||||
|
||||
# Hydrate
|
||||
logger.debug("Hydrating config with current value")
|
||||
for _, _, option in _get_options_iterator(config_panel):
|
||||
if option['name'] in parsed_values:
|
||||
value = parsed_values[option['name']]
|
||||
if isinstance(value, dict):
|
||||
option.update(value)
|
||||
else:
|
||||
option["current_value"] = value #args_dict[option["name"]][0]
|
||||
|
||||
return config_panel
|
||||
|
||||
|
||||
def _get_app_settings(app_id):
|
||||
"""
|
||||
|
@ -2808,6 +2831,7 @@ class YunoHostArgumentFormatParser(object):
|
|||
parsed_question.name = question["name"]
|
||||
parsed_question.type = question.get("type", 'string')
|
||||
parsed_question.default = question.get("default", None)
|
||||
parsed_question.current_value = question.get("current_value")
|
||||
parsed_question.optional = question.get("optional", False)
|
||||
parsed_question.choices = question.get("choices", [])
|
||||
parsed_question.pattern = question.get("pattern")
|
||||
|
@ -2835,11 +2859,20 @@ class YunoHostArgumentFormatParser(object):
|
|||
msignals.display(text_for_user_input_in_cli)
|
||||
|
||||
elif question.value is None:
|
||||
question.value = msignals.prompt(
|
||||
message=text_for_user_input_in_cli,
|
||||
is_password=self.hide_user_input_in_prompt,
|
||||
confirm=self.hide_user_input_in_prompt
|
||||
)
|
||||
prefill = None
|
||||
if question.current_value is not None:
|
||||
prefill = question.current_value
|
||||
elif question.default is not None:
|
||||
prefill = question.default
|
||||
readline.set_startup_hook(lambda: readline.insert_text(prefill))
|
||||
try:
|
||||
question.value = msignals.prompt(
|
||||
message=text_for_user_input_in_cli,
|
||||
is_password=self.hide_user_input_in_prompt,
|
||||
confirm=self.hide_user_input_in_prompt
|
||||
)
|
||||
finally:
|
||||
readline.set_startup_hook()
|
||||
|
||||
|
||||
# Apply default value
|
||||
|
@ -2897,8 +2930,6 @@ class YunoHostArgumentFormatParser(object):
|
|||
if question.choices:
|
||||
text_for_user_input_in_cli += " [{0}]".format(" | ".join(question.choices))
|
||||
|
||||
if question.default is not None:
|
||||
text_for_user_input_in_cli += " (default: {0})".format(question.default)
|
||||
if question.help or question.helpLink:
|
||||
text_for_user_input_in_cli += ":\033[m"
|
||||
if question.help:
|
||||
|
@ -2919,6 +2950,18 @@ class StringArgumentParser(YunoHostArgumentFormatParser):
|
|||
default_value = ""
|
||||
|
||||
|
||||
class TagsArgumentParser(YunoHostArgumentFormatParser):
|
||||
argument_type = "tags"
|
||||
|
||||
def _prevalidate(self, question):
|
||||
values = question.value
|
||||
for value in values.split(','):
|
||||
question.value = value
|
||||
super()._prevalidate(question)
|
||||
question.value = values
|
||||
|
||||
|
||||
|
||||
class PasswordArgumentParser(YunoHostArgumentFormatParser):
|
||||
hide_user_input_in_prompt = True
|
||||
argument_type = "password"
|
||||
|
@ -2938,13 +2981,15 @@ class PasswordArgumentParser(YunoHostArgumentFormatParser):
|
|||
return question
|
||||
|
||||
def _prevalidate(self, question):
|
||||
if any(char in question.value for char in self.forbidden_chars):
|
||||
raise YunohostValidationError(
|
||||
"pattern_password_app", forbidden_chars=self.forbidden_chars
|
||||
)
|
||||
super()._prevalidate(question)
|
||||
|
||||
# If it's an optional argument the value should be empty or strong enough
|
||||
if not question.optional or question.value:
|
||||
if question.value is not None:
|
||||
if any(char in question.value for char in self.forbidden_chars):
|
||||
raise YunohostValidationError(
|
||||
"pattern_password_app", forbidden_chars=self.forbidden_chars
|
||||
)
|
||||
|
||||
# If it's an optional argument the value should be empty or strong enough
|
||||
from yunohost.utils.password import assert_password_is_strong_enough
|
||||
|
||||
assert_password_is_strong_enough("user", question.value)
|
||||
|
@ -3098,23 +3143,26 @@ class DisplayTextArgumentParser(YunoHostArgumentFormatParser):
|
|||
readonly = True
|
||||
|
||||
def parse_question(self, question, user_answers):
|
||||
question = super(DisplayTextArgumentParser, self).parse_question(
|
||||
question_parsed = super().parse_question(
|
||||
question, user_answers
|
||||
)
|
||||
|
||||
question.optional = True
|
||||
question_parsed.optional = True
|
||||
question_parsed.style = question.get('style', 'info')
|
||||
|
||||
return question
|
||||
return question_parsed
|
||||
|
||||
def _format_text_for_user_input_in_cli(self, question):
|
||||
text = question.ask['en']
|
||||
if question.type in ['info', 'warning', 'danger']:
|
||||
|
||||
if question.style in ['success', 'info', 'warning', 'danger']:
|
||||
color = {
|
||||
'success': 'green',
|
||||
'info': 'cyan',
|
||||
'warning': 'yellow',
|
||||
'danger': 'red'
|
||||
}
|
||||
return colorize(m18n.g(question.type), color[question.type]) + f" {text}"
|
||||
return colorize(m18n.g(question.style), color[question.style]) + f" {text}"
|
||||
else:
|
||||
return text
|
||||
|
||||
|
@ -3137,7 +3185,7 @@ class FileArgumentParser(YunoHostArgumentFormatParser):
|
|||
if question.get('accept'):
|
||||
question_parsed.accept = question.get('accept').replace(' ', '').split(',')
|
||||
else:
|
||||
question.accept = []
|
||||
question_parsed.accept = []
|
||||
if msettings.get('interface') == 'api':
|
||||
if user_answers.get(question_parsed.name):
|
||||
question_parsed.value = {
|
||||
|
@ -3200,7 +3248,7 @@ ARGUMENTS_TYPE_PARSERS = {
|
|||
"string": StringArgumentParser,
|
||||
"text": StringArgumentParser,
|
||||
"select": StringArgumentParser,
|
||||
"tags": StringArgumentParser,
|
||||
"tags": TagsArgumentParser,
|
||||
"email": StringArgumentParser,
|
||||
"url": StringArgumentParser,
|
||||
"date": StringArgumentParser,
|
||||
|
@ -3214,10 +3262,7 @@ ARGUMENTS_TYPE_PARSERS = {
|
|||
"number": NumberArgumentParser,
|
||||
"range": NumberArgumentParser,
|
||||
"display_text": DisplayTextArgumentParser,
|
||||
"success": DisplayTextArgumentParser,
|
||||
"danger": DisplayTextArgumentParser,
|
||||
"warning": DisplayTextArgumentParser,
|
||||
"info": DisplayTextArgumentParser,
|
||||
"alert": DisplayTextArgumentParser,
|
||||
"markdown": DisplayTextArgumentParser,
|
||||
"file": FileArgumentParser,
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue