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 # Retrieve arguments list for change_url script
# TODO: Allow to specify arguments # TODO: Allow to specify arguments
args_odict = _parse_args_from_manifest(manifest, 'change_url') 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 # Prepare env. var. to pass to script
env_dict = _make_environment_for_app_script(app, args=args_odict) 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"))) 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'), 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 msg = "Failed to change '%s' url." % app
logger.error(msg) logger.error(msg)
operation_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", logger.success(m18n.n("app_change_url_success",
app=app, domain=domain, path=path)) 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): 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 # Retrieve arguments list for upgrade script
# TODO: Allow to specify arguments # TODO: Allow to specify arguments
args_odict = _parse_args_from_manifest(manifest, 'upgrade') 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 # Prepare env. var. to pass to script
env_dict = _make_environment_for_app_script(app_instance_name, args=args_odict) 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 upgrade_failed = True
try: try:
upgrade_retcode = hook_exec(extracted_app_folder + '/scripts/upgrade', 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 upgrade_failed = True if upgrade_retcode != 0 else False
if upgrade_failed: if upgrade_failed:
@ -637,7 +633,7 @@ def app_upgrade(app=[], url=None, file=None, force=False):
# So much win # So much win
logger.success(m18n.n('app_upgraded', app=app_instance_name)) 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() operation_logger.success()
permission_sync_to_user() 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 domain / path availability for webapps
_validate_and_normalize_webpath(manifest, args_odict, extracted_app_folder) _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 ... # Attempt to patch legacy helpers ...
_patch_legacy_helpers(extracted_app_folder) _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: try:
install_retcode = hook_exec( install_retcode = hook_exec(
os.path.join(extracted_app_folder, 'scripts/install'), os.path.join(extracted_app_folder, 'scripts/install'),
args=args_list, env=env_dict env=env_dict
)[0] )[0]
# "Common" app install failure : the script failed and returned exit code != 0 # "Common" app install failure : the script failed and returned exit code != 0
install_failed = True if install_retcode != 0 else False 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')) 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): 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('chown -R admin: /tmp/yunohost_remove')
os.system('chmod -R u+rX /tmp/yunohost_remove') os.system('chmod -R u+rX /tmp/yunohost_remove')
args_list = [app]
env_dict = {} env_dict = {}
app_id, app_instance_nb = _parse_app_instance_name(app) app_id, app_instance_nb = _parse_app_instance_name(app)
env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_ID"] = app_id
@ -1046,7 +1036,6 @@ def app_remove(operation_logger, app):
try: try:
ret = hook_exec('/tmp/yunohost_remove/scripts/remove', ret = hook_exec('/tmp/yunohost_remove/scripts/remove',
args=args_list,
env=env_dict)[0] env=env_dict)[0]
# Here again, calling hook_exec could fail miserably, or get # Here again, calling hook_exec could fail miserably, or get
# manually interrupted (by mistake or because script was stuck) # manually interrupted (by mistake or because script was stuck)
@ -1059,7 +1048,7 @@ def app_remove(operation_logger, app):
if ret == 0: if ret == 0:
logger.success(m18n.n('app_removed', app=app)) 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: else:
logger.warning(m18n.n('app_not_properly_removed', app=app)) 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 # Retrieve arguments list for install script
args_dict = dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) if args else {} 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_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 = _make_environment_for_app_script(app, args=args_odict, args_prefix="ACTION_")
env_dict["YNH_ACTION"] = action env_dict["YNH_ACTION"] = action
@ -1489,7 +1477,6 @@ def app_action_run(operation_logger, app, action, args=None):
retcode = hook_exec( retcode = hook_exec(
path, path,
args=args_list,
env=env_dict, env=env_dict,
chdir=cwd, chdir=cwd,
user=action_declaration.get("user", "root"), 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(): 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 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+__)?", "pattern": r"(Automatic diagnosis data from YunoHost( *\n)*)? *(__\w+__)? *\$\(yunohost tools diagnosis.*\)(__\w+__)?",
"replace": r"", "replace": r"",
"important": False "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 show_warning = False
for helper, infos in stuff_to_replace.items(): 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 is used, attempt to patch the file
if helper in content and infos["pattern"]: if helper in content and infos["pattern"]:
content = infos["pattern"].sub(infos["replace"], content) content = infos["pattern"].sub(infos["replace"], content)
@ -3164,11 +3183,11 @@ def _patch_legacy_helpers(app_folder):
if infos["important"]: if infos["important"]:
show_warning = True 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 # couldn't patch the deprecated helper in the previous lines. In
# that case, abort the install or whichever step is performed # that case, abort the install or whichever step is performed
if helper in content and infos["important"]: 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: if replaced_stuff:

View file

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

View file

@ -212,7 +212,7 @@ def hook_list(action, list_by='name', show_info=False):
return {'hooks': result} 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): env=None, pre_callback=None, post_callback=None):
""" """
Execute all scripts binded to an action 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 action -- Action name
hooks -- List of hooks names to execute hooks -- List of hooks names to execute
args -- Ordered list of arguments to pass to the scripts 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 chdir -- The directory from where the scripts will be executed
env -- Dictionnary of environment variables to export env -- Dictionnary of environment variables to export
pre_callback -- An object to call before each script execution with 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, hook_args = pre_callback(name=name, priority=priority,
path=path, args=args) path=path, args=args)
hook_return = hook_exec(path, args=hook_args, chdir=chdir, env=env, 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: except YunohostError as e:
state = 'failed' state = 'failed'
hook_return = {} hook_return = {}
@ -298,8 +297,8 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None,
return result return result
def hook_exec(path, args=None, raise_on_error=False, no_trace=False, def hook_exec(path, args=None, raise_on_error=False,
chdir=None, env=None, user="root", return_format="json"): chdir=None, env=None, return_format="json"):
""" """
Execute hook from a file with arguments 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 path -- Path of the script to execute
args -- Ordered list of arguments to pass to the script args -- Ordered list of arguments to pass to the script
raise_on_error -- Raise if the script returns a non-zero exit code 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 chdir -- The directory from where the script will be executed
env -- Dictionnary of environment variables to export env -- Dictionnary of environment variables to export
user -- User with which to run the command
""" """
# Validate hook path # 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': if hook_type == 'text/x-python':
returncode, returndata = _hook_exec_python(path, args, env, loggers) returncode, returndata = _hook_exec_python(path, args, env, loggers)
else: 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 # Check and return process' return code
if returncode is None: if returncode is None:
@ -365,7 +361,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
return returncode, returndata 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 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('') f.write('')
env['YNH_STDRETURN'] = stdreturn 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 # 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 logger.debug("Executing command '%s'" % cmd)
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'" % ' '.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 raw_content = None
try: try:

View file

@ -3,7 +3,7 @@ import pytest
import sys import sys
import moulinette import moulinette
from moulinette import m18n from moulinette import m18n, msettings
from yunohost.utils.error import YunohostError from yunohost.utils.error import YunohostError
from contextlib import contextmanager from contextlib import contextmanager
sys.path.append("..") sys.path.append("..")
@ -78,3 +78,4 @@ def pytest_cmdline_main(config):
sys.path.insert(0, "/usr/lib/moulinette/") sys.path.insert(0, "/usr/lib/moulinette/")
import yunohost import yunohost
yunohost.init(debug=config.option.yunodebug) 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_create(groupname=username, gid=uid, primary_group=True, sync_perm=False)
user_group_update(groupname='all_users', add=username, force=True, sync_perm=True) 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 # TODO: Send a welcome mail to user
logger.success(m18n.n('user_created')) logger.success(m18n.n('user_created'))
hook_callback('post_user_create',
args=[username, mail, password, firstname, lastname])
return {'fullname': fullname, 'username': username, 'mail': mail} 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.app import app_ssowatconf
from yunohost.utils.password import assert_password_is_strong_enough from yunohost.utils.password import assert_password_is_strong_enough
from yunohost.utils.ldap import _get_ldap_interface from yunohost.utils.ldap import _get_ldap_interface
from yunohost.hook import hook_callback
domains = domain_list()['domains'] domains = domain_list()['domains']
@ -312,16 +321,21 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail=
if not result: if not result:
raise YunohostError('user_unknown', user=username) raise YunohostError('user_unknown', user=username)
user = result[0] user = result[0]
env_dict = {
"YNH_USER_USERNAME": username
}
# Get modifications from arguments # Get modifications from arguments
new_attr_dict = {} new_attr_dict = {}
if firstname: if firstname:
new_attr_dict['givenName'] = [firstname] # TODO: Validate new_attr_dict['givenName'] = [firstname] # TODO: Validate
new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + user['sn'][0]] new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + user['sn'][0]]
env_dict["YNH_USER_FIRSTNAME"] = firstname
if lastname: if lastname:
new_attr_dict['sn'] = [lastname] # TODO: Validate new_attr_dict['sn'] = [lastname] # TODO: Validate
new_attr_dict['cn'] = new_attr_dict['displayName'] = [user['givenName'][0] + ' ' + lastname] new_attr_dict['cn'] = new_attr_dict['displayName'] = [user['givenName'][0] + ' ' + lastname]
env_dict["YNH_USER_LASTNAME"] = lastname
if lastname and firstname: if lastname and firstname:
new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + lastname] 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) assert_password_is_strong_enough("user", change_password)
new_attr_dict['userPassword'] = [_hash_user_password(change_password)] new_attr_dict['userPassword'] = [_hash_user_password(change_password)]
env_dict["YNH_USER_PASSWORD"] = change_password
if mail: if mail:
main_domain = _get_maindomain() 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) raise YunohostError('mail_alias_remove_failed', mail=mail)
new_attr_dict['mail'] = user['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 add_mailforward:
if not isinstance(add_mailforward, list): if not isinstance(add_mailforward, list):
add_mailforward = [add_mailforward] 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) raise YunohostError('mail_forward_remove_failed', mail=mail)
new_attr_dict['maildrop'] = user['maildrop'] 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: if mailbox_quota is not None:
new_attr_dict['mailuserquota'] = [mailbox_quota] new_attr_dict['mailuserquota'] = [mailbox_quota]
env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota
operation_logger.start() operation_logger.start()
@ -410,6 +432,9 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail=
except Exception as e: except Exception as e:
raise YunohostError('user_update_failed', user=username, error=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')) logger.success(m18n.n('user_updated'))
app_ssowatconf() app_ssowatconf()
return user_info(username) return user_info(username)