Merge pull request #1096 from YunoHost/less-madness-for-hook-exec

Less madness for hook exec
This commit is contained in:
Alexandre Aubin 2021-01-19 23:26:10 +01:00 committed by GitHub
commit 47181a4598
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 94 additions and 59 deletions

View file

@ -371,8 +371,6 @@ def app_change_url(operation_logger, app, domain, path):
# Retrieve arguments list for change_url script
# TODO: Allow to specify arguments
args_odict = _parse_args_from_manifest(manifest, 'change_url')
args_list = [value[0] for value in args_odict.values()]
args_list.append(app)
# Prepare env. var. to pass to script
env_dict = _make_environment_for_app_script(app, args=args_odict)
@ -404,7 +402,7 @@ def app_change_url(operation_logger, app, domain, path):
os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url")))
if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'),
args=args_list, env=env_dict)[0] != 0:
env=env_dict)[0] != 0:
msg = "Failed to change '%s' url." % app
logger.error(msg)
operation_logger.error(msg)
@ -432,7 +430,7 @@ def app_change_url(operation_logger, app, domain, path):
logger.success(m18n.n("app_change_url_success",
app=app, domain=domain, path=path))
hook_callback('post_app_change_url', args=args_list, env=env_dict)
hook_callback('post_app_change_url', env=env_dict)
def app_upgrade(app=[], url=None, file=None, force=False):
@ -530,8 +528,6 @@ def app_upgrade(app=[], url=None, file=None, force=False):
# Retrieve arguments list for upgrade script
# TODO: Allow to specify arguments
args_odict = _parse_args_from_manifest(manifest, 'upgrade')
args_list = [value[0] for value in args_odict.values()]
args_list.append(app_instance_name)
# Prepare env. var. to pass to script
env_dict = _make_environment_for_app_script(app_instance_name, args=args_odict)
@ -560,7 +556,7 @@ def app_upgrade(app=[], url=None, file=None, force=False):
upgrade_failed = True
try:
upgrade_retcode = hook_exec(extracted_app_folder + '/scripts/upgrade',
args=args_list, env=env_dict)[0]
env=env_dict)[0]
upgrade_failed = True if upgrade_retcode != 0 else False
if upgrade_failed:
@ -637,7 +633,7 @@ def app_upgrade(app=[], url=None, file=None, force=False):
# So much win
logger.success(m18n.n('app_upgraded', app=app_instance_name))
hook_callback('post_app_upgrade', args=args_list, env=env_dict)
hook_callback('post_app_upgrade', env=env_dict)
operation_logger.success()
permission_sync_to_user()
@ -754,10 +750,6 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
# Validate domain / path availability for webapps
_validate_and_normalize_webpath(manifest, args_odict, extracted_app_folder)
# build arg list tq
args_list = [value[0] for value in args_odict.values()]
args_list.append(app_instance_name)
# Attempt to patch legacy helpers ...
_patch_legacy_helpers(extracted_app_folder)
@ -830,7 +822,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
try:
install_retcode = hook_exec(
os.path.join(extracted_app_folder, 'scripts/install'),
args=args_list, env=env_dict
env=env_dict
)[0]
# "Common" app install failure : the script failed and returned exit code != 0
install_failed = True if install_retcode != 0 else False
@ -946,7 +938,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
logger.success(m18n.n('installation_complete'))
hook_callback('post_app_install', args=args_list, env=env_dict)
hook_callback('post_app_install', env=env_dict)
def dump_app_log_extract_for_debugging(operation_logger):
@ -1033,8 +1025,6 @@ def app_remove(operation_logger, app):
os.system('chown -R admin: /tmp/yunohost_remove')
os.system('chmod -R u+rX /tmp/yunohost_remove')
args_list = [app]
env_dict = {}
app_id, app_instance_nb = _parse_app_instance_name(app)
env_dict["YNH_APP_ID"] = app_id
@ -1046,7 +1036,6 @@ def app_remove(operation_logger, app):
try:
ret = hook_exec('/tmp/yunohost_remove/scripts/remove',
args=args_list,
env=env_dict)[0]
# Here again, calling hook_exec could fail miserably, or get
# manually interrupted (by mistake or because script was stuck)
@ -1059,7 +1048,7 @@ def app_remove(operation_logger, app):
if ret == 0:
logger.success(m18n.n('app_removed', app=app))
hook_callback('post_app_remove', args=args_list, env=env_dict)
hook_callback('post_app_remove', env=env_dict)
else:
logger.warning(m18n.n('app_not_properly_removed', app=app))
@ -1470,7 +1459,6 @@ def app_action_run(operation_logger, app, action, args=None):
# Retrieve arguments list for install script
args_dict = dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) if args else {}
args_odict = _parse_args_for_action(actions[action], args=args_dict)
args_list = [value[0] for value in args_odict.values()]
env_dict = _make_environment_for_app_script(app, args=args_odict, args_prefix="ACTION_")
env_dict["YNH_ACTION"] = action
@ -1489,7 +1477,6 @@ def app_action_run(operation_logger, app, action, args=None):
retcode = hook_exec(
path,
args=args_list,
env=env_dict,
chdir=cwd,
user=action_declaration.get("user", "root"),
@ -2775,7 +2762,7 @@ def _make_environment_for_app_script(app, args={}, args_prefix="APP_ARG_"):
}
for arg_name, arg_value_and_type in args.items():
env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = arg_value_and_type[0]
env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = str(arg_value_and_type[0])
return env_dict
@ -3139,6 +3126,33 @@ def _patch_legacy_helpers(app_folder):
"pattern": r"(Automatic diagnosis data from YunoHost( *\n)*)? *(__\w+__)? *\$\(yunohost tools diagnosis.*\)(__\w+__)?",
"replace": r"",
"important": False
},
# Old $1, $2 in backup/restore scripts...
"app=$2": {
"only_for": ["scripts/backup", "scripts/restore"],
"pattern": r"app=\$2",
"replace": r"app=$YNH_APP_INSTANCE_NAME",
"important": True
},
# Old $1, $2 in backup/restore scripts...
"backup_dir=$1": {
"only_for": ["scripts/backup", "scripts/restore"],
"pattern": r"backup_dir=\$1",
"replace": r"backup_dir=.",
"important": True
},
# Old $1, $2 in backup/restore scripts...
"restore_dir=$1": {
"only_for": ["scripts/restore"],
"pattern": r"restore_dir=\$1",
"replace": r"restore_dir=.",
"important": True
},
# Old $1, $2 in install scripts...
# We ain't patching that shit because it ain't trivial to patch all args...
"domain=$1": {
"only_for": ["scripts/install"],
"important": True
}
}
@ -3157,6 +3171,11 @@ def _patch_legacy_helpers(app_folder):
show_warning = False
for helper, infos in stuff_to_replace.items():
# Ignore if not relevant for this file
if infos.get("only_for") and not any(filename.endswith(f) for f in infos["only_for"]):
continue
# If helper is used, attempt to patch the file
if helper in content and infos["pattern"]:
content = infos["pattern"].sub(infos["replace"], content)
@ -3164,11 +3183,11 @@ def _patch_legacy_helpers(app_folder):
if infos["important"]:
show_warning = True
# If the helpert is *still* in the content, it means that we
# If the helper is *still* in the content, it means that we
# couldn't patch the deprecated helper in the previous lines. In
# that case, abort the install or whichever step is performed
if helper in content and infos["important"]:
raise YunohostError("This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.")
raise YunohostError("This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.", raw_msg=True)
if replaced_stuff:

View file

@ -686,8 +686,10 @@ class BackupManager():
app_script = os.path.join(app_setting_path, 'scripts/backup')
subprocess.call(['install', '-Dm555', app_script, tmp_script])
hook_exec(tmp_script, args=[tmp_app_bkp_dir, app],
raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict)[0]
hook_exec(tmp_script,
raise_on_error=True,
chdir=tmp_app_bkp_dir,
env=env_dict)[0]
self._import_to_list_to_backup(env_dict["YNH_BACKUP_CSV"])
@ -1383,7 +1385,6 @@ class RestoreManager():
# Execute app restore script
hook_exec(restore_script,
args=[app_backup_in_archive, app_instance_name],
chdir=app_backup_in_archive,
raise_on_error=True,
env=env_dict)[0]
@ -1408,7 +1409,7 @@ class RestoreManager():
operation_logger.start()
# Execute remove script
if hook_exec(remove_script, args=[app_instance_name],
if hook_exec(remove_script,
env=env_dict_remove)[0] != 0:
msg = m18n.n('app_not_properly_removed', app=app_instance_name)
logger.warning(msg)

View file

@ -212,7 +212,7 @@ def hook_list(action, list_by='name', show_info=False):
return {'hooks': result}
def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None,
def hook_callback(action, hooks=[], args=None, chdir=None,
env=None, pre_callback=None, post_callback=None):
"""
Execute all scripts binded to an action
@ -221,7 +221,6 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None,
action -- Action name
hooks -- List of hooks names to execute
args -- Ordered list of arguments to pass to the scripts
no_trace -- Do not print each command that will be executed
chdir -- The directory from where the scripts will be executed
env -- Dictionnary of environment variables to export
pre_callback -- An object to call before each script execution with
@ -282,7 +281,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None,
hook_args = pre_callback(name=name, priority=priority,
path=path, args=args)
hook_return = hook_exec(path, args=hook_args, chdir=chdir, env=env,
no_trace=no_trace, raise_on_error=True)[1]
raise_on_error=True)[1]
except YunohostError as e:
state = 'failed'
hook_return = {}
@ -298,8 +297,8 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None,
return result
def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
chdir=None, env=None, user="root", return_format="json"):
def hook_exec(path, args=None, raise_on_error=False,
chdir=None, env=None, return_format="json"):
"""
Execute hook from a file with arguments
@ -307,11 +306,8 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
path -- Path of the script to execute
args -- Ordered list of arguments to pass to the script
raise_on_error -- Raise if the script returns a non-zero exit code
no_trace -- Do not print each command that will be executed
chdir -- The directory from where the script will be executed
env -- Dictionnary of environment variables to export
user -- User with which to run the command
"""
# Validate hook path
@ -350,7 +346,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
if hook_type == 'text/x-python':
returncode, returndata = _hook_exec_python(path, args, env, loggers)
else:
returncode, returndata = _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, loggers)
returncode, returndata = _hook_exec_bash(path, args, chdir, env, return_format, loggers)
# Check and return process' return code
if returncode is None:
@ -365,7 +361,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
return returncode, returndata
def _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, loggers):
def _hook_exec_bash(path, args, chdir, env, return_format, loggers):
from moulinette.utils.process import call_async_output
@ -393,27 +389,20 @@ def _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, logge
f.write('')
env['YNH_STDRETURN'] = stdreturn
# Construct command to execute
if user == "root":
command = ['sh', '-c']
else:
command = ['sudo', '-n', '-u', user, '-H', 'sh', '-c']
if no_trace:
cmd = '/bin/bash "{script}" {args}'
else:
# use xtrace on fd 7 which is redirected to stdout
cmd = 'BASH_XTRACEFD=7 /bin/bash -x "{script}" {args} 7>&1'
env['BASH_XTRACEFD'] = "7"
cmd = '/bin/bash -x "{script}" {args} 7>&1'
cmd = cmd.format(script=cmd_script, args=cmd_args)
# prepend environment variables
cmd = '{0} {1}'.format(
' '.join(['{0}={1}'.format(k, shell_quote(v))
for k, v in env.items()]), cmd)
command.append(cmd.format(script=cmd_script, args=cmd_args))
logger.debug("Executing command '%s'" % cmd)
logger.debug("Executing command '%s'" % ' '.join(command))
_env = os.environ.copy()
_env.update(env)
returncode = call_async_output(command, loggers, shell=False, cwd=chdir)
returncode = call_async_output(
cmd, loggers, shell=False, cwd=chdir,
env=_env
)
raw_content = None
try:

View file

@ -3,7 +3,7 @@ import pytest
import sys
import moulinette
from moulinette import m18n
from moulinette import m18n, msettings
from yunohost.utils.error import YunohostError
from contextlib import contextmanager
sys.path.append("..")
@ -78,3 +78,4 @@ def pytest_cmdline_main(config):
sys.path.insert(0, "/usr/lib/moulinette/")
import yunohost
yunohost.init(debug=config.option.yunodebug)
msettings["interface"] = "test"

View file

@ -219,12 +219,20 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor
user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False)
user_group_update(groupname='all_users', add=username, force=True, sync_perm=True)
# Trigger post_user_create hooks
env_dict = {
"YNH_USER_USERNAME": username,
"YNH_USER_MAIL": mail,
"YNH_USER_PASSWORD": password,
"YNH_USER_FIRSTNAME": firstname,
"YNH_USER_LASTNAME": lastname
}
hook_callback('post_user_create', args=[username, mail], env=env_dict)
# TODO: Send a welcome mail to user
logger.success(m18n.n('user_created'))
hook_callback('post_user_create',
args=[username, mail, password, firstname, lastname])
return {'fullname': fullname, 'username': username, 'mail': mail}
@ -302,6 +310,7 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail=
from yunohost.app import app_ssowatconf
from yunohost.utils.password import assert_password_is_strong_enough
from yunohost.utils.ldap import _get_ldap_interface
from yunohost.hook import hook_callback
domains = domain_list()['domains']
@ -312,16 +321,21 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail=
if not result:
raise YunohostError('user_unknown', user=username)
user = result[0]
env_dict = {
"YNH_USER_USERNAME": username
}
# Get modifications from arguments
new_attr_dict = {}
if firstname:
new_attr_dict['givenName'] = [firstname] # TODO: Validate
new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + user['sn'][0]]
env_dict["YNH_USER_FIRSTNAME"] = firstname
if lastname:
new_attr_dict['sn'] = [lastname] # TODO: Validate
new_attr_dict['cn'] = new_attr_dict['displayName'] = [user['givenName'][0] + ' ' + lastname]
env_dict["YNH_USER_LASTNAME"] = lastname
if lastname and firstname:
new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + lastname]
@ -337,6 +351,7 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail=
assert_password_is_strong_enough("user", change_password)
new_attr_dict['userPassword'] = [_hash_user_password(change_password)]
env_dict["YNH_USER_PASSWORD"] = change_password
if mail:
main_domain = _get_maindomain()
@ -381,6 +396,9 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail=
raise YunohostError('mail_alias_remove_failed', mail=mail)
new_attr_dict['mail'] = user['mail']
if 'mail' in new_attr_dict:
env_dict["YNH_USER_MAILS"] = ','.join(new_attr_dict['mail'])
if add_mailforward:
if not isinstance(add_mailforward, list):
add_mailforward = [add_mailforward]
@ -400,8 +418,12 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail=
raise YunohostError('mail_forward_remove_failed', mail=mail)
new_attr_dict['maildrop'] = user['maildrop']
if 'maildrop' in new_attr_dict:
env_dict["YNH_USER_MAILFORWARDS"] = ','.join(new_attr_dict['maildrop'])
if mailbox_quota is not None:
new_attr_dict['mailuserquota'] = [mailbox_quota]
env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota
operation_logger.start()
@ -410,6 +432,9 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail=
except Exception as e:
raise YunohostError('user_update_failed', user=username, error=e)
# Trigger post_user_update hooks
hook_callback('post_user_update', env=env_dict)
logger.success(m18n.n('user_updated'))
app_ssowatconf()
return user_info(username)