mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Merge pull request #488 from YunoHost/config_panel
[enh] POC to allow applications to ship configurations panels for the software they bundle
This commit is contained in:
commit
2056e0272b
4 changed files with 174 additions and 3 deletions
|
@ -771,6 +771,31 @@ app:
|
||||||
apps:
|
apps:
|
||||||
nargs: "+"
|
nargs: "+"
|
||||||
|
|
||||||
|
subcategories:
|
||||||
|
|
||||||
|
config:
|
||||||
|
subcategory_help: Applications configuration panel
|
||||||
|
actions:
|
||||||
|
|
||||||
|
### app_config_show_panel()
|
||||||
|
show-panel:
|
||||||
|
action_help: show config panel for the application
|
||||||
|
api: GET /apps/<app_id>/config-panel
|
||||||
|
arguments:
|
||||||
|
app_id:
|
||||||
|
help: App ID
|
||||||
|
|
||||||
|
### app_config_apply()
|
||||||
|
apply:
|
||||||
|
action_help: apply the new configuration
|
||||||
|
api: POST /apps/<app_id>/config
|
||||||
|
arguments:
|
||||||
|
app_id:
|
||||||
|
help: App ID
|
||||||
|
-a:
|
||||||
|
full: --args
|
||||||
|
help: Serialized arguments for new configuration (i.e. "domain=domain.tld&path=/path")
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
# Backup #
|
# Backup #
|
||||||
#############################
|
#############################
|
||||||
|
|
|
@ -179,6 +179,7 @@
|
||||||
"executing_command": "Executing command '{command:s}'...",
|
"executing_command": "Executing command '{command:s}'...",
|
||||||
"executing_script": "Executing script '{script:s}'...",
|
"executing_script": "Executing script '{script:s}'...",
|
||||||
"extracting": "Extracting...",
|
"extracting": "Extracting...",
|
||||||
|
"experimental_feature": "Warning: this feature is experimental and not consider stable, you shouldn't be using it except if you know what you are doing.",
|
||||||
"field_invalid": "Invalid field '{:s}'",
|
"field_invalid": "Invalid field '{:s}'",
|
||||||
"firewall_reload_failed": "Unable to reload the firewall",
|
"firewall_reload_failed": "Unable to reload the firewall",
|
||||||
"firewall_reloaded": "The firewall has been reloaded",
|
"firewall_reloaded": "The firewall has been reloaded",
|
||||||
|
|
|
@ -40,6 +40,7 @@ from collections import OrderedDict
|
||||||
from moulinette import msignals, m18n, msettings
|
from moulinette import msignals, m18n, msettings
|
||||||
from moulinette.core import MoulinetteError
|
from moulinette.core import MoulinetteError
|
||||||
from moulinette.utils.log import getActionLogger
|
from moulinette.utils.log import getActionLogger
|
||||||
|
from moulinette.utils.filesystem import read_json
|
||||||
|
|
||||||
from yunohost.service import service_log, _run_service_command
|
from yunohost.service import service_log, _run_service_command
|
||||||
from yunohost.utils import packages
|
from yunohost.utils import packages
|
||||||
|
@ -640,6 +641,9 @@ def app_upgrade(auth, app=[], url=None, file=None):
|
||||||
os.system('rm -rf "%s/scripts" "%s/manifest.json %s/conf"' % (app_setting_path, app_setting_path, app_setting_path))
|
os.system('rm -rf "%s/scripts" "%s/manifest.json %s/conf"' % (app_setting_path, app_setting_path, app_setting_path))
|
||||||
os.system('mv "%s/manifest.json" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, app_setting_path))
|
os.system('mv "%s/manifest.json" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, app_setting_path))
|
||||||
|
|
||||||
|
if os.path.exists(os.path.join(extracted_app_folder, "config_panel.json")):
|
||||||
|
os.system('cp -R %s/config_panel.json %s' % (extracted_app_folder, app_setting_path))
|
||||||
|
|
||||||
if os.path.exists(os.path.join(extracted_app_folder, "conf")):
|
if os.path.exists(os.path.join(extracted_app_folder, "conf")):
|
||||||
os.system('cp -R %s/conf %s' % (extracted_app_folder, app_setting_path))
|
os.system('cp -R %s/conf %s' % (extracted_app_folder, app_setting_path))
|
||||||
|
|
||||||
|
@ -757,6 +761,9 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False):
|
||||||
os.system('cp %s/manifest.json %s' % (extracted_app_folder, app_setting_path))
|
os.system('cp %s/manifest.json %s' % (extracted_app_folder, app_setting_path))
|
||||||
os.system('cp -R %s/scripts %s' % (extracted_app_folder, app_setting_path))
|
os.system('cp -R %s/scripts %s' % (extracted_app_folder, app_setting_path))
|
||||||
|
|
||||||
|
if os.path.exists(os.path.join(extracted_app_folder, "config_panel.json")):
|
||||||
|
os.system('cp -R %s/config_panel.json %s' % (extracted_app_folder, app_setting_path))
|
||||||
|
|
||||||
if os.path.exists(os.path.join(extracted_app_folder, "conf")):
|
if os.path.exists(os.path.join(extracted_app_folder, "conf")):
|
||||||
os.system('cp -R %s/conf %s' % (extracted_app_folder, app_setting_path))
|
os.system('cp -R %s/conf %s' % (extracted_app_folder, app_setting_path))
|
||||||
|
|
||||||
|
@ -1360,6 +1367,143 @@ def app_change_label(auth, app, new_label):
|
||||||
app_ssowatconf(auth)
|
app_ssowatconf(auth)
|
||||||
|
|
||||||
|
|
||||||
|
# Config panel todo list:
|
||||||
|
# * docstrings
|
||||||
|
# * merge translations on the json once the workflow is in place
|
||||||
|
def app_config_show_panel(app_id):
|
||||||
|
logger.warning(m18n.n('experimental_feature'))
|
||||||
|
|
||||||
|
from yunohost.hook import hook_exec
|
||||||
|
|
||||||
|
# this will take care of checking if the app is installed
|
||||||
|
app_info_dict = app_info(app_id)
|
||||||
|
|
||||||
|
config_panel = os.path.join(APPS_SETTING_PATH, app_id, 'config_panel.json')
|
||||||
|
config_script = os.path.join(APPS_SETTING_PATH, app_id, 'scripts', 'config')
|
||||||
|
|
||||||
|
if not os.path.exists(config_panel) or not os.path.exists(config_script):
|
||||||
|
return {
|
||||||
|
"config_panel": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
config_panel = read_json(config_panel)
|
||||||
|
|
||||||
|
env = {"YNH_APP_ID": app_id}
|
||||||
|
parsed_values = {}
|
||||||
|
|
||||||
|
# I need to parse stdout to communicate between scripts because I can't
|
||||||
|
# read the child environment :( (that would simplify things so much)
|
||||||
|
# after hours of research this is apparently quite a standard way, another
|
||||||
|
# option would be to add an explicite pipe or a named pipe for that
|
||||||
|
# a third option would be to write in a temporary file but I don't like
|
||||||
|
# that because that could expose sensitive data
|
||||||
|
def parse_stdout(line):
|
||||||
|
line = line.rstrip()
|
||||||
|
logger.info(line)
|
||||||
|
|
||||||
|
if line.strip().startswith("YNH_CONFIG_") and "=" in line:
|
||||||
|
# XXX error handling?
|
||||||
|
# XXX this might not work for multilines stuff :( (but echo without
|
||||||
|
# formatting should do it no?)
|
||||||
|
key, value = line.strip().split("=", 1)
|
||||||
|
logger.debug("config script declared: %s -> %s", key, value)
|
||||||
|
parsed_values[key] = value
|
||||||
|
|
||||||
|
return_code = hook_exec(config_script,
|
||||||
|
args=["show"],
|
||||||
|
env=env,
|
||||||
|
user="root",
|
||||||
|
stdout_callback=parse_stdout,
|
||||||
|
)
|
||||||
|
|
||||||
|
if return_code != 0:
|
||||||
|
raise Exception("script/config show return value code: %s (considered as an error)", return_code)
|
||||||
|
|
||||||
|
logger.debug("Generating global variables:")
|
||||||
|
for tab in config_panel.get("panel", []):
|
||||||
|
tab_id = tab["id"] # this makes things easier to debug on crash
|
||||||
|
for section in tab.get("sections", []):
|
||||||
|
section_id = section["id"]
|
||||||
|
for option in section.get("options", []):
|
||||||
|
option_id = option["id"]
|
||||||
|
generated_id = ("YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_id)).upper()
|
||||||
|
option["id"] = generated_id
|
||||||
|
logger.debug(" * '%s'.'%s'.'%s' -> %s", tab.get("name"), section.get("name"), option.get("name"), generated_id)
|
||||||
|
|
||||||
|
if generated_id in parsed_values:
|
||||||
|
# XXX we should probably uses the one of install here but it's at a POC state right now
|
||||||
|
option_type = option["type"]
|
||||||
|
if option_type == "bool":
|
||||||
|
assert parsed_values[generated_id].lower() in ("true", "false")
|
||||||
|
option["value"] = True if parsed_values[generated_id].lower() == "true" else False
|
||||||
|
elif option_type == "integer":
|
||||||
|
option["value"] = int(parsed_values[generated_id])
|
||||||
|
elif option_type == "text":
|
||||||
|
option["value"] = parsed_values[generated_id]
|
||||||
|
else:
|
||||||
|
logger.debug("Variable '%s' is not declared by config script, using default", generated_id)
|
||||||
|
option["value"] = option["default"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"app_id": app_id,
|
||||||
|
"app_name": app_info_dict["name"],
|
||||||
|
"config_panel": config_panel,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def app_config_apply(app_id, args):
|
||||||
|
logger.warning(m18n.n('experimental_feature'))
|
||||||
|
|
||||||
|
from yunohost.hook import hook_exec
|
||||||
|
|
||||||
|
installed = _is_installed(app_id)
|
||||||
|
if not installed:
|
||||||
|
raise MoulinetteError(errno.ENOPKG,
|
||||||
|
m18n.n('app_not_installed', app=app_id))
|
||||||
|
|
||||||
|
config_panel = os.path.join(APPS_SETTING_PATH, app_id, 'config_panel.json')
|
||||||
|
config_script = os.path.join(APPS_SETTING_PATH, app_id, 'scripts', 'config')
|
||||||
|
|
||||||
|
if not os.path.exists(config_panel) or not os.path.exists(config_script):
|
||||||
|
# XXX real exception
|
||||||
|
raise Exception("Not config-panel.json nor scripts/config")
|
||||||
|
|
||||||
|
config_panel = read_json(config_panel)
|
||||||
|
|
||||||
|
env = {"YNH_APP_ID": app_id}
|
||||||
|
args = dict(urlparse.parse_qsl(args, keep_blank_values=True)) if args else {}
|
||||||
|
|
||||||
|
for tab in config_panel.get("panel", []):
|
||||||
|
tab_id = tab["id"] # this makes things easier to debug on crash
|
||||||
|
for section in tab.get("sections", []):
|
||||||
|
section_id = section["id"]
|
||||||
|
for option in section.get("options", []):
|
||||||
|
option_id = option["id"]
|
||||||
|
generated_id = ("YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_id)).upper()
|
||||||
|
|
||||||
|
if generated_id in args:
|
||||||
|
logger.debug("include into env %s=%s", generated_id, args[generated_id])
|
||||||
|
env[generated_id] = args[generated_id]
|
||||||
|
else:
|
||||||
|
logger.debug("no value for key id %s", generated_id)
|
||||||
|
|
||||||
|
# for debug purpose
|
||||||
|
for key in args:
|
||||||
|
if key not in env:
|
||||||
|
logger.warning("Ignore key '%s' from arguments because it is not in the config", key)
|
||||||
|
|
||||||
|
return_code = hook_exec(config_script,
|
||||||
|
args=["apply"],
|
||||||
|
env=env,
|
||||||
|
user="root",
|
||||||
|
)
|
||||||
|
|
||||||
|
if return_code != 0:
|
||||||
|
raise Exception("'script/config apply' return value code: %s (considered as an error)", return_code)
|
||||||
|
|
||||||
|
logger.success("Config updated as expected")
|
||||||
|
|
||||||
|
|
||||||
def _get_app_settings(app_id):
|
def _get_app_settings(app_id):
|
||||||
"""
|
"""
|
||||||
Get settings of an installed app
|
Get settings of an installed app
|
||||||
|
|
|
@ -297,7 +297,8 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None,
|
||||||
|
|
||||||
|
|
||||||
def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
|
def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
|
||||||
chdir=None, env=None, user="admin"):
|
chdir=None, env=None, user="admin", stdout_callback=None,
|
||||||
|
stderr_callback=None):
|
||||||
"""
|
"""
|
||||||
Execute hook from a file with arguments
|
Execute hook from a file with arguments
|
||||||
|
|
||||||
|
@ -361,8 +362,8 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
|
||||||
|
|
||||||
# Define output callbacks and call command
|
# Define output callbacks and call command
|
||||||
callbacks = (
|
callbacks = (
|
||||||
lambda l: logger.debug(l.rstrip()),
|
stdout_callback if stdout_callback else lambda l: logger.debug(l.rstrip()),
|
||||||
lambda l: logger.warning(l.rstrip()),
|
stderr_callback if stderr_callback else lambda l: logger.warning(l.rstrip()),
|
||||||
)
|
)
|
||||||
returncode = call_async_output(
|
returncode = call_async_output(
|
||||||
command, callbacks, shell=False, cwd=chdir
|
command, callbacks, shell=False, cwd=chdir
|
||||||
|
|
Loading…
Add table
Reference in a new issue