From eef247d34b856397806c4fd83444b5d4259200c0 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 02:07:28 +0200 Subject: [PATCH 001/103] [enh] start to work on journals --- data/actionsmap/yunohost.yml | 12 ++++++++++ src/yunohost/journals.py | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/yunohost/journals.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 966de21df..ea7f5e14f 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1588,3 +1588,15 @@ hook: -d: full: --chdir help: The directory from where the script will be executed + +############################# +# Journals # +############################# +journals: + category_help: Manage debug journals + actions: + + ### domain_list() + list: + action_help: List journals + api: GET /journals diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py new file mode 100644 index 000000000..086af2113 --- /dev/null +++ b/src/yunohost/journals.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2016 YunoHost + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" + +""" yunohost_journals.py + + Manage debug journals +""" + +import os + +from moulinette.utils.log import getActionLogger + +JOURNALS_PATH = '/var/log/journals/' + +logger = getActionLogger('yunohost.journals') + + +def journals_list(): + """ + List domains + + Keyword argument: + filter -- LDAP filter used to search + offset -- Starting number for domain fetching + limit -- Maximum number of domain fetched + + """ + return {} From db83eb5a2b17f4941f16dce5d4a284a806d29c19 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 02:08:59 +0200 Subject: [PATCH 002/103] [enh] if not journals available, return nothing --- src/yunohost/journals.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 086af2113..1e4481ed0 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -43,4 +43,8 @@ def journals_list(): limit -- Maximum number of domain fetched """ + + if not os.path.exists(JOURNALS_PATH): + return {} + return {} From 08b7c4f6d7fcdd3cd696979324334a6af3113d2c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 05:32:58 +0200 Subject: [PATCH 003/103] [enh] start Journal class --- src/yunohost/journals.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 1e4481ed0..20037bbc0 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -48,3 +48,41 @@ def journals_list(): return {} return {} + + +class Journal(object): + def __init__(self, name, category, on_stdout=None, on_stderr=None, on_write=None, **kwargs): + self.name = name + self.category = category + self.first_write = False + self.started_at = None + + self.on_stdout = [] if on_stdout is None else on_stdout + self.on_stderr = [] if on_stderr is None else on_stderr + self.on_write = [] if on_write is None else on_write + + self.additional_information = kwargs + + def write(self, line): + print "[journal]", line.rstrip() + + def stdout(self, line): + for i in self.on_stdout: + i(line) + + self.write(line) + + def stderr(self, line): + for i in self.on_stderr: + i(line) + + self.write(line) + + def as_callbacks_tuple(self, stdout=None, stderr=None): + if stdout: + self.on_stdout.append(stdout) + + if stderr: + self.on_stderr.append(stderr) + + return (self.stdout, self.stderr) From 1355b2b754c51ca4e7c7491e1fb4e999e045243f Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 05:33:10 +0200 Subject: [PATCH 004/103] [enh] allow to use a journal for hook exec --- src/yunohost/hook.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 1f971edb6..95ed2aec4 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -297,7 +297,7 @@ 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, - chdir=None, env=None, user="admin"): + chdir=None, env=None, journal=None, user="admin"): """ Execute hook from a file with arguments @@ -359,11 +359,18 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, else: logger.info(m18n.n('executing_script', script=path)) - # Define output callbacks and call command - callbacks = ( - lambda l: logger.info(l.rstrip()), - lambda l: logger.warning(l.rstrip()), - ) + if journal is None: + # Define output callbacks and call command + callbacks = ( + lambda l: logger.info(l.rstrip()), + lambda l: logger.warning(l.rstrip()), + ) + else: + callbacks = journal.as_callbacks_tuple( + stdout=lambda l: logger.info(l.rstrip()), + stderr=lambda l: logger.warning(l.rstrip()), + ) + returncode = call_async_output( command, callbacks, shell=False, cwd=chdir ) From 0668c7e350d6648f119d301713474e42e481126b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 06:26:02 +0200 Subject: [PATCH 005/103] [enh] journal class works as expected \o/ --- src/yunohost/journals.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 20037bbc0..ab245a78f 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -25,6 +25,9 @@ """ import os +import yaml + +from datetime import datetime from moulinette.utils.log import getActionLogger @@ -54,17 +57,47 @@ class Journal(object): def __init__(self, name, category, on_stdout=None, on_stderr=None, on_write=None, **kwargs): self.name = name self.category = category - self.first_write = False + self.first_write = True self.started_at = None + self.path = os.path.join(JOURNALS_PATH, category) + + self.fd = None + self.on_stdout = [] if on_stdout is None else on_stdout self.on_stderr = [] if on_stderr is None else on_stderr self.on_write = [] if on_write is None else on_write self.additional_information = kwargs + def __del__(self): + if self.fd: + self.fd.close() + def write(self, line): - print "[journal]", line.rstrip() + if self.first_write: + self._do_first_write() + self.first_write = False + + self.fd.write("%s: " % datetime.now().strftime("%F_%X").replace(":", "-")) + self.fd.write(line.rstrip()) + self.fd.write("\n") + self.fd.flush() + + def _do_first_write(self): + self.started_at = datetime.now() + + if not os.path.exists(self.path): + os.makedirs(self.path) + + file_name = "%s_%s.journal" % (self.name if isinstance(self.name, basestring) else "_".join(self.name), self.started_at.strftime("%F_%X").replace(":", "-")) + + serialized_additional_information = yaml.safe_dump(self.additional_information, default_flow_style=False) + + self.fd = open(os.path.join(self.path, file_name), "w") + + self.fd.write(serialized_additional_information) + self.fd.write("\n---\n") def stdout(self, line): for i in self.on_stdout: From a6d89f8ea1f007a49c0d38c1556b67bf9f2b7541 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 07:42:50 +0200 Subject: [PATCH 006/103] [enh] journal list correctly list available journals --- src/yunohost/journals.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index ab245a78f..0be01853f 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -50,7 +50,23 @@ def journals_list(): if not os.path.exists(JOURNALS_PATH): return {} - return {} + result = {} + + for category in os.listdir(JOURNALS_PATH): + result[category] = [] + for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): + + journal = journal[:-len(".journal")] + journal = journal.split("_") + journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") + result[category].append({ + "started_at": journal_datetime, + "name": " ".join(journal[:-2]), + }) + + result[category] = list(reversed(sorted(result[category], key=lambda x: x["started_at"]))) + + return result class Journal(object): From 5fba28426037fb58e82fee7bed059c3cba51fb66 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 07:49:52 +0200 Subject: [PATCH 007/103] [enh] generate a journal for app installation --- src/yunohost/app.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 403e76cc4..03c8b9d93 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -653,6 +653,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): """ from yunohost.hook import hook_add, hook_remove, hook_exec + from yunohost.journals import Journal # Fetch or extract sources try: @@ -738,7 +739,12 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): try: install_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/install'), - args=args_list, env=env_dict, user="root") + args=args_list, env=env_dict, user="root" + journal = Journal( + ["install", app_instance_name], + "app", args=args_list, env=env_dict + ), + ) except (KeyboardInterrupt, EOFError): install_retcode = -1 except: From 5eda0fe20e8b9d1f8be7734c521ce240df3401e7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 08:06:52 +0200 Subject: [PATCH 008/103] [fix] avoid a possible thread problem --- src/yunohost/journals.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 0be01853f..b8807fa94 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -74,7 +74,9 @@ class Journal(object): self.name = name self.category = category self.first_write = True - self.started_at = None + + # this help uniformise file name and avoir threads concurrency errors + self.started_at = datetime.now() self.path = os.path.join(JOURNALS_PATH, category) @@ -101,8 +103,6 @@ class Journal(object): self.fd.flush() def _do_first_write(self): - self.started_at = datetime.now() - if not os.path.exists(self.path): os.makedirs(self.path) From d159a437f67d7784517de51871803af6f0ec77ee Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 08:25:11 +0200 Subject: [PATCH 009/103] [enh] journal on app remove when installation failed --- src/yunohost/app.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 03c8b9d93..bad492fae 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -739,7 +739,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): try: install_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/install'), - args=args_list, env=env_dict, user="root" + args=args_list, env=env_dict, user="root", journal = Journal( ["install", app_instance_name], "app", args=args_list, env=env_dict @@ -761,7 +761,12 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): # Execute remove script remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), - args=[app_instance_name], env=env_dict_remove, user="root") + args=[app_instance_name], env=env_dict_remove, user="root", + journal = Journal( + ["remove", app_instance_name, "failed install"], + "app", args=[app_instance_name], env=env_dict_remove, + ), + ) if remove_retcode != 0: logger.warning(m18n.n('app_not_properly_removed', app=app_instance_name)) From de7d26521b623d153871c45466d6bafb41e71cb6 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 08:25:20 +0200 Subject: [PATCH 010/103] [enh] journal on app remove --- src/yunohost/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index bad492fae..c7e1cfec9 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -812,6 +812,7 @@ def app_remove(auth, app): """ from yunohost.hook import hook_exec, hook_remove + from yunohost.journals import Journal if not _is_installed(app): raise MoulinetteError(errno.EINVAL, @@ -837,7 +838,8 @@ def app_remove(auth, app): env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0: + journal = Journal(["remove", app], "app", args=args_list, env=env_dict) + if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root", journal=journal) == 0: logger.success(m18n.n('app_removed', app=app)) if os.path.exists(app_setting_path): From 278f692a2fc7773bcdc2c39cbc4b31e4ebf8f23c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 08:42:49 +0200 Subject: [PATCH 011/103] [enh] journal on app upgrade --- src/yunohost/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index c7e1cfec9..0662fa98c 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -531,6 +531,7 @@ def app_upgrade(auth, app=[], url=None, file=None): """ from yunohost.hook import hook_add, hook_remove, hook_exec + from yunohost.journals import Journal # Retrieve interface is_api = msettings.get('interface') == 'api' @@ -603,7 +604,8 @@ def app_upgrade(auth, app=[], url=None, file=None): # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) - if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root") != 0: + journal = Journal(["upgrade", app_instance_name], "app", args=args_list, env=env_dict) + if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root", journa=journal) != 0: logger.error(m18n.n('app_upgrade_failed', app=app_instance_name)) else: now = int(time.time()) From 5dcf0b782f62691dcaf01c462faaf4c725c6ed75 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 08:46:12 +0200 Subject: [PATCH 012/103] [enh] allow to limit to number of journals per categories --- data/actionsmap/yunohost.yml | 5 +++++ src/yunohost/journals.py | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index ea7f5e14f..05528bb1a 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1600,3 +1600,8 @@ journals: list: action_help: List journals api: GET /journals + arguments: + -l: + full: --limit + help: Maximum number of journals per categories + type: int diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index b8807fa94..e07a96fa2 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -36,7 +36,7 @@ JOURNALS_PATH = '/var/log/journals/' logger = getActionLogger('yunohost.journals') -def journals_list(): +def journals_list(limit=None): """ List domains @@ -66,6 +66,9 @@ def journals_list(): result[category] = list(reversed(sorted(result[category], key=lambda x: x["started_at"]))) + if limit is not None: + result[category] = result[category][:limit] + return result From b18092e329a494f9aa57aa84a71be3dd9b395cde Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 09:48:41 +0200 Subject: [PATCH 013/103] [enh] display more informations on journals list --- src/yunohost/journals.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index e07a96fa2..90aa80398 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -56,12 +56,16 @@ def journals_list(limit=None): result[category] = [] for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): + file_name = journal + journal = journal[:-len(".journal")] journal = journal.split("_") journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") result[category].append({ "started_at": journal_datetime, "name": " ".join(journal[:-2]), + "file_name": file_name, + "path": os.path.join(JOURNALS_PATH, category, file_name), }) result[category] = list(reversed(sorted(result[category], key=lambda x: x["started_at"]))) From 6cb179703e8a940719448dc5ef083914637a04d2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 09:51:18 +0200 Subject: [PATCH 014/103] [fix] bad docstring --- data/actionsmap/yunohost.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 05528bb1a..b4e160e59 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1596,7 +1596,7 @@ journals: category_help: Manage debug journals actions: - ### domain_list() + ### journals_list() list: action_help: List journals api: GET /journals From 19a29d9b3471f85370ac16610100cea3470df155 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 10:00:14 +0200 Subject: [PATCH 015/103] [mod] use more classical datetime format for logs --- src/yunohost/journals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 90aa80398..18a0d5672 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -104,7 +104,7 @@ class Journal(object): self._do_first_write() self.first_write = False - self.fd.write("%s: " % datetime.now().strftime("%F_%X").replace(":", "-")) + self.fd.write("%s: " % datetime.now().strftime("%F %X")) self.fd.write(line.rstrip()) self.fd.write("\n") self.fd.flush() From 3eed2a28fee4dcef4685b7be515cb9ea069ac905 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 10:05:43 +0200 Subject: [PATCH 016/103] [enh] add journals display command --- data/actionsmap/yunohost.yml | 8 ++++++++ src/yunohost/journals.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index b4e160e59..91b6c2c76 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1605,3 +1605,11 @@ journals: full: --limit help: Maximum number of journals per categories type: int + + ### journals_display() + display: + action_help: List journals + api: GET /journals/ + arguments: + file_name: + help: Journal file name diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 18a0d5672..b9de4bcda 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -76,6 +76,40 @@ def journals_list(limit=None): return result +def journals_display(file_name): + if not os.path.exists(JOURNALS_PATH): + # TODO raise exception + return {} + + for category in os.listdir(JOURNALS_PATH): + for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): + if journal != file_name: + continue + + with open(os.path.join(JOURNALS_PATH, category, file_name), "r") as content: + content = content.read() + + journal = journal[:-len(".journal")] + journal = journal.split("_") + journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") + + infos, logs = content.split("\n---\n", 1) + infos = yaml.safe_load(infos) + logs = [x.split(": ", 1) for x in logs.split("\n") if x] + + return { + "started_at": journal_datetime, + "name": " ".join(journal[:-2]), + "file_name": file_name, + "path": os.path.join(JOURNALS_PATH, category, file_name), + "metadata": infos, + "logs": logs, + } + + # TODO raise exception + return {} + + class Journal(object): def __init__(self, name, category, on_stdout=None, on_stderr=None, on_write=None, **kwargs): self.name = name From 1fc7065eafa4fdcb5a47007aba53ae674808b7fe Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 10:06:13 +0200 Subject: [PATCH 017/103] [doc] add TODO comment --- src/yunohost/journals.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index b9de4bcda..425ec15c8 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -112,6 +112,7 @@ def journals_display(file_name): class Journal(object): def __init__(self, name, category, on_stdout=None, on_stderr=None, on_write=None, **kwargs): + # TODO add a way to not save password on app installation self.name = name self.category = category self.first_write = True From 9a08b8ad48cc3f634b0b063306bd5fcd3e2e479b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 28 Jun 2016 04:02:14 +0200 Subject: [PATCH 018/103] [mod] modify data format because mustache is lame --- src/yunohost/journals.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 425ec15c8..f59e5c2c7 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -47,13 +47,13 @@ def journals_list(limit=None): """ + result = {"categories": []} + if not os.path.exists(JOURNALS_PATH): - return {} + return result - result = {} - - for category in os.listdir(JOURNALS_PATH): - result[category] = [] + for category in sorted(os.listdir(JOURNALS_PATH)): + result["categories"].append({"name": category, "journals": []}) for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): file_name = journal @@ -61,17 +61,17 @@ def journals_list(limit=None): journal = journal[:-len(".journal")] journal = journal.split("_") journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") - result[category].append({ + result["categories"][-1]["journals"].append({ "started_at": journal_datetime, "name": " ".join(journal[:-2]), "file_name": file_name, "path": os.path.join(JOURNALS_PATH, category, file_name), }) - result[category] = list(reversed(sorted(result[category], key=lambda x: x["started_at"]))) + result["categories"][-1]["journals"] = list(reversed(sorted(result["categories"][-1]["journals"], key=lambda x: x["started_at"]))) if limit is not None: - result[category] = result[category][:limit] + result["categories"][-1]["journals"] = result["categories"][-1]["journals"][:limit] return result From 804b6ee96236cef10fda9b38fe80259f933b6a14 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 28 Jun 2016 04:05:33 +0200 Subject: [PATCH 019/103] [mod] store journals in yunohost varlog dir --- src/yunohost/journals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index f59e5c2c7..7ee0c2694 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -31,7 +31,7 @@ from datetime import datetime from moulinette.utils.log import getActionLogger -JOURNALS_PATH = '/var/log/journals/' +JOURNALS_PATH = '/var/log/yunohost/journals/' logger = getActionLogger('yunohost.journals') From 41aef0f2d645e6916d4a4e23bb1e99b34cda08ea Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 28 Jun 2016 06:07:21 +0200 Subject: [PATCH 020/103] [mod] adapt logs format to stupid mustache --- src/yunohost/journals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 7ee0c2694..640eac419 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -95,7 +95,7 @@ def journals_display(file_name): infos, logs = content.split("\n---\n", 1) infos = yaml.safe_load(infos) - logs = [x.split(": ", 1) for x in logs.split("\n") if x] + logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] return { "started_at": journal_datetime, From 4543a23a92044f56440a8dc060b7aa12c926ed2d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 6 Jan 2018 01:09:32 +0100 Subject: [PATCH 021/103] [enh] add journal to change_url --- src/yunohost/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0662fa98c..bac85eaec 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -431,6 +431,7 @@ def app_change_url(auth, app, domain, path): """ from yunohost.hook import hook_exec + from yunohost.journals import Journal installed = _is_installed(app) if not installed: @@ -488,8 +489,8 @@ def app_change_url(auth, app, domain, path): os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts"))) os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) - # XXX journal - if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root") != 0: + journal = Journal(["change_url", app], "app", args=args_list, env=env_dict) + if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root", journa=journal) != 0: logger.error("Failed to change '%s' url." % app) # restore values modified by app_checkurl From 021a989c03b251e74f5dae8a9781aaf949c22f5f Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 6 Jan 2018 01:43:07 +0100 Subject: [PATCH 022/103] [fix] documentation was lying --- src/yunohost/journals.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 640eac419..e01f364a5 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -38,13 +38,10 @@ logger = getActionLogger('yunohost.journals') def journals_list(limit=None): """ - List domains + List available journals Keyword argument: - filter -- LDAP filter used to search - offset -- Starting number for domain fetching - limit -- Maximum number of domain fetched - + limit -- Maximum number of journals per categories """ result = {"categories": []} From d92b8562d0478d69d5349b664c8ac24857dfa5ae Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 6 Jan 2018 11:18:28 +0100 Subject: [PATCH 023/103] [doc] document journal display --- data/actionsmap/yunohost.yml | 2 +- src/yunohost/journals.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 91b6c2c76..1de6c729d 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1608,7 +1608,7 @@ journals: ### journals_display() display: - action_help: List journals + action_help: Display a journal content api: GET /journals/ arguments: file_name: diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index e01f364a5..5677d77e5 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -74,6 +74,13 @@ def journals_list(limit=None): def journals_display(file_name): + """ + Display a journal content + + Argument: + file_name + """ + if not os.path.exists(JOURNALS_PATH): # TODO raise exception return {} From 223e41743b88bcffcce7390895bd974455deb9bb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 6 Jan 2018 11:18:42 +0100 Subject: [PATCH 024/103] [mod] spacing for lisiblity --- src/yunohost/journals.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 5677d77e5..d6a41bed5 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -57,7 +57,9 @@ def journals_list(limit=None): journal = journal[:-len(".journal")] journal = journal.split("_") + journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") + result["categories"][-1]["journals"].append({ "started_at": journal_datetime, "name": " ".join(journal[:-2]), From 388e611c459701dcabecbbd7726c40353f71144e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 6 Jan 2018 11:20:41 +0100 Subject: [PATCH 025/103] [enh] raise exception when journal doesn't exist --- locales/en.json | 1 + src/yunohost/journals.py | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index 8dac6e799..92864f348 100644 --- a/locales/en.json +++ b/locales/en.json @@ -201,6 +201,7 @@ "invalid_url_format": "Invalid URL format", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", + "journal_does_exists": "There is not journal with the name '{journal}', use 'yunohost journal list to see all available journals'", "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", "ldap_initialized": "LDAP has been initialized", "license_undefined": "undefined", diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index d6a41bed5..0f1785697 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -26,9 +26,12 @@ import os import yaml +import errno from datetime import datetime +from moulinette import m18n +from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger JOURNALS_PATH = '/var/log/yunohost/journals/' @@ -84,8 +87,8 @@ def journals_display(file_name): """ if not os.path.exists(JOURNALS_PATH): - # TODO raise exception - return {} + raise MoulinetteError(errno.EINVAL, + m18n.n('journal_does_exists', journal=file_name)) for category in os.listdir(JOURNALS_PATH): for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): @@ -112,8 +115,8 @@ def journals_display(file_name): "logs": logs, } - # TODO raise exception - return {} + raise MoulinetteError(errno.EINVAL, + m18n.n('journal_does_exists', journal=file_name)) class Journal(object): From 60727e5fab2b949ec28c85e9572dfdf1939f9955 Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 20 Mar 2018 22:27:42 +0100 Subject: [PATCH 026/103] [fix] typo --- src/yunohost/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index c2c796089..27dee21e7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -498,7 +498,7 @@ def app_change_url(auth, app, domain, path): os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) journal = Journal(["change_url", app], "app", args=args_list, env=env_dict) - if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root", journa=journal) != 0: + if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root", journal=journal) != 0: logger.error("Failed to change '%s' url." % app) # restore values modified by app_checkurl @@ -617,7 +617,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) journal = Journal(["upgrade", app_instance_name], "app", args=args_list, env=env_dict) - if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root", journa=journal) != 0: + if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root", journal=journal) != 0: logger.error(m18n.n('app_upgrade_failed', app=app_instance_name)) else: now = int(time.time()) From ba01168684c8459701e7ec26d1ec37084062e850 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 3 Apr 2018 20:38:56 +0200 Subject: [PATCH 027/103] [enh] Allow to display all operation logs with one command --- data/actionsmap/yunohost.yml | 25 ++++---- locales/en.json | 2 +- src/yunohost/app.py | 8 +-- src/yunohost/{journals.py => log.py} | 93 +++++++++++++++------------- 4 files changed, 69 insertions(+), 59 deletions(-) rename src/yunohost/{journals.py => log.py} (54%) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 3a16bf3bd..b30167b18 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1603,26 +1603,27 @@ hook: help: The directory from where the script will be executed ############################# -# Journals # +# Log # ############################# -journals: - category_help: Manage debug journals +log: + category_help: Manage debug logs actions: - ### journals_list() + ### log_list() list: - action_help: List journals - api: GET /journals + action_help: List logs + api: GET /logs arguments: -l: full: --limit - help: Maximum number of journals per categories + help: Maximum number of logs per categories type: int - ### journals_display() + ### log_display() display: - action_help: Display a journal content - api: GET /journals/ + action_help: Display a log content + api: GET /logs/ arguments: - file_name: - help: Journal file name + file_name_list: + help: Log filenames for which to display the content + nargs: "*" diff --git a/locales/en.json b/locales/en.json index 2859bc888..27946e18d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -205,7 +205,7 @@ "invalid_url_format": "Invalid URL format", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", - "journal_does_exists": "There is not journal with the name '{journal}', use 'yunohost journal list to see all available journals'", + "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", "ldap_initialized": "LDAP has been initialized", "license_undefined": "undefined", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 27dee21e7..2ee96afd1 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -433,7 +433,7 @@ def app_change_url(auth, app, domain, path): """ from yunohost.hook import hook_exec, hook_callback - from yunohost.journals import Journal + from yunohost.log import Journal installed = _is_installed(app) if not installed: @@ -542,7 +542,7 @@ def app_upgrade(auth, app=[], url=None, file=None): """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback - from yunohost.journals import Journal + from yunohost.log import Journal # Retrieve interface is_api = msettings.get('interface') == 'api' @@ -673,7 +673,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback - from yunohost.journals import Journal + from yunohost.log import Journal # Fetch or extract sources try: @@ -837,7 +837,7 @@ def app_remove(auth, app): """ from yunohost.hook import hook_exec, hook_remove, hook_callback - from yunohost.journals import Journal + from yunohost.log import Journal if not _is_installed(app): raise MoulinetteError(errno.EINVAL, diff --git a/src/yunohost/journals.py b/src/yunohost/log.py similarity index 54% rename from src/yunohost/journals.py rename to src/yunohost/log.py index 0f1785697..7c1f3b2a8 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/log.py @@ -19,9 +19,9 @@ """ -""" yunohost_journals.py +""" yunohost_log.py - Manage debug journals + Manage debug logs """ import os @@ -34,90 +34,98 @@ from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -JOURNALS_PATH = '/var/log/yunohost/journals/' +OPERATIONS_PATH = '/var/log/yunohost/operation/' +OPERATION_FILE_EXT = '.yml' -logger = getActionLogger('yunohost.journals') +logger = getActionLogger('yunohost.log') -def journals_list(limit=None): +def log_list(limit=None): """ - List available journals + List available logs Keyword argument: - limit -- Maximum number of journals per categories + limit -- Maximum number of logs per categories """ result = {"categories": []} - if not os.path.exists(JOURNALS_PATH): + if not os.path.exists(OPERATIONS_PATH): return result - for category in sorted(os.listdir(JOURNALS_PATH)): - result["categories"].append({"name": category, "journals": []}) - for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): + for category in sorted(os.listdir(OPERATIONS_PATH)): + result["categories"].append({"name": category, "logs": []}) + for operation in filter(lambda x: x.endswith(OPERATION_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): - file_name = journal + file_name = operation - journal = journal[:-len(".journal")] - journal = journal.split("_") + operation = operation[:-len(OPERATION_FILE_EXT)] + operation = operation.split("_") - journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") + operation_datetime = datetime.strptime(" ".join(operation[-2:]), "%Y-%m-%d %H-%M-%S") - result["categories"][-1]["journals"].append({ - "started_at": journal_datetime, - "name": " ".join(journal[:-2]), + result["categories"][-1]["logs"].append({ + "started_at": operation_datetime, + "name": " ".join(operation[:-2]), "file_name": file_name, - "path": os.path.join(JOURNALS_PATH, category, file_name), + "path": os.path.join(OPERATIONS_PATH, category, file_name), }) - result["categories"][-1]["journals"] = list(reversed(sorted(result["categories"][-1]["journals"], key=lambda x: x["started_at"]))) + result["categories"][-1]["logs"] = list(reversed(sorted(result["categories"][-1]["logs"], key=lambda x: x["started_at"]))) if limit is not None: - result["categories"][-1]["journals"] = result["categories"][-1]["journals"][:limit] + result["categories"][-1]["logs"] = result["categories"][-1]["logs"][:limit] return result -def journals_display(file_name): +def log_display(file_name_list): """ - Display a journal content + Display full log or specific logs listed Argument: - file_name + file_name_list """ - if not os.path.exists(JOURNALS_PATH): + if not os.path.exists(OPERATIONS_PATH): raise MoulinetteError(errno.EINVAL, - m18n.n('journal_does_exists', journal=file_name)) + m18n.n('log_does_exists', log=" ".join(file_name_list))) - for category in os.listdir(JOURNALS_PATH): - for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): - if journal != file_name: + result = {"logs": []} + + for category in os.listdir(OPERATIONS_PATH): + for operation in filter(lambda x: x.endswith(OPERATION_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): + if operation not in file_name_list and file_name_list: continue - with open(os.path.join(JOURNALS_PATH, category, file_name), "r") as content: + file_name = operation + + with open(os.path.join(OPERATIONS_PATH, category, file_name), "r") as content: content = content.read() - journal = journal[:-len(".journal")] - journal = journal.split("_") - journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") + operation = operation[:-len(OPERATION_FILE_EXT)] + operation = operation.split("_") + operation_datetime = datetime.strptime(" ".join(operation[-2:]), "%Y-%m-%d %H-%M-%S") infos, logs = content.split("\n---\n", 1) infos = yaml.safe_load(infos) logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] - return { - "started_at": journal_datetime, - "name": " ".join(journal[:-2]), + result['logs'].append({ + "started_at": operation_datetime, + "name": " ".join(operation[:-2]), "file_name": file_name, - "path": os.path.join(JOURNALS_PATH, category, file_name), + "path": os.path.join(OPERATIONS_PATH, category, file_name), "metadata": infos, "logs": logs, - } + }) - raise MoulinetteError(errno.EINVAL, - m18n.n('journal_does_exists', journal=file_name)) + logger.debug("====> %s", len(file_name_list), exc_info=1) + if len(file_name_list) > 0 and len(result['logs']) < len(file_name_list): + logger.error(m18n.n('log_does_exists', log="', '".join(file_name_list))) + if len(result['logs']) > 0: + return result class Journal(object): def __init__(self, name, category, on_stdout=None, on_stderr=None, on_write=None, **kwargs): @@ -129,7 +137,7 @@ class Journal(object): # this help uniformise file name and avoir threads concurrency errors self.started_at = datetime.now() - self.path = os.path.join(JOURNALS_PATH, category) + self.path = os.path.join(OPERATIONS_PATH, category) self.fd = None @@ -157,7 +165,8 @@ class Journal(object): if not os.path.exists(self.path): os.makedirs(self.path) - file_name = "%s_%s.journal" % (self.name if isinstance(self.name, basestring) else "_".join(self.name), self.started_at.strftime("%F_%X").replace(":", "-")) + file_name = "%s_%s" % (self.name if isinstance(self.name, basestring) else "_".join(self.name), self.started_at.strftime("%F_%X").replace(":", "-")) + file_name += OPERATION_FILE_EXT serialized_additional_information = yaml.safe_dump(self.additional_information, default_flow_style=False) From d310540a6f52727bf2475d0a9bc59d13628875e8 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 10 Apr 2018 15:42:48 +0200 Subject: [PATCH 028/103] [enh] Set the date before operation name --- src/yunohost/log.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 7c1f3b2a8..6fa7ae489 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -62,11 +62,11 @@ def log_list(limit=None): operation = operation[:-len(OPERATION_FILE_EXT)] operation = operation.split("_") - operation_datetime = datetime.strptime(" ".join(operation[-2:]), "%Y-%m-%d %H-%M-%S") + operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y-%m-%d %H-%M-%S") result["categories"][-1]["logs"].append({ "started_at": operation_datetime, - "name": " ".join(operation[:-2]), + "name": " ".join(operation[-2:]), "file_name": file_name, "path": os.path.join(OPERATIONS_PATH, category, file_name), }) @@ -105,7 +105,7 @@ def log_display(file_name_list): operation = operation[:-len(OPERATION_FILE_EXT)] operation = operation.split("_") - operation_datetime = datetime.strptime(" ".join(operation[-2:]), "%Y-%m-%d %H-%M-%S") + operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y-%m-%d %H-%M-%S") infos, logs = content.split("\n---\n", 1) infos = yaml.safe_load(infos) @@ -113,14 +113,13 @@ def log_display(file_name_list): result['logs'].append({ "started_at": operation_datetime, - "name": " ".join(operation[:-2]), + "name": " ".join(operation[-2:]), "file_name": file_name, "path": os.path.join(OPERATIONS_PATH, category, file_name), "metadata": infos, "logs": logs, }) - logger.debug("====> %s", len(file_name_list), exc_info=1) if len(file_name_list) > 0 and len(result['logs']) < len(file_name_list): logger.error(m18n.n('log_does_exists', log="', '".join(file_name_list))) @@ -165,7 +164,7 @@ class Journal(object): if not os.path.exists(self.path): os.makedirs(self.path) - file_name = "%s_%s" % (self.name if isinstance(self.name, basestring) else "_".join(self.name), self.started_at.strftime("%F_%X").replace(":", "-")) + file_name = "%s_%s" % (self.started_at.strftime("%F_%X").replace(":", "-"), self.name if isinstance(self.name, basestring) else "_".join(self.name)) file_name += OPERATION_FILE_EXT serialized_additional_information = yaml.safe_dump(self.additional_information, default_flow_style=False) From fb27b4d5f213ee309e831152dbcde435c07d59a1 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 10 Apr 2018 16:28:31 +0200 Subject: [PATCH 029/103] [enh] Sort logs display by start date --- src/yunohost/log.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 6fa7ae489..83fcc032d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -124,6 +124,7 @@ def log_display(file_name_list): logger.error(m18n.n('log_does_exists', log="', '".join(file_name_list))) if len(result['logs']) > 0: + result['logs'] = sorted(result['logs'], key=lambda operation: operation['started_at']) return result class Journal(object): From c1bd302973ed4b2c3b147fd93eab09a746ccfc1e Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 11 Apr 2018 09:31:40 +0200 Subject: [PATCH 030/103] [enh] Rename some logs into operations --- src/yunohost/log.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 83fcc032d..389f446a6 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -54,7 +54,7 @@ def log_list(limit=None): return result for category in sorted(os.listdir(OPERATIONS_PATH)): - result["categories"].append({"name": category, "logs": []}) + result["categories"].append({"name": category, "operations": []}) for operation in filter(lambda x: x.endswith(OPERATION_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): file_name = operation @@ -64,17 +64,17 @@ def log_list(limit=None): operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y-%m-%d %H-%M-%S") - result["categories"][-1]["logs"].append({ + result["categories"][-1]["operations"].append({ "started_at": operation_datetime, "name": " ".join(operation[-2:]), "file_name": file_name, "path": os.path.join(OPERATIONS_PATH, category, file_name), }) - result["categories"][-1]["logs"] = list(reversed(sorted(result["categories"][-1]["logs"], key=lambda x: x["started_at"]))) + result["categories"][-1]["operations"] = list(reversed(sorted(result["categories"][-1]["operations"], key=lambda x: x["started_at"]))) if limit is not None: - result["categories"][-1]["logs"] = result["categories"][-1]["logs"][:limit] + result["categories"][-1]["operations"] = result["categories"][-1]["operations"][:limit] return result @@ -91,7 +91,7 @@ def log_display(file_name_list): raise MoulinetteError(errno.EINVAL, m18n.n('log_does_exists', log=" ".join(file_name_list))) - result = {"logs": []} + result = {"operations": []} for category in os.listdir(OPERATIONS_PATH): for operation in filter(lambda x: x.endswith(OPERATION_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): @@ -111,7 +111,7 @@ def log_display(file_name_list): infos = yaml.safe_load(infos) logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] - result['logs'].append({ + result['operations'].append({ "started_at": operation_datetime, "name": " ".join(operation[-2:]), "file_name": file_name, @@ -120,11 +120,11 @@ def log_display(file_name_list): "logs": logs, }) - if len(file_name_list) > 0 and len(result['logs']) < len(file_name_list): + if len(file_name_list) > 0 and len(result['operations']) < len(file_name_list): logger.error(m18n.n('log_does_exists', log="', '".join(file_name_list))) - if len(result['logs']) > 0: - result['logs'] = sorted(result['logs'], key=lambda operation: operation['started_at']) + if len(result['operations']) > 0: + result['operations'] = sorted(result['operations'], key=lambda operation: operation['started_at']) return result class Journal(object): From a22672ede84496cfa2f49438e8e3d58663802314 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 11 Apr 2018 10:03:20 +0200 Subject: [PATCH 031/103] [fix] Avoid operation log filename with space --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2ee96afd1..0e40470c5 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -786,7 +786,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): os.path.join(extracted_app_folder, 'scripts/remove'), args=[app_instance_name], env=env_dict_remove, user="root", journal = Journal( - ["remove", app_instance_name, "failed install"], + ["remove", app_instance_name, "failed_install"], "app", args=[app_instance_name], env=env_dict_remove, ), ) From 219f1c6262ccbef6829fafdd124691bfb42b522b Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 11 Apr 2018 22:49:26 +0200 Subject: [PATCH 032/103] [enh] Use Handler to write log --- src/yunohost/app.py | 40 ++++++------- src/yunohost/hook.py | 14 +---- src/yunohost/log.py | 128 +++++++++++++++++++++++++--------------- src/yunohost/service.py | 3 +- 4 files changed, 105 insertions(+), 80 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0e40470c5..1ad566bcb 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -422,6 +422,7 @@ def app_map(app=None, raw=False, user=None): return result +@is_unit_operation() def app_change_url(auth, app, domain, path): """ Modify the URL at which an application is installed. @@ -433,7 +434,6 @@ def app_change_url(auth, app, domain, path): """ from yunohost.hook import hook_exec, hook_callback - from yunohost.log import Journal installed = _is_installed(app) if not installed: @@ -497,8 +497,7 @@ def app_change_url(auth, app, domain, path): os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts"))) os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) - journal = Journal(["change_url", app], "app", args=args_list, env=env_dict) - if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root", journal=journal) != 0: + if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root") != 0: logger.error("Failed to change '%s' url." % app) # restore values modified by app_checkurl @@ -531,6 +530,7 @@ def app_change_url(auth, app, domain, path): hook_callback('post_app_change_url', args=args_list, env=env_dict) +@is_unit_operation() def app_upgrade(auth, app=[], url=None, file=None): """ Upgrade app @@ -616,8 +616,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) - journal = Journal(["upgrade", app_instance_name], "app", args=args_list, env=env_dict) - if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root", journal=journal) != 0: + if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root") != 0: logger.error(m18n.n('app_upgrade_failed', app=app_instance_name)) else: now = int(time.time()) @@ -660,8 +659,7 @@ def app_upgrade(auth, app=[], url=None, file=None): if is_api: return {"log": service_log('yunohost-api', number="100").values()[0]} - -def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): +def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, **kwargs): """ Install apps @@ -673,7 +671,9 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback - from yunohost.log import Journal + from yunohost.log import UnitOperationHandler + + uo_install = UnitOperationHandler('app_install', 'app', args=kwargs) # Fetch or extract sources try: @@ -762,11 +762,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): try: install_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/install'), - args=args_list, env=env_dict, user="root", - journal = Journal( - ["install", app_instance_name], - "app", args=args_list, env=env_dict - ), + args=args_list, env=env_dict, user="root" ) except (KeyboardInterrupt, EOFError): install_retcode = -1 @@ -774,6 +770,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): logger.exception(m18n.n('unexpected_error')) finally: if install_retcode != 0: + uo_install.close() if not no_remove_on_failure: # Setup environment for remove script env_dict_remove = {} @@ -782,18 +779,19 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) # Execute remove script + uo_remove = UnitOperationHandler('remove_on_failed_install', + 'app', args=env_dict_remove) + remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), - args=[app_instance_name], env=env_dict_remove, user="root", - journal = Journal( - ["remove", app_instance_name, "failed_install"], - "app", args=[app_instance_name], env=env_dict_remove, - ), + args=[app_instance_name], env=env_dict_remove, user="root" ) if remove_retcode != 0: logger.warning(m18n.n('app_not_properly_removed', app=app_instance_name)) + uo_remove.close() + # Clean tmp folders shutil.rmtree(app_setting_path) shutil.rmtree(extracted_app_folder) @@ -827,7 +825,10 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): hook_callback('post_app_install', args=args_list, env=env_dict) + uo_install.close() + +@is_unit_operation() def app_remove(auth, app): """ Remove app @@ -863,8 +864,7 @@ def app_remove(auth, app): env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - journal = Journal(["remove", app], "app", args=args_list, env=env_dict) - if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root", journal=journal) == 0: + if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0: logger.success(m18n.n('app_removed', app=app)) hook_callback('post_app_remove', args=args_list, env=env_dict) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 95ed2aec4..95025d827 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -297,7 +297,7 @@ 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, - chdir=None, env=None, journal=None, user="admin"): + chdir=None, env=None, user="admin"): """ Execute hook from a file with arguments @@ -359,18 +359,6 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, else: logger.info(m18n.n('executing_script', script=path)) - if journal is None: - # Define output callbacks and call command - callbacks = ( - lambda l: logger.info(l.rstrip()), - lambda l: logger.warning(l.rstrip()), - ) - else: - callbacks = journal.as_callbacks_tuple( - stdout=lambda l: logger.info(l.rstrip()), - stderr=lambda l: logger.warning(l.rstrip()), - ) - returncode = call_async_output( command, callbacks, shell=False, cwd=chdir ) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 389f446a6..6d51f62c4 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -27,8 +27,11 @@ import os import yaml import errno +import logging from datetime import datetime +from logging import StreamHandler, getLogger, Formatter +from sys import exc_info from moulinette import m18n from moulinette.core import MoulinetteError @@ -39,7 +42,6 @@ OPERATION_FILE_EXT = '.yml' logger = getActionLogger('yunohost.log') - def log_list(limit=None): """ List available logs @@ -127,71 +129,105 @@ def log_display(file_name_list): result['operations'] = sorted(result['operations'], key=lambda operation: operation['started_at']) return result -class Journal(object): - def __init__(self, name, category, on_stdout=None, on_stderr=None, on_write=None, **kwargs): +def is_unit_operation(categorie=None, description_key=None): + def decorate(func): + def func_wrapper(*args, **kwargs): + cat = categorie + desc_key = description_key + + if cat is None: + cat = func.__module__.split('.')[1] + if desc_key is None: + desc_key = func.__name__ + uo = UnitOperationHandler(desc_key, cat, args=kwargs) + try: + result = func(*args, **kwargs) + finally: + uo.close(exc_info()[0]) + return result + return func_wrapper + return decorate + +class UnitOperationHandler(StreamHandler): + def __init__(self, name, category, **kwargs): # TODO add a way to not save password on app installation - self.name = name + self._name = name self.category = category self.first_write = True + self.closed = False # this help uniformise file name and avoir threads concurrency errors self.started_at = datetime.now() self.path = os.path.join(OPERATIONS_PATH, category) - self.fd = None + if not os.path.exists(self.path): + os.makedirs(self.path) - self.on_stdout = [] if on_stdout is None else on_stdout - self.on_stderr = [] if on_stderr is None else on_stderr - self.on_write = [] if on_write is None else on_write + self.filename = "%s_%s" % (self.started_at.strftime("%F_%X").replace(":", "-"), self._name if isinstance(self._name, basestring) else "_".join(self._name)) + self.filename += OPERATION_FILE_EXT self.additional_information = kwargs - def __del__(self): - if self.fd: - self.fd.close() + logging.StreamHandler.__init__(self, self._open()) - def write(self, line): + self.formatter = Formatter('%(asctime)s: %(levelname)s - %(message)s') + + if self.stream is None: + self.stream = self._open() + + # Listen to the root logger + self.logger = getLogger('yunohost') + self.logger.addHandler(self) + + + def _open(self): + stream = open(os.path.join(self.path, self.filename), "w") + return stream + + def close(self, error=None): + """ + Closes the stream. + """ + if self.closed: + return + self.acquire() + #self.ended_at = datetime.now() + #self.error = error + #self.stream.seek(0) + #context = { + # 'ended_at': datetime.now() + #} + #if error is not None: + # context['error'] = error + #self.stream.write(yaml.safe_dump(context)) + self.logger.removeHandler(self) + try: + if self.stream: + try: + self.flush() + finally: + stream = self.stream + self.stream = None + if hasattr(stream, "close"): + stream.close() + finally: + self.release() + self.closed = True + + def __del__(self): + self.close() + + def emit(self, record): if self.first_write: self._do_first_write() self.first_write = False - self.fd.write("%s: " % datetime.now().strftime("%F %X")) - self.fd.write(line.rstrip()) - self.fd.write("\n") - self.fd.flush() + StreamHandler.emit(self, record) def _do_first_write(self): - if not os.path.exists(self.path): - os.makedirs(self.path) - - file_name = "%s_%s" % (self.started_at.strftime("%F_%X").replace(":", "-"), self.name if isinstance(self.name, basestring) else "_".join(self.name)) - file_name += OPERATION_FILE_EXT serialized_additional_information = yaml.safe_dump(self.additional_information, default_flow_style=False) - self.fd = open(os.path.join(self.path, file_name), "w") - - self.fd.write(serialized_additional_information) - self.fd.write("\n---\n") - - def stdout(self, line): - for i in self.on_stdout: - i(line) - - self.write(line) - - def stderr(self, line): - for i in self.on_stderr: - i(line) - - self.write(line) - - def as_callbacks_tuple(self, stdout=None, stderr=None): - if stdout: - self.on_stdout.append(stdout) - - if stderr: - self.on_stderr.append(stderr) - - return (self.stdout, self.stderr) + self.stream.write(serialized_additional_information) + self.stream.write("\n---\n") diff --git a/src/yunohost/service.py b/src/yunohost/service.py index f0948c961..4e264e310 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -38,6 +38,7 @@ from moulinette.core import MoulinetteError from moulinette.utils import log, filesystem from yunohost.hook import hook_callback +from yunohost.log import is_unit_operation BASE_CONF_PATH = '/home/yunohost.conf' BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup') @@ -141,7 +142,7 @@ def service_stop(names): m18n.n('service_stop_failed', service=name)) logger.info(m18n.n('service_already_stopped', service=name)) - +@is_unit_operation() def service_enable(names): """ Enable one or more services From fac50997dceefbe0e4a8243765bb8f766a0e0d65 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 11 Apr 2018 22:54:46 +0200 Subject: [PATCH 033/103] [fix] Some missing --- src/yunohost/app.py | 1 - src/yunohost/hook.py | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 1ad566bcb..1e6ac3a82 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -542,7 +542,6 @@ def app_upgrade(auth, app=[], url=None, file=None): """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback - from yunohost.log import Journal # Retrieve interface is_api = msettings.get('interface') == 'api' diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 95025d827..89defe55e 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -359,6 +359,12 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, else: logger.info(m18n.n('executing_script', script=path)) + # Define output callbacks and call command + callbacks = ( + lambda l: logger.info(l.rstrip()), + lambda l: logger.warning(l.rstrip()), + ) + returncode = call_async_output( command, callbacks, shell=False, cwd=chdir ) From 6c063112bfcb74c67c130f876e9c2834297f3028 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 11 Apr 2018 22:58:26 +0200 Subject: [PATCH 034/103] [fix] Some missing --- src/yunohost/app.py | 1 + src/yunohost/hook.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 1e6ac3a82..1024b8b28 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -658,6 +658,7 @@ def app_upgrade(auth, app=[], url=None, file=None): if is_api: return {"log": service_log('yunohost-api', number="100").values()[0]} + def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, **kwargs): """ Install apps diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 89defe55e..1f971edb6 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -364,7 +364,6 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, lambda l: logger.info(l.rstrip()), lambda l: logger.warning(l.rstrip()), ) - returncode = call_async_output( command, callbacks, shell=False, cwd=chdir ) From b81d89f93ab56795700b9db9fe99aac11a50f77c Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 11 Apr 2018 23:01:24 +0200 Subject: [PATCH 035/103] [fix] Remove old ref to Journal --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 1024b8b28..2bfca443e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -44,6 +44,7 @@ from moulinette.utils.log import getActionLogger from yunohost.service import service_log, _run_service_command from yunohost.utils import packages +from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.app') @@ -838,7 +839,6 @@ def app_remove(auth, app): """ from yunohost.hook import hook_exec, hook_remove, hook_callback - from yunohost.log import Journal if not _is_installed(app): raise MoulinetteError(errno.EINVAL, From 79ee0396d0e301c4e41493b392f723190d1419aa Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 12 Apr 2018 21:22:17 +0200 Subject: [PATCH 036/103] [enh] Add related info --- locales/en.json | 1 + src/yunohost/app.py | 66 ++++++++---- src/yunohost/certificate.py | 2 +- src/yunohost/domain.py | 3 + src/yunohost/dyndns.py | 14 ++- src/yunohost/log.py | 207 ++++++++++++++++++++---------------- src/yunohost/tools.py | 24 ++++- src/yunohost/user.py | 4 + 8 files changed, 200 insertions(+), 121 deletions(-) diff --git a/locales/en.json b/locales/en.json index 27946e18d..9f310c3b9 100644 --- a/locales/en.json +++ b/locales/en.json @@ -206,6 +206,7 @@ "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", + "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", "ldap_initialized": "LDAP has been initialized", "license_undefined": "undefined", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2bfca443e..f83a11222 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -188,6 +188,7 @@ def app_fetchlist(url=None, name=None): _write_appslist_list(appslists) +@is_unit_operation() def app_removelist(name): """ Remove list from the repositories @@ -423,8 +424,8 @@ def app_map(app=None, raw=False, user=None): return result -@is_unit_operation() -def app_change_url(auth, app, domain, path): +@is_unit_operation(lazy=True) +def app_change_url(uo, auth, app, domain, path): """ Modify the URL at which an application is installed. @@ -481,6 +482,9 @@ def app_change_url(auth, app, domain, path): env_dict["YNH_APP_NEW_DOMAIN"] = domain env_dict["YNH_APP_NEW_PATH"] = path.rstrip("/") + uo.extra.update({'env': env_dict}) + uo.start() + if os.path.exists(os.path.join(APP_TMP_FOLDER, "scripts")): shutil.rmtree(os.path.join(APP_TMP_FOLDER, "scripts")) @@ -499,13 +503,14 @@ def app_change_url(auth, 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, user="root") != 0: - logger.error("Failed to change '%s' url." % app) + msg = "Failed to change '%s' url." % app + logger.error(msg) + uo.error(msg) # restore values modified by app_checkurl # see begining of the function app_setting(app, "domain", value=old_domain) app_setting(app, "path", value=old_path) - return # this should idealy be done in the change_url script but let's avoid common mistakes @@ -531,7 +536,6 @@ def app_change_url(auth, app, domain, path): hook_callback('post_app_change_url', args=args_list, env=env_dict) -@is_unit_operation() def app_upgrade(auth, app=[], url=None, file=None): """ Upgrade app @@ -614,10 +618,15 @@ def app_upgrade(auth, app=[], url=None, file=None): env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + # Start register change on system + uo = UnitOperation('app_upgrade', 'app', app_instance_name, env=env_dict) + # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root") != 0: - logger.error(m18n.n('app_upgrade_failed', app=app_instance_name)) + msg = m18n.n('app_upgrade_failed', app=app_instance_name) + logger.error(msg) + uo.error(msg) else: now = int(time.time()) # TODO: Move install_time away from app_setting @@ -646,7 +655,7 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.success(m18n.n('app_upgraded', app=app_instance_name)) hook_callback('post_app_upgrade', args=args_list, env=env_dict) - + uo.success() if not upgraded_apps: raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) @@ -660,7 +669,8 @@ def app_upgrade(auth, app=[], url=None, file=None): return {"log": service_log('yunohost-api', number="100").values()[0]} -def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, **kwargs): +@is_unit_operation(lazy=True) +def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False): """ Install apps @@ -672,9 +682,8 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, ** """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback - from yunohost.log import UnitOperationHandler + from yunohost.log import UnitOperation - uo_install = UnitOperationHandler('app_install', 'app', args=kwargs) # Fetch or extract sources try: @@ -732,6 +741,10 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, ** env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) + # Start register change on system + uo.extra.update({'env':env_dict}) + uo.start() + # Create app directory app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) if os.path.exists(app_setting_path): @@ -771,7 +784,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, ** logger.exception(m18n.n('unexpected_error')) finally: if install_retcode != 0: - uo_install.close() + uo.error(m18n.n('unexpected_error')) if not no_remove_on_failure: # Setup environment for remove script env_dict_remove = {} @@ -780,18 +793,21 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, ** env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) # Execute remove script - uo_remove = UnitOperationHandler('remove_on_failed_install', - 'app', args=env_dict_remove) + uo_remove = UnitOperation('remove_on_failed_install', + 'app', app_instance_name, + env=env_dict_remove) remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), args=[app_instance_name], env=env_dict_remove, user="root" ) if remove_retcode != 0: - logger.warning(m18n.n('app_not_properly_removed', - app=app_instance_name)) - - uo_remove.close() + msg = m18n.n('app_not_properly_removed', + app=app_instance_name) + logger.warning(msg) + uo_remove.error(msg) + else: + uo_remove.success() # Clean tmp folders shutil.rmtree(app_setting_path) @@ -826,11 +842,9 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, ** hook_callback('post_app_install', args=args_list, env=env_dict) - uo_install.close() - -@is_unit_operation() -def app_remove(auth, app): +@is_unit_operation(lazy=True) +def app_remove(uo, auth, app): """ Remove app @@ -839,11 +853,12 @@ def app_remove(auth, app): """ from yunohost.hook import hook_exec, hook_remove, hook_callback - if not _is_installed(app): raise MoulinetteError(errno.EINVAL, m18n.n('app_not_installed', app=app)) + uo.start() + app_setting_path = APPS_SETTING_PATH + app # TODO: display fail messages from script @@ -863,6 +878,8 @@ def app_remove(auth, app): env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + uo.extra.update({'env': env_dict}) + uo.flush() if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0: logger.success(m18n.n('app_removed', app=app)) @@ -1034,7 +1051,8 @@ def app_debug(app): } -def app_makedefault(auth, app, domain=None): +@is_unit_operation(lazy=True) +def app_makedefault(uo, auth, app, domain=None): """ Redirect domain root to an app @@ -1051,9 +1069,11 @@ def app_makedefault(auth, app, domain=None): if domain is None: domain = app_domain + uo.related_to['domain']=[domain] elif domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + uo.start() if '/' in app_map(raw=True)[domain]: raise MoulinetteError(errno.EEXIST, m18n.n('app_make_default_location_already_used', diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 310c5d131..a242f51b0 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -49,7 +49,7 @@ from yunohost.utils.network import get_public_ip from moulinette import m18n from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command, service_regen_conf - +from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.certmanager') diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 026c4da36..774ab928e 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -38,6 +38,7 @@ import yunohost.certificate from yunohost.service import service_regen_conf from yunohost.utils.network import get_public_ip +from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.domain') @@ -62,6 +63,7 @@ def domain_list(auth): return {'domains': result_list} +@is_unit_operation() def domain_add(auth, domain, dyndns=False): """ Create a custom domain @@ -127,6 +129,7 @@ def domain_add(auth, domain, dyndns=False): logger.success(m18n.n('domain_created')) +@is_unit_operation() def domain_remove(auth, domain, force=False): """ Delete domains diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index ec3bf88c8..739f2da9e 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -41,6 +41,7 @@ from moulinette.utils.network import download_json from yunohost.domain import _get_maindomain, _build_dns_conf from yunohost.utils.network import get_public_ip +from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.dyndns') @@ -113,7 +114,8 @@ def _dyndns_available(provider, domain): return r == u"Domain %s is available" % domain -def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None): +@is_unit_operation('domain', lazy=True) +def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key=None): """ Subscribe to a DynDNS service @@ -126,6 +128,10 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None if domain is None: domain = _get_maindomain() + uo.on = [domain] + uo.related_to['domain'] = [domain] + uo.start() + # Verify if domain is provided by subscribe_host if not _dyndns_provides(subscribe_host, domain): raise MoulinetteError(errno.ENOENT, @@ -170,7 +176,8 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None dyndns_installcron() -def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, +@is_unit_operation('domain',lazy=True) +def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, ipv4=None, ipv6=None): """ Update IP on DynDNS platform @@ -212,6 +219,9 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, return else: logger.info("Updated needed, going on...") + uo.on = [domain] + uo.related_to['domain'] = [domain] + uo.start() # If domain is not given, try to guess it from keys available... if domain is None: diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 6d51f62c4..fbf2fc232 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -27,10 +27,9 @@ import os import yaml import errno -import logging from datetime import datetime -from logging import StreamHandler, getLogger, Formatter +from logging import FileHandler, getLogger, Formatter from sys import exc_info from moulinette import m18n @@ -38,7 +37,9 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger OPERATIONS_PATH = '/var/log/yunohost/operation/' -OPERATION_FILE_EXT = '.yml' +METADATA_FILE_EXT = '.yml' +LOG_FILE_EXT = '.log' +RELATED_CATEGORIES = ['app', 'domain', 'service', 'user'] logger = getActionLogger('yunohost.log') @@ -57,11 +58,11 @@ def log_list(limit=None): for category in sorted(os.listdir(OPERATIONS_PATH)): result["categories"].append({"name": category, "operations": []}) - for operation in filter(lambda x: x.endswith(OPERATION_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): + for operation in filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): file_name = operation - operation = operation[:-len(OPERATION_FILE_EXT)] + operation = operation[:-len(METADATA_FILE_EXT)] operation = operation.split("_") operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y-%m-%d %H-%M-%S") @@ -96,31 +97,27 @@ def log_display(file_name_list): result = {"operations": []} for category in os.listdir(OPERATIONS_PATH): - for operation in filter(lambda x: x.endswith(OPERATION_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): + for operation in filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): if operation not in file_name_list and file_name_list: continue - file_name = operation + file_name = operation[:-len(METADATA_FILE_EXT)] + operation = file_name.split("_") - with open(os.path.join(OPERATIONS_PATH, category, file_name), "r") as content: - content = content.read() + with open(os.path.join(OPERATIONS_PATH, category, file_name + METADATA_FILE_EXT), "r") as md_file: + try: + infos = yaml.safe_load(md_file) + except yaml.YAMLError as exc: + print(exc) - operation = operation[:-len(OPERATION_FILE_EXT)] - operation = operation.split("_") - operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y-%m-%d %H-%M-%S") - - infos, logs = content.split("\n---\n", 1) - infos = yaml.safe_load(infos) + with open(os.path.join(OPERATIONS_PATH, category, file_name + LOG_FILE_EXT), "r") as content: + logs = content.read() logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] - - result['operations'].append({ - "started_at": operation_datetime, - "name": " ".join(operation[-2:]), - "file_name": file_name, - "path": os.path.join(OPERATIONS_PATH, category, file_name), - "metadata": infos, - "logs": logs, - }) + infos['logs'] = logs + infos['name'] = " ".join(operation[-2:]) + infos['file_name'] = file_name + METADATA_FILE_EXT + infos['path'] = os.path.join(OPERATIONS_PATH, category, file_name) + result['operations'].append(infos) if len(file_name_list) > 0 and len(result['operations']) < len(file_name_list): logger.error(m18n.n('log_does_exists', log="', '".join(file_name_list))) @@ -129,105 +126,133 @@ def log_display(file_name_list): result['operations'] = sorted(result['operations'], key=lambda operation: operation['started_at']) return result -def is_unit_operation(categorie=None, description_key=None): +def is_unit_operation(categorie=None, operation_key=None, lazy=False): def decorate(func): def func_wrapper(*args, **kwargs): cat = categorie - desc_key = description_key + op_key = operation_key + on = None + related_to = {} + inject = lazy + to_start = not lazy if cat is None: cat = func.__module__.split('.')[1] - if desc_key is None: - desc_key = func.__name__ - uo = UnitOperationHandler(desc_key, cat, args=kwargs) + if op_key is None: + op_key = func.__name__ + if cat in kwargs: + on = kwargs[cat] + for r_category in RELATED_CATEGORIES: + if r_category in kwargs and kwargs[r_category] is not None: + if r_category not in related_to: + related_to[r_category] = [] + if isinstance(kwargs[r_category], basestring): + related_to[r_category] += [kwargs[r_category]] + else: + related_to[r_category] += kwargs[r_category] + context = kwargs.copy() + if 'auth' in context: + context.pop('auth', None) + uo = UnitOperation(op_key, cat, on, related_to, args=context) + if to_start: + uo.start() try: + if inject: + args = (uo,) + args result = func(*args, **kwargs) finally: - uo.close(exc_info()[0]) + if uo.started_at is not None: + uo.close(exc_info()[0]) return result return func_wrapper return decorate -class UnitOperationHandler(StreamHandler): - def __init__(self, name, category, **kwargs): +class UnitOperation(object): + def __init__(self, operation, category, on=None, related_to=None, **kwargs): # TODO add a way to not save password on app installation - self._name = name + self.operation = operation self.category = category - self.first_write = True - self.closed = False + self.on = on + if isinstance(self.on, basestring): + self.on = [self.on] - # this help uniformise file name and avoir threads concurrency errors - self.started_at = datetime.now() + self.related_to = related_to + if related_to is None: + if self.category in RELATED_CATEGORIES: + self.related_to = {self.category: self.on} + self.extra = kwargs + self.started_at = None + self.ended_at = None + self.logger = None self.path = os.path.join(OPERATIONS_PATH, category) if not os.path.exists(self.path): os.makedirs(self.path) - self.filename = "%s_%s" % (self.started_at.strftime("%F_%X").replace(":", "-"), self._name if isinstance(self._name, basestring) else "_".join(self._name)) - self.filename += OPERATION_FILE_EXT + def start(self): + if self.started_at is None: + self.started_at = datetime.now() + self.flush() + self._register_log() - self.additional_information = kwargs - - logging.StreamHandler.__init__(self, self._open()) - - self.formatter = Formatter('%(asctime)s: %(levelname)s - %(message)s') - - if self.stream is None: - self.stream = self._open() + def _register_log(self): + # TODO add a way to not save password on app installation + filename = os.path.join(self.path, self.name + LOG_FILE_EXT) + self.file_handler = FileHandler(filename) + self.file_handler.formatter = Formatter('%(asctime)s: %(levelname)s - %(message)s') # Listen to the root logger self.logger = getLogger('yunohost') - self.logger.addHandler(self) + self.logger.addHandler(self.file_handler) + def flush(self): + filename = os.path.join(self.path, self.name + METADATA_FILE_EXT) + with open(filename, 'w') as outfile: + yaml.safe_dump(self.metadata, outfile, default_flow_style=False) - def _open(self): - stream = open(os.path.join(self.path, self.filename), "w") - return stream + @property + def name(self): + name = [self.started_at.strftime("%F_%X").replace(":", "-")] + name += [self.operation] + if self.on is not None: + name += self.on + return '_'.join(name) - def close(self, error=None): - """ - Closes the stream. - """ - if self.closed: - return - self.acquire() - #self.ended_at = datetime.now() - #self.error = error - #self.stream.seek(0) - #context = { - # 'ended_at': datetime.now() - #} - #if error is not None: - # context['error'] = error - #self.stream.write(yaml.safe_dump(context)) - self.logger.removeHandler(self) - try: - if self.stream: - try: - self.flush() - finally: - stream = self.stream - self.stream = None - if hasattr(stream, "close"): - stream.close() - finally: - self.release() - self.closed = True + @property + def metadata(self): + data = { + 'started_at': self.started_at, + 'operation': self.operation, + 'related_to': self.related_to + } + if self.on is not None: + data['on'] = self.on + if self.ended_at is not None: + data['ended_at'] = self.ended_at + data['success'] = self._success + if self.error is not None: + data['error'] = self._error + # TODO: detect if 'extra' erase some key of 'data' + data.update(self.extra) + return data - def __del__(self): + def success(self): self.close() - def emit(self, record): - if self.first_write: - self._do_first_write() - self.first_write = False + def error(self, error): + self.close(error) - StreamHandler.emit(self, record) + def close(self, error=None): + if self.ended_at is not None or self.started_at is None: + return + self.ended_at = datetime.now() + self._error = error + self._success = error is None + if self.logger is not None: + self.logger.removeHandler(self.file_handler) + self.flush() - def _do_first_write(self): + def __del__(self): + self.error(m18n.n('log_operation_unit_unclosed_properly')) - serialized_additional_information = yaml.safe_dump(self.additional_information, default_flow_style=False) - - self.stream.write(serialized_additional_information) - self.stream.write("\n---\n") diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 381cd07e0..ba942d6e4 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -52,6 +52,7 @@ from yunohost.service import service_status, service_regen_conf, service_log, se from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version from yunohost.utils.network import get_public_ip +from yunohost.log import is_unit_operation # FIXME this is a duplicate from apps.py APPS_SETTING_PATH = '/etc/yunohost/apps/' @@ -138,7 +139,8 @@ def tools_adminpw(auth, new_password): logger.success(m18n.n('admin_password_changed')) -def tools_maindomain(auth, new_domain=None): +@is_unit_operation('domain', lazy=True) +def tools_maindomain(uo, auth, new_domain=None): """ Check the current main domain, or change it @@ -155,6 +157,10 @@ def tools_maindomain(auth, new_domain=None): if new_domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + uo.on = [new_domain] + uo.related_to['domain'] = [new_domain] + uo.start() + # Apply changes to ssl certs ssl_key = "/etc/ssl/private/yunohost_key.pem" ssl_crt = "/etc/ssl/private/yunohost_crt.pem" @@ -244,6 +250,7 @@ def _is_inside_container(): return out.split()[1] != "(1," +@is_unit_operation() def tools_postinstall(domain, password, ignore_dyndns=False): """ YunoHost post-install @@ -464,7 +471,8 @@ def tools_update(ignore_apps=False, ignore_packages=False): return {'packages': packages, 'apps': apps} -def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): +@is_unit_operation(lazy=True) +def tools_upgrade(uo, auth, ignore_apps=False, ignore_packages=False): """ Update apps & package cache, then display changelog @@ -505,6 +513,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): if cache.get_changes(): logger.info(m18n.n('upgrading_packages')) + uo.start() try: # Apply APT changes # TODO: Logs output for the API @@ -514,11 +523,14 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): failure = True logger.warning('unable to upgrade packages: %s' % str(e)) logger.error(m18n.n('packages_upgrade_failed')) + uo.error(m18n.n('packages_upgrade_failed')) else: logger.info(m18n.n('done')) + uo.success() else: logger.info(m18n.n('packages_no_upgrade')) + if not ignore_apps: try: app_upgrade(auth) @@ -699,7 +711,8 @@ def tools_port_available(port): return False -def tools_shutdown(force=False): +@is_unit_operation(lazy=True) +def tools_shutdown(uo, force=False): shutdown = force if not shutdown: try: @@ -712,11 +725,13 @@ def tools_shutdown(force=False): shutdown = True if shutdown: + uo.start() logger.warn(m18n.n('server_shutdown')) subprocess.check_call(['systemctl', 'poweroff']) -def tools_reboot(force=False): +@is_unit_operation(lazy=True) +def tools_reboot(uo, force=False): reboot = force if not reboot: try: @@ -728,6 +743,7 @@ def tools_reboot(force=False): if i.lower() == 'y' or i.lower() == 'yes': reboot = True if reboot: + uo.start() logger.warn(m18n.n('server_reboot')) subprocess.check_call(['systemctl', 'reboot']) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 11f61d807..08946633e 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -36,6 +36,7 @@ from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from yunohost.service import service_status +from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.user') @@ -89,6 +90,7 @@ def user_list(auth, fields=None): return {'users': users} +@is_unit_operation() def user_create(auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ @@ -210,6 +212,7 @@ def user_create(auth, username, firstname, lastname, mail, password, raise MoulinetteError(169, m18n.n('user_creation_failed')) +@is_unit_operation() def user_delete(auth, username, purge=False): """ Delete user @@ -245,6 +248,7 @@ def user_delete(auth, username, purge=False): logger.success(m18n.n('user_deleted')) +@is_unit_operation() def user_update(auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): From a9d937db211871e26415125c5c75f86b9e55334b Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 13 Apr 2018 10:11:44 +0200 Subject: [PATCH 037/103] [enh] Display nice operation name --- locales/en.json | 18 ++++++++++++++++++ src/yunohost/log.py | 10 +++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index 9f310c3b9..47b3eefee 100644 --- a/locales/en.json +++ b/locales/en.json @@ -207,6 +207,24 @@ "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", + "log_app_removelist": "Remove an application list", + "log_app_change_url": "Change the url of '{}' application", + "log_app_install": "Install '{}' application", + "log_app_remove": "Remove '{}' application", + "log_app_upgrade": "Upgrade '{}' application", + "log_app_makedefault": "Make '{}' as default application", + "log_domain_add": "Add '{}' domain into system configuration", + "log_domain_remove": "Remove '{}' domain from system configuration", + "log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'", + "log_dyndns_update": "Update the ip associated with your YunoHost subdomain '{}'", + "log_user_create": "Add '{}' user", + "log_user_delete": "Delete '{}' user", + "log_user_update": "Update information of '{}' user", + "log_tools_maindomain": "Make '{}' as main domain", + "log_tools_postinstall": "Postinstall your YunoHost server", + "log_tools_upgrade": "Upgrade debian packages", + "log_tools_shutdown": "Shutdown your server", + "log_tools_reboot": "Reboot your server", "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", "ldap_initialized": "LDAP has been initialized", "license_undefined": "undefined", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index fbf2fc232..da6543c86 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -63,13 +63,13 @@ def log_list(limit=None): file_name = operation operation = operation[:-len(METADATA_FILE_EXT)] - operation = operation.split("_") + operation = operation.split("-") - operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y-%m-%d %H-%M-%S") + operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y%m%d %H%M%S") result["categories"][-1]["operations"].append({ "started_at": operation_datetime, - "name": " ".join(operation[-2:]), + "name": m18n.n("log_" + operation[2], *operation[3:]), "file_name": file_name, "path": os.path.join(OPERATIONS_PATH, category, file_name), }) @@ -213,11 +213,11 @@ class UnitOperation(object): @property def name(self): - name = [self.started_at.strftime("%F_%X").replace(":", "-")] + name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] if self.on is not None: name += self.on - return '_'.join(name) + return '-'.join(name) @property def metadata(self): From 73828306df0fa143a912690099fdf3a7f9e9dc50 Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 13 Apr 2018 11:27:31 +0200 Subject: [PATCH 038/103] [fix] Path log file with no extension --- src/yunohost/log.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index da6543c86..51fa85fa1 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -60,18 +60,19 @@ def log_list(limit=None): result["categories"].append({"name": category, "operations": []}) for operation in filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): - file_name = operation + base_filename = operation[:-len(METADATA_FILE_EXT)] + md_filename = operation + md_path = os.path.join(OPERATIONS_PATH, category, md_filename) - operation = operation[:-len(METADATA_FILE_EXT)] - operation = operation.split("-") + operation = base_filename.split("-") operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y%m%d %H%M%S") result["categories"][-1]["operations"].append({ "started_at": operation_datetime, - "name": m18n.n("log_" + operation[2], *operation[3:]), - "file_name": file_name, - "path": os.path.join(OPERATIONS_PATH, category, file_name), + "description": m18n.n("log_" + operation[2], *operation[3:]), + "name": base_filename, + "path": md_path, }) result["categories"][-1]["operations"] = list(reversed(sorted(result["categories"][-1]["operations"], key=lambda x: x["started_at"]))) @@ -101,22 +102,26 @@ def log_display(file_name_list): if operation not in file_name_list and file_name_list: continue - file_name = operation[:-len(METADATA_FILE_EXT)] - operation = file_name.split("_") + base_filename = operation[:-len(METADATA_FILE_EXT)] + md_filename = operation + md_path = os.path.join(OPERATIONS_PATH, category, md_filename) + log_filename = base_filename + LOG_FILE_EXT + log_path = os.path.join(OPERATIONS_PATH, category, log_filename) + operation = base_filename.split("-") - with open(os.path.join(OPERATIONS_PATH, category, file_name + METADATA_FILE_EXT), "r") as md_file: + with open(md_path, "r") as md_file: try: infos = yaml.safe_load(md_file) except yaml.YAMLError as exc: print(exc) - with open(os.path.join(OPERATIONS_PATH, category, file_name + LOG_FILE_EXT), "r") as content: + with open(log_path, "r") as content: logs = content.read() logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] infos['logs'] = logs - infos['name'] = " ".join(operation[-2:]) - infos['file_name'] = file_name + METADATA_FILE_EXT - infos['path'] = os.path.join(OPERATIONS_PATH, category, file_name) + infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), + infos['name'] = base_filename + infos['log_path'] = log_path result['operations'].append(infos) if len(file_name_list) > 0 and len(result['operations']) < len(file_name_list): From 650b768229e62a0c5b1bada0e6cb7b0584b5ff33 Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 14 Apr 2018 04:04:09 +0200 Subject: [PATCH 039/103] [enh] Replace category by related_to fields --- data/actionsmap/yunohost.yml | 10 +- src/yunohost/app.py | 17 ++-- src/yunohost/dyndns.py | 27 +++--- src/yunohost/log.py | 175 ++++++++++++++++------------------- src/yunohost/tools.py | 11 +-- src/yunohost/user.py | 6 +- 6 files changed, 118 insertions(+), 128 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index b30167b18..e57d44716 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1614,16 +1614,18 @@ log: action_help: List logs api: GET /logs arguments: - -l: + -l: full: --limit - help: Maximum number of logs per categories + help: Maximum number of logs type: int + --full: + help: Show more details + action: store_true ### log_display() display: action_help: Display a log content api: GET /logs/ arguments: - file_name_list: + file_name: help: Log filenames for which to display the content - nargs: "*" diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f83a11222..df36ccdf7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -424,7 +424,7 @@ def app_map(app=None, raw=False, user=None): return result -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def app_change_url(uo, auth, app, domain, path): """ Modify the URL at which an application is installed. @@ -482,6 +482,8 @@ def app_change_url(uo, auth, app, domain, path): env_dict["YNH_APP_NEW_DOMAIN"] = domain env_dict["YNH_APP_NEW_PATH"] = path.rstrip("/") + if domain != old_domain: + uo.related_to.append(('domain', old_domain)) uo.extra.update({'env': env_dict}) uo.start() @@ -619,7 +621,8 @@ def app_upgrade(auth, app=[], url=None, file=None): env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) # Start register change on system - uo = UnitOperation('app_upgrade', 'app', app_instance_name, env=env_dict) + related_to = [('app', app_instance_name)] + uo = UnitOperation('app_upgrade', related_to, env=env_dict) # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) @@ -669,7 +672,7 @@ def app_upgrade(auth, app=[], url=None, file=None): return {"log": service_log('yunohost-api', number="100").values()[0]} -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False): """ Install apps @@ -794,7 +797,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False # Execute remove script uo_remove = UnitOperation('remove_on_failed_install', - 'app', app_instance_name, + [('app', app_instance_name)], env=env_dict_remove) remove_retcode = hook_exec( @@ -843,7 +846,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False hook_callback('post_app_install', args=args_list, env=env_dict) -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def app_remove(uo, auth, app): """ Remove app @@ -1051,7 +1054,7 @@ def app_debug(app): } -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def app_makedefault(uo, auth, app, domain=None): """ Redirect domain root to an app @@ -1069,7 +1072,7 @@ def app_makedefault(uo, auth, app, domain=None): if domain is None: domain = app_domain - uo.related_to['domain']=[domain] + uo.related_to.append(('domain',domain)) elif domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 739f2da9e..438116fb1 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -114,7 +114,7 @@ def _dyndns_available(provider, domain): return r == u"Domain %s is available" % domain -@is_unit_operation('domain', lazy=True) +@is_unit_operation(auto=False) def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key=None): """ Subscribe to a DynDNS service @@ -127,9 +127,7 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= """ if domain is None: domain = _get_maindomain() - - uo.on = [domain] - uo.related_to['domain'] = [domain] + uo.related_to.append(('domain', domain)) uo.start() # Verify if domain is provided by subscribe_host @@ -176,7 +174,7 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= dyndns_installcron() -@is_unit_operation('domain',lazy=True) +@is_unit_operation(auto=False) def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, ipv4=None, ipv6=None): """ @@ -219,21 +217,24 @@ def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, return else: logger.info("Updated needed, going on...") - uo.on = [domain] - uo.related_to['domain'] = [domain] - uo.start() + if domain is not None: + uo.related_to.append(('domain', domain)) # If domain is not given, try to guess it from keys available... if domain is None: (domain, key) = _guess_current_dyndns_domain(dyn_host) + uo.related_to.append(('domain', domain)) + uo.start() # If key is not given, pick the first file we find with the domain given - elif key is None: - keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) + else: + uo.start() + if key is None: + keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) - if not keys: - raise MoulinetteError(errno.EIO, m18n.n('dyndns_key_not_found')) + if not keys: + raise MoulinetteError(errno.EIO, m18n.n('dyndns_key_not_found')) - key = keys[0] + key = keys[0] # This mean that hmac-md5 is used # (Re?)Trigger the migration to sha256 and return immediately. diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 51fa85fa1..2b9c17a37 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -43,154 +43,140 @@ RELATED_CATEGORIES = ['app', 'domain', 'service', 'user'] logger = getActionLogger('yunohost.log') -def log_list(limit=None): +def log_list(limit=None, full=False): """ List available logs Keyword argument: - limit -- Maximum number of logs per categories + limit -- Maximum number of logs """ - result = {"categories": []} + result = {"operations": []} if not os.path.exists(OPERATIONS_PATH): return result - for category in sorted(os.listdir(OPERATIONS_PATH)): - result["categories"].append({"name": category, "operations": []}) - for operation in filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): + operations = filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(OPERATIONS_PATH)) + operations = reversed(sorted(operations)) - base_filename = operation[:-len(METADATA_FILE_EXT)] - md_filename = operation - md_path = os.path.join(OPERATIONS_PATH, category, md_filename) + if limit is not None: + operations = operations[:limit] - operation = base_filename.split("-") + for operation in operations: - operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y%m%d %H%M%S") + base_filename = operation[:-len(METADATA_FILE_EXT)] + md_filename = operation + md_path = os.path.join(OPERATIONS_PATH, md_filename) - result["categories"][-1]["operations"].append({ - "started_at": operation_datetime, - "description": m18n.n("log_" + operation[2], *operation[3:]), - "name": base_filename, - "path": md_path, - }) + operation = base_filename.split("-") - result["categories"][-1]["operations"] = list(reversed(sorted(result["categories"][-1]["operations"], key=lambda x: x["started_at"]))) + operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y%m%d %H%M%S") - if limit is not None: - result["categories"][-1]["operations"] = result["categories"][-1]["operations"][:limit] + result["operations"].append({ + "started_at": operation_datetime, + "description": m18n.n("log_" + operation[2], *operation[3:]), + "name": base_filename, + "path": md_path, + }) return result -def log_display(file_name_list): +def log_display(file_name): """ Display full log or specific logs listed Argument: - file_name_list + file_name """ - if not os.path.exists(OPERATIONS_PATH): + if file_name.endswith(METADATA_FILE_EXT): + base_filename = file_name[:-len(METADATA_FILE_EXT)] + elif file_name.endswith(LOG_FILE_EXT): + base_filename = file_name[:-len(LOG_FILE_EXT)] + else: + base_filename = file_name + md_filename = base_filename + METADATA_FILE_EXT + md_path = os.path.join(OPERATIONS_PATH, md_filename) + log_filename = base_filename + LOG_FILE_EXT + log_path = os.path.join(OPERATIONS_PATH, log_filename) + operation = base_filename.split("-") + + if not os.path.exists(md_path) and not os.path.exists(log_path): raise MoulinetteError(errno.EINVAL, - m18n.n('log_does_exists', log=" ".join(file_name_list))) + m18n.n('log_does_exists', log=file_name)) + infos = {} + infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), + infos['name'] = base_filename - result = {"operations": []} + if os.path.exists(md_path): + with open(md_path, "r") as md_file: + try: + metadata = yaml.safe_load(md_file) + infos['metadata_path'] = md_path + infos['metadata'] = metadata + except yaml.YAMLError as exc: + print(exc) - for category in os.listdir(OPERATIONS_PATH): - for operation in filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): - if operation not in file_name_list and file_name_list: - continue - - base_filename = operation[:-len(METADATA_FILE_EXT)] - md_filename = operation - md_path = os.path.join(OPERATIONS_PATH, category, md_filename) - log_filename = base_filename + LOG_FILE_EXT - log_path = os.path.join(OPERATIONS_PATH, category, log_filename) - operation = base_filename.split("-") - - with open(md_path, "r") as md_file: - try: - infos = yaml.safe_load(md_file) - except yaml.YAMLError as exc: - print(exc) - - with open(log_path, "r") as content: - logs = content.read() - logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] - infos['logs'] = logs - infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), - infos['name'] = base_filename + if os.path.exists(log_path): + with open(log_path, "r") as content: + logs = content.read() + logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] infos['log_path'] = log_path - result['operations'].append(infos) + infos['logs'] = logs - if len(file_name_list) > 0 and len(result['operations']) < len(file_name_list): - logger.error(m18n.n('log_does_exists', log="', '".join(file_name_list))) + return infos - if len(result['operations']) > 0: - result['operations'] = sorted(result['operations'], key=lambda operation: operation['started_at']) - return result - -def is_unit_operation(categorie=None, operation_key=None, lazy=False): +def is_unit_operation(entities='app,domain,service,user', exclude='auth,password', operation_key=None, auto=True): def decorate(func): def func_wrapper(*args, **kwargs): - cat = categorie + entities_list = entities.split(',') + exclude_list = exclude.split(',') op_key = operation_key - on = None - related_to = {} - inject = lazy - to_start = not lazy + related_to = [] - if cat is None: - cat = func.__module__.split('.')[1] if op_key is None: op_key = func.__name__ - if cat in kwargs: - on = kwargs[cat] - for r_category in RELATED_CATEGORIES: - if r_category in kwargs and kwargs[r_category] is not None: - if r_category not in related_to: - related_to[r_category] = [] - if isinstance(kwargs[r_category], basestring): - related_to[r_category] += [kwargs[r_category]] + + for entity in entities_list: + entity = entity.split(':') + entity_type = entity[-1] + entity = entity[0] + if entity in kwargs and kwargs[entity] is not None: + if isinstance(kwargs[entity], basestring): + related_to.append({entity_type: kwargs[entity]}) else: - related_to[r_category] += kwargs[r_category] + for x in kwargs[entity]: + related_to.append({entity_type: kwargs[x]}) + context = kwargs.copy() - if 'auth' in context: - context.pop('auth', None) - uo = UnitOperation(op_key, cat, on, related_to, args=context) - if to_start: + for field in exclude_list: + if field in context: + context.pop(field, None) + uo = UnitOperation(op_key, related_to, args=context) + if auto: uo.start() try: - if inject: + if not auto: args = (uo,) + args result = func(*args, **kwargs) finally: - if uo.started_at is not None: - uo.close(exc_info()[0]) + uo.close(exc_info()[0]) return result return func_wrapper return decorate class UnitOperation(object): - def __init__(self, operation, category, on=None, related_to=None, **kwargs): + def __init__(self, operation, related_to=None, **kwargs): # TODO add a way to not save password on app installation self.operation = operation - self.category = category - self.on = on - if isinstance(self.on, basestring): - self.on = [self.on] - self.related_to = related_to - if related_to is None: - if self.category in RELATED_CATEGORIES: - self.related_to = {self.category: self.on} self.extra = kwargs self.started_at = None self.ended_at = None self.logger = None - self.path = os.path.join(OPERATIONS_PATH, category) + self.path = OPERATIONS_PATH if not os.path.exists(self.path): os.makedirs(self.path) @@ -220,8 +206,8 @@ class UnitOperation(object): def name(self): name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] - if self.on is not None: - name += self.on + if self.related_to: + name += self.related_to[0].values() return '-'.join(name) @property @@ -229,10 +215,9 @@ class UnitOperation(object): data = { 'started_at': self.started_at, 'operation': self.operation, - 'related_to': self.related_to } - if self.on is not None: - data['on'] = self.on + if self.related_to is not None: + data['related_to'] = self.related_to if self.ended_at is not None: data['ended_at'] = self.ended_at data['success'] = self._success diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index ba942d6e4..eefe2d28d 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -139,7 +139,7 @@ def tools_adminpw(auth, new_password): logger.success(m18n.n('admin_password_changed')) -@is_unit_operation('domain', lazy=True) +@is_unit_operation(auto=False) def tools_maindomain(uo, auth, new_domain=None): """ Check the current main domain, or change it @@ -157,8 +157,7 @@ def tools_maindomain(uo, auth, new_domain=None): if new_domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) - uo.on = [new_domain] - uo.related_to['domain'] = [new_domain] + uo.related_to.append(('domain', new_domain)) uo.start() # Apply changes to ssl certs @@ -471,7 +470,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): return {'packages': packages, 'apps': apps} -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def tools_upgrade(uo, auth, ignore_apps=False, ignore_packages=False): """ Update apps & package cache, then display changelog @@ -711,7 +710,7 @@ def tools_port_available(port): return False -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def tools_shutdown(uo, force=False): shutdown = force if not shutdown: @@ -730,7 +729,7 @@ def tools_shutdown(uo, force=False): subprocess.check_call(['systemctl', 'poweroff']) -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def tools_reboot(uo, force=False): reboot = force if not reboot: diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 08946633e..2b9a70463 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -90,7 +90,7 @@ def user_list(auth, fields=None): return {'users': users} -@is_unit_operation() +@is_unit_operation('username:user') def user_create(auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ @@ -212,7 +212,7 @@ def user_create(auth, username, firstname, lastname, mail, password, raise MoulinetteError(169, m18n.n('user_creation_failed')) -@is_unit_operation() +@is_unit_operation('username:user') def user_delete(auth, username, purge=False): """ Delete user @@ -248,7 +248,7 @@ def user_delete(auth, username, purge=False): logger.success(m18n.n('user_deleted')) -@is_unit_operation() +@is_unit_operation('username:user', exclude='auth,change_password') def user_update(auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): From 2c30bb89659c38a7bb0a5772c6064cb42285737e Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 14 Apr 2018 04:14:14 +0200 Subject: [PATCH 040/103] [enh] Configure unit operations on cert management --- locales/en.json | 3 +++ src/yunohost/certificate.py | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/locales/en.json b/locales/en.json index 47b3eefee..930722a9e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -217,6 +217,9 @@ "log_domain_remove": "Remove '{}' domain from system configuration", "log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'", "log_dyndns_update": "Update the ip associated with your YunoHost subdomain '{}'", + "log_letsencrypt_cert_install": "Install Let's encrypt certificate on '{}' domain", + "log_selfsigned_cert_install": "Install self signed certificate on '{}' domain", + "log_letsencrypt_cert_renew": "Renew '{}' Let's encrypt certificate", "log_user_create": "Add '{}' user", "log_user_delete": "Delete '{}' user", "log_user_update": "Update information of '{}' user", diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index a242f51b0..69043bdbd 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -49,7 +49,7 @@ from yunohost.utils.network import get_public_ip from moulinette import m18n from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command, service_regen_conf -from yunohost.log import is_unit_operation +from yunohost.log import is_unit_operation, UnitOperation logger = getActionLogger('yunohost.certmanager') @@ -162,6 +162,10 @@ def _certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: + uo = UnitOperation('selfsigned_cert_install', [{'domain', domain}], + args={'force': force}) + uo.start() + # Paths of files and folder we'll need date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") new_cert_folder = "%s/%s-history/%s-selfsigned" % ( @@ -240,9 +244,11 @@ def _certificate_install_selfsigned(domain_list, force=False): if status and status["CA_type"]["code"] == "self-signed" and status["validity"] > 3648: logger.success( m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) + uo.success() else: - logger.error( - "Installation of self-signed certificate installation for %s failed !", domain) + msg = "Installation of self-signed certificate installation for %s failed !" % (domain) + logger.error(msg) + uo.error(msg) def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False, staging=False): @@ -281,6 +287,11 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F # Actual install steps for domain in domain_list: + uo = UnitOperation('letsencrypt_cert_install', [{'domain', domain}], + args={'force': force, 'no_checks': no_checks, + 'staging': staging}) + uo.start() + logger.info( "Now attempting install of certificate for domain %s!", domain) @@ -295,9 +306,12 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F logger.success( m18n.n("certmanager_cert_install_success", domain=domain)) - except Exception as e: - logger.error("Certificate installation for %s failed !\nException: %s", domain, e) + uo.success() + except Exception as e: + msg = "Certificate installation for %s failed !\nException: %s" % (domain, e) + logger.error(msg) + uo.error(msg) def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): """ @@ -373,6 +387,12 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Actual renew steps for domain in domain_list: + + uo = UnitOperation('letsencrypt_cert_renew', [{'domain', domain}], + args={'force': force, 'no_checks': no_checks, + 'staging': staging, 'email': email}) + uo.start() + logger.info( "Now attempting renewing of certificate for domain %s !", domain) @@ -385,12 +405,16 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal logger.success( m18n.n("certmanager_cert_renew_success", domain=domain)) + uo.success() + except Exception as e: import traceback from StringIO import StringIO stack = StringIO() traceback.print_exc(file=stack) - logger.error("Certificate renewing for %s failed !", domain) + msg = "Certificate renewing for %s failed !" % (domain) + logger.error(msg) + uo.error(msg) logger.error(stack.getvalue()) logger.error(str(e)) @@ -398,7 +422,6 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal logger.error("Sending email with details to root ...") _email_renewing_failed(domain, e, stack.getvalue()) - ############################################################################### # Back-end stuff # ############################################################################### From bb03e90d162e32ad0b1f3a6665f80385ff983ad0 Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 14 Apr 2018 04:29:41 +0200 Subject: [PATCH 041/103] [enh] Support other log path in metadata --- src/yunohost/log.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 2b9c17a37..736f9e795 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -115,6 +115,8 @@ def log_display(file_name): metadata = yaml.safe_load(md_file) infos['metadata_path'] = md_path infos['metadata'] = metadata + if 'log_path' in metadata: + log_path = metadata['log_path'] except yaml.YAMLError as exc: print(exc) From d17193b404fbb6bb4577a7d21e52358dd5eeae5d Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 14 Apr 2018 04:55:20 +0200 Subject: [PATCH 042/103] [enh] Limit number of line per log --- src/yunohost/log.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 736f9e795..bb0be6d0d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -82,12 +82,13 @@ def log_list(limit=None, full=False): return result -def log_display(file_name): +def log_display(file_name, number=50): """ Display full log or specific logs listed Argument: file_name + number """ if file_name.endswith(METADATA_FILE_EXT): @@ -96,6 +97,7 @@ def log_display(file_name): base_filename = file_name[:-len(LOG_FILE_EXT)] else: base_filename = file_name + md_filename = base_filename + METADATA_FILE_EXT md_path = os.path.join(OPERATIONS_PATH, md_filename) log_filename = base_filename + LOG_FILE_EXT @@ -121,11 +123,11 @@ def log_display(file_name): print(exc) if os.path.exists(log_path): - with open(log_path, "r") as content: - logs = content.read() - logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] - infos['log_path'] = log_path - infos['logs'] = logs + from yunohost.service import _tail + logs = _tail(log_path, int(number)) + logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs if x] + infos['log_path'] = log_path + infos['logs'] = logs return infos From 1b62e9425d276691605ab7ba620b3d2be923dded Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 14 Apr 2018 18:11:50 +0200 Subject: [PATCH 043/103] [enh] Allow to display part of log specified with path --- data/actionsmap/yunohost.yml | 9 +++++++-- src/yunohost/log.py | 17 ++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index e57d44716..2150aa7d3 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1625,7 +1625,12 @@ log: ### log_display() display: action_help: Display a log content - api: GET /logs/ + api: GET /logs/ arguments: file_name: - help: Log filenames for which to display the content + help: Log filename which to display the content + -n: + full: --number + help: Number of lines to display + default: 50 + type: int diff --git a/src/yunohost/log.py b/src/yunohost/log.py index bb0be6d0d..86e2f9267 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -98,18 +98,21 @@ def log_display(file_name, number=50): else: base_filename = file_name - md_filename = base_filename + METADATA_FILE_EXT - md_path = os.path.join(OPERATIONS_PATH, md_filename) - log_filename = base_filename + LOG_FILE_EXT - log_path = os.path.join(OPERATIONS_PATH, log_filename) + base_path = base_filename + if not base_filename.startswith('/'): + base_path = os.path.join(OPERATIONS_PATH, base_filename) + + md_path = base_path + METADATA_FILE_EXT + log_path = base_path + LOG_FILE_EXT operation = base_filename.split("-") if not os.path.exists(md_path) and not os.path.exists(log_path): raise MoulinetteError(errno.EINVAL, m18n.n('log_does_exists', log=file_name)) infos = {} - infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), - infos['name'] = base_filename + if not base_filename.startswith('/'): + infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), + infos['name'] = base_filename if os.path.exists(md_path): with open(md_path, "r") as md_file: @@ -125,7 +128,7 @@ def log_display(file_name, number=50): if os.path.exists(log_path): from yunohost.service import _tail logs = _tail(log_path, int(number)) - logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs if x] + #logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs if x] infos['log_path'] = log_path infos['logs'] = logs From b0a60b89d2dd8b0b03b274beb970bbe54e17118a Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 14 Apr 2018 18:16:56 +0200 Subject: [PATCH 044/103] [fix] Display operation name if exist --- src/yunohost/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 86e2f9267..c73269ba0 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -110,7 +110,7 @@ def log_display(file_name, number=50): raise MoulinetteError(errno.EINVAL, m18n.n('log_does_exists', log=file_name)) infos = {} - if not base_filename.startswith('/'): + if not base_path.startswith(OPERATIONS_PATH): infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), infos['name'] = base_filename From 844b35589e31deec76cd997567e43a4262d28c80 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 10 May 2018 01:14:53 +0200 Subject: [PATCH 045/103] Add ynh_render_template helper to render templates with jinja2 --- data/helpers.d/utils | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index cc057eb0b..05f9a4703 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -234,3 +234,20 @@ ynh_local_curl () { # Curl the URL curl --silent --show-error -kL -H "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url" } + +# Render templates with Jinja2 +# +# Attention : Variables should be exported before calling this helper to be +# accessible inside templates. +# +# usage: ynh_render_template some_template output_path +# | arg: some_template - Template file to be rendered +# | arg: output_path - The path where the output will be redirected to +ynh_render_template() { + local template_path=$1 + local output_path=$2 + # Taken from https://stackoverflow.com/a/35009576 + python2.7 -c 'import os, sys, jinja2; sys.stdout.write( + jinja2.Template(sys.stdin.read() + ).render(os.environ));' < $template_path > $output_path +} From 890eb7a885ef7b2df7611dee3fe1cdd1b75b2b9b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 10 May 2018 01:29:03 +0200 Subject: [PATCH 046/103] Add dependency to python-jinja2 --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index d1505994a..d069dc678 100644 --- a/debian/control +++ b/debian/control @@ -12,7 +12,7 @@ Architecture: all Depends: ${python:Depends}, ${misc:Depends} , moulinette (>= 2.7.1), ssowat (>= 2.7.1) , python-psutil, python-requests, python-dnspython, python-openssl - , python-apt, python-miniupnpc + , python-apt, python-miniupnpc, python-jinja2 , glances , dnsutils, bind9utils, unzip, git, curl, cron, wget , ca-certificates, netcat-openbsd, iproute From 17bbe6f4b25b33e760d7cfaba47360f1adcec163 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 22 May 2018 20:06:51 +0200 Subject: [PATCH 047/103] [fix] Index error on description --- src/yunohost/log.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index c73269ba0..91d53541a 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -111,7 +111,8 @@ def log_display(file_name, number=50): m18n.n('log_does_exists', log=file_name)) infos = {} if not base_path.startswith(OPERATIONS_PATH): - infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), + if len(operation) > 2: + infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), infos['name'] = base_filename if os.path.exists(md_path): From 440e96b4955047a58c3ebe9017b02f1046bd21d8 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 22 May 2018 21:07:18 +0200 Subject: [PATCH 048/103] [enh] Allow to ask fo .err or .warn log file --- src/yunohost/log.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 91d53541a..94671321b 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -91,28 +91,28 @@ def log_display(file_name, number=50): number """ - if file_name.endswith(METADATA_FILE_EXT): - base_filename = file_name[:-len(METADATA_FILE_EXT)] - elif file_name.endswith(LOG_FILE_EXT): - base_filename = file_name[:-len(LOG_FILE_EXT)] - else: - base_filename = file_name + abs_path = file_name + log_path = None + if not file_name.startswith('/'): + abs_path = os.path.join(OPERATIONS_PATH, file_name) - base_path = base_filename - if not base_filename.startswith('/'): - base_path = os.path.join(OPERATIONS_PATH, base_filename) + if os.path.exists(abs_path) and not file_name.endswith(METADATA_FILE_EXT) : + log_path = abs_path + base_path = os.path.splitext(abs_path)[0] + base_filename = os.path.basename(base_path) md_path = base_path + METADATA_FILE_EXT - log_path = base_path + LOG_FILE_EXT - operation = base_filename.split("-") + if log_path is None: + log_path = base_path + LOG_FILE_EXT if not os.path.exists(md_path) and not os.path.exists(log_path): raise MoulinetteError(errno.EINVAL, m18n.n('log_does_exists', log=file_name)) + infos = {} - if not base_path.startswith(OPERATIONS_PATH): - if len(operation) > 2: - infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), + if base_path.startswith(OPERATIONS_PATH): + operation = base_filename.split("-") + infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), infos['name'] = base_filename if os.path.exists(md_path): From 24bd5f9c835b3ba1f6ec41fd03b3f9037e1228de Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 22 May 2018 21:18:59 +0200 Subject: [PATCH 049/103] [fix] Manage yaml corrupted error --- locales/en.json | 1 + src/yunohost/log.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 930722a9e..1b97fd90f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -205,6 +205,7 @@ "invalid_url_format": "Invalid URL format", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", + "log_corrupted_md_file": "The yaml metadata file associated with logs is corrupted : '{md_file}'", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", "log_app_removelist": "Remove an application list", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 94671321b..7ad52720f 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -123,8 +123,12 @@ def log_display(file_name, number=50): infos['metadata'] = metadata if 'log_path' in metadata: log_path = metadata['log_path'] - except yaml.YAMLError as exc: - print(exc) + except yaml.YAMLError: + error = m18n.n('log_corrupted_md_file', file=md_path) + if os.path.exists(log_path): + logger.warning(error) + else: + raise MoulinetteError(errno.EINVAL, error) if os.path.exists(log_path): from yunohost.service import _tail From 2f5861a6a556e2760ed120135c94ff763bf6137b Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 22 May 2018 22:16:54 +0200 Subject: [PATCH 050/103] [enh] Add coments in log part --- src/yunohost/log.py | 87 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 7ad52720f..5d5d1d93d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -84,13 +84,17 @@ def log_list(limit=None, full=False): def log_display(file_name, number=50): """ - Display full log or specific logs listed + Display a log file enriched with metadata if any. + + If the file_name is not an absolute path, it will try to search the file in + the unit operations log path (see OPERATIONS_PATH). Argument: file_name number """ + # Normalize log/metadata paths and filenames abs_path = file_name log_path = None if not file_name.startswith('/'): @@ -110,11 +114,14 @@ def log_display(file_name, number=50): m18n.n('log_does_exists', log=file_name)) infos = {} + + # If it's a unit operation, display the name and the description if base_path.startswith(OPERATIONS_PATH): operation = base_filename.split("-") infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), infos['name'] = base_filename + # Display metadata if exist if os.path.exists(md_path): with open(md_path, "r") as md_file: try: @@ -130,6 +137,7 @@ def log_display(file_name, number=50): else: raise MoulinetteError(errno.EINVAL, error) + # Display logs if exist if os.path.exists(log_path): from yunohost.service import _tail logs = _tail(log_path, int(number)) @@ -140,8 +148,34 @@ def log_display(file_name, number=50): return infos def is_unit_operation(entities='app,domain,service,user', exclude='auth,password', operation_key=None, auto=True): + """ + Configure quickly a unit operation + + This decorator help you to configure quickly the record of a unit operations. + + Argument: + entities A list seperated by coma of entity types related to the unit + operation. The entity type is searched inside argument's names of the + decorated function. If something match, the argument value is added as + related entity. + + exclude Remove some arguments from the context. By default, arguments + called 'password' and 'auth' are removed. If an argument is an object, you + need to exclude it or create manually the unit operation without this + decorator. + + operation_key Key describing the unit operation. If you want to display a + well formed description you should add a translation key like this + "log_" + operation_key in locales files. + + auto If true, start the recording. If False, the unit operation object + created is given to the decorated function as the first argument and you can + start recording at the good time. + """ def decorate(func): def func_wrapper(*args, **kwargs): + # For a strange reason we can't use directly the arguments from + # is_unit_operation function. We need to store them in a var before. entities_list = entities.split(',') exclude_list = exclude.split(',') op_key = operation_key @@ -150,6 +184,7 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password if op_key is None: op_key = func.__name__ + # Search related entity in arguments of the decorated function for entity in entities_list: entity = entity.split(':') entity_type = entity[-1] @@ -162,10 +197,15 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password related_to.append({entity_type: kwargs[x]}) context = kwargs.copy() + + # Exclude unappropriate data from the context for field in exclude_list: if field in context: context.pop(field, None) uo = UnitOperation(op_key, related_to, args=context) + + # Start to record or give the unit operation in argument to let the + # developper start the record itself if auto: uo.start() try: @@ -173,12 +213,22 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password args = (uo,) + args result = func(*args, **kwargs) finally: + # Close the unit operation if it hasn't been closed before uo.close(exc_info()[0]) return result return func_wrapper return decorate class UnitOperation(object): + """ + Instances of this class represents unit operation the yunohost admin as done. + + Each time an action of the yunohost cli/api change the system, one or several + unit operations should be registered. + + This class record logs and some metadata like context or start time/end time. + """ + def __init__(self, operation, related_to=None, **kwargs): # TODO add a way to not save password on app installation self.operation = operation @@ -194,12 +244,21 @@ class UnitOperation(object): os.makedirs(self.path) def start(self): + """ + Start to record logs that change the system + Until this start method is run, no unit operation will be registered. + """ + if self.started_at is None: self.started_at = datetime.now() self.flush() self._register_log() def _register_log(self): + """ + Register log with a handler connected on log system + """ + # TODO add a way to not save password on app installation filename = os.path.join(self.path, self.name + LOG_FILE_EXT) self.file_handler = FileHandler(filename) @@ -210,12 +269,20 @@ class UnitOperation(object): self.logger.addHandler(self.file_handler) def flush(self): + """ + Write or rewrite the metadata file with all metadata known + """ + filename = os.path.join(self.path, self.name + METADATA_FILE_EXT) with open(filename, 'w') as outfile: yaml.safe_dump(self.metadata, outfile, default_flow_style=False) @property def name(self): + """ + Name of the operation + This name is used as filename, so don't use space + """ name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] if self.related_to: @@ -224,6 +291,10 @@ class UnitOperation(object): @property def metadata(self): + """ + Dictionnary of all metadata collected + """ + data = { 'started_at': self.started_at, 'operation': self.operation, @@ -240,12 +311,21 @@ class UnitOperation(object): return data def success(self): + """ + Declare the success end of the unit operation + """ self.close() def error(self, error): + """ + Declare the failure of the unit operation + """ self.close(error) def close(self, error=None): + """ + Close properly the unit operation + """ if self.ended_at is not None or self.started_at is None: return self.ended_at = datetime.now() @@ -256,5 +336,10 @@ class UnitOperation(object): self.flush() def __del__(self): + """ + Try to close the unit operation, if it's missing. + The missing of the message below could help to see an electrical + shortage. + """ self.error(m18n.n('log_operation_unit_unclosed_properly')) From 2cb0e7916ed33c4a1a1cbbbc8dbe83e7e07171a2 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 23 May 2018 01:17:32 +0200 Subject: [PATCH 051/103] [enh] Watch in compress log file too --- src/yunohost/service.py | 46 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 4e264e310..cc65fe237 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -596,13 +596,21 @@ def _tail(file, n, offset=None): value is a tuple in the form ``(lines, has_more)`` where `has_more` is an indicator that is `True` if there are more lines in the file. + This function works even with splitted logs (gz compression, log rotate...) """ avg_line_length = 74 to_read = n + (offset or 0) try: - with open(file, 'r') as f: - while 1: + if file.endswith(".gz"): + import gzip + f = gzip.open(file) + lines = f.read().splitlines() + else: + f = open(file) + pos = 1 + lines = [] + while len(lines) < to_read and pos > 0: try: f.seek(-(avg_line_length * to_read), 2) except IOError: @@ -611,13 +619,43 @@ def _tail(file, n, offset=None): f.seek(0) pos = f.tell() lines = f.read().splitlines() - if len(lines) >= to_read or pos == 0: - return lines[-to_read:offset and -offset or None] avg_line_length *= 1.3 + f.close() except IOError: return [] + if len(lines) < to_read: + previous_log_file = _find_previous_log_file(file) + if previous_log_file is not None: + lines = _tail(previous_log_file, to_read - len(lines)) + lines + return lines[-to_read:offset and -offset or None] + + +def _find_previous_log_file(file): + """ + Find the previous log file + """ + import re + + splitext = os.path.splitext(file) + if splitext[1] == '.gz': + file = splitext[0] + splitext = os.path.splitext(file) + ext = splitext[1] + i = re.findall(r'\.(\d+)', ext) + i = int(i[0]) + 1 if len(i) > 0 else 1 + + previous_file = file if i == 1 else splitext[0] + previous_file = previous_file + '.%d' % (i) + if os.path.exists(previous_file): + return previous_file + + previous_file = previous_file + ".gz" + if os.path.exists(previous_file): + return previous_file + + return None def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): """Compare two files and return the differences From 930567f54f77f2755b5677aab1fd232a186eec8e Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 25 May 2018 11:58:46 +0200 Subject: [PATCH 052/103] [enh] Replace file_name by path in log display cmd --- data/actionsmap/yunohost.yml | 6 +++--- src/yunohost/log.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 2150aa7d3..90067d627 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1625,10 +1625,10 @@ log: ### log_display() display: action_help: Display a log content - api: GET /logs/ + api: GET /logs/display arguments: - file_name: - help: Log filename which to display the content + path: + help: Log file which to display the content -n: full: --number help: Number of lines to display diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 5d5d1d93d..44a979075 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -82,7 +82,7 @@ def log_list(limit=None, full=False): return result -def log_display(file_name, number=50): +def log_display(path, number=50): """ Display a log file enriched with metadata if any. @@ -95,12 +95,12 @@ def log_display(file_name, number=50): """ # Normalize log/metadata paths and filenames - abs_path = file_name + abs_path = path log_path = None - if not file_name.startswith('/'): - abs_path = os.path.join(OPERATIONS_PATH, file_name) + if not path.startswith('/'): + abs_path = os.path.join(OPERATIONS_PATH, path) - if os.path.exists(abs_path) and not file_name.endswith(METADATA_FILE_EXT) : + if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT) : log_path = abs_path base_path = os.path.splitext(abs_path)[0] @@ -111,7 +111,7 @@ def log_display(file_name, number=50): if not os.path.exists(md_path) and not os.path.exists(log_path): raise MoulinetteError(errno.EINVAL, - m18n.n('log_does_exists', log=file_name)) + m18n.n('log_does_exists', log=path)) infos = {} From 89124d4a9fad60c5e85cc255f067865fca663c67 Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 2 Jun 2018 13:42:43 +0200 Subject: [PATCH 053/103] [enh] Support others log categories --- data/actionsmap/yunohost.yml | 6 +-- src/yunohost/log.py | 75 +++++++++++++++++++++++------------- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 90067d627..78b6433f8 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1614,13 +1614,13 @@ log: action_help: List logs api: GET /logs arguments: + categories: + help: Log categories to display (default operations) + nargs: "*" -l: full: --limit help: Maximum number of logs type: int - --full: - help: Show more details - action: store_true ### log_display() display: diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 44a979075..156e8aa0f 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -27,23 +27,26 @@ import os import yaml import errno +import collections from datetime import datetime from logging import FileHandler, getLogger, Formatter from sys import exc_info -from moulinette import m18n +from moulinette import m18n, msettings from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -OPERATIONS_PATH = '/var/log/yunohost/operation/' +CATEGORIES_PATH = '/var/log/yunohost/categories/' +CATEGORIES = ['operation', 'history', 'package', 'system', 'access', 'service', \ + 'app'] METADATA_FILE_EXT = '.yml' LOG_FILE_EXT = '.log' RELATED_CATEGORIES = ['app', 'domain', 'service', 'user'] logger = getActionLogger('yunohost.log') -def log_list(limit=None, full=False): +def log_list(categories=[], limit=None): """ List available logs @@ -51,33 +54,45 @@ def log_list(limit=None, full=False): limit -- Maximum number of logs """ - result = {"operations": []} + if not categories: + is_api = msettings.get('interface') == 'api' + categories = ["operation"] if not is_api else CATEGORIES - if not os.path.exists(OPERATIONS_PATH): - return result + result = collections.OrderedDict() + for category in categories: + result[category] = [] - operations = filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(OPERATIONS_PATH)) - operations = reversed(sorted(operations)) + category_path = os.path.join(CATEGORIES_PATH, category) + if not os.path.exists(category_path): + continue - if limit is not None: - operations = operations[:limit] + logs = filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(category_path)) + logs = reversed(sorted(logs)) - for operation in operations: + if limit is not None: + logs = logs[:limit] - base_filename = operation[:-len(METADATA_FILE_EXT)] - md_filename = operation - md_path = os.path.join(OPERATIONS_PATH, md_filename) + for log in logs: - operation = base_filename.split("-") + base_filename = log[:-len(METADATA_FILE_EXT)] + md_filename = log + md_path = os.path.join(category_path, md_filename) - operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y%m%d %H%M%S") + log = base_filename.split("-") - result["operations"].append({ - "started_at": operation_datetime, - "description": m18n.n("log_" + operation[2], *operation[3:]), - "name": base_filename, - "path": md_path, - }) + entry = { + "name": base_filename, + "path": md_path, + } + try: + log_datetime = datetime.strptime(" ".join(log[:2]), "%Y%m%d %H%M%S") + except ValueError: + entry["description"] = m18n.n("log_" + log[0], *log[1:]), + else: + entry["description"] = m18n.n("log_" + log[2], *log[3:]), + entry["started_at"] = log_datetime + + result[category].append(entry) return result @@ -98,7 +113,10 @@ def log_display(path, number=50): abs_path = path log_path = None if not path.startswith('/'): - abs_path = os.path.join(OPERATIONS_PATH, path) + for category in CATEGORIES: + abs_path = os.path.join(CATEGORIES_PATH, category, path) + if os.path.exists(abs_path) or os.path.exists(abs_path + METADATA_FILE_EXT): + break if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT) : log_path = abs_path @@ -116,9 +134,14 @@ def log_display(path, number=50): infos = {} # If it's a unit operation, display the name and the description - if base_path.startswith(OPERATIONS_PATH): - operation = base_filename.split("-") - infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), + if base_path.startswith(CATEGORIES_PATH): + log = base_filename.split("-") + try: + datetime.strptime(" ".join(log[:2]), "%Y%m%d %H%M%S") + except ValueError: + infos["description"] = m18n.n("log_" + log[0], *log[1:]), + else: + infos["description"] = m18n.n("log_" + log[2], *log[3:]), infos['name'] = base_filename # Display metadata if exist From 5676f05dfbfd3e702ea715df0a8dae31183c792b Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 04:04:10 +0200 Subject: [PATCH 054/103] [enh] Better help for log list action --- data/actionsmap/yunohost.yml | 4 ++-- locales/en.json | 1 + src/yunohost/log.py | 7 ++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 78b6433f8..ec150c1ec 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1614,8 +1614,8 @@ log: action_help: List logs api: GET /logs arguments: - categories: - help: Log categories to display (default operations) + category: + help: Log category to display (default operations), could be operation, history, package, system, access, service or app nargs: "*" -l: full: --limit diff --git a/locales/en.json b/locales/en.json index 1b97fd90f..9d4267de6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -206,6 +206,7 @@ "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "log_corrupted_md_file": "The yaml metadata file associated with logs is corrupted : '{md_file}'", + "log_category_404": "The log category '{category}' does not exist", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", "log_app_removelist": "Remove an application list", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 156e8aa0f..7adee7e73 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -46,7 +46,7 @@ RELATED_CATEGORIES = ['app', 'domain', 'service', 'user'] logger = getActionLogger('yunohost.log') -def log_list(categories=[], limit=None): +def log_list(category=[], limit=None): """ List available logs @@ -54,6 +54,9 @@ def log_list(categories=[], limit=None): limit -- Maximum number of logs """ + categories = category + + # In cli we just display `operation` logs by default if not categories: is_api = msettings.get('interface') == 'api' categories = ["operation"] if not is_api else CATEGORIES @@ -64,6 +67,8 @@ def log_list(categories=[], limit=None): category_path = os.path.join(CATEGORIES_PATH, category) if not os.path.exists(category_path): + logger.warning(m18n.n('log_category_404', category=category)) + continue logs = filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(category_path)) From a20281ebfcdc118660a7602624e4ef15bb3e54b1 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 18:57:00 +0200 Subject: [PATCH 055/103] [enh] Operational unit for backup regenconf, migrations and others --- src/yunohost/app.py | 28 ++++++++++++++++++++++++++++ src/yunohost/backup.py | 38 ++++++++++++++++++++++++++++++++++---- src/yunohost/service.py | 7 ++++++- src/yunohost/tools.py | 19 ++++++++++++++++--- 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index df36ccdf7..4273471a0 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -623,6 +623,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Start register change on system related_to = [('app', app_instance_name)] uo = UnitOperation('app_upgrade', related_to, env=env_dict) + uo.start() # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) @@ -918,6 +919,8 @@ def app_addaccess(auth, apps, users=[]): apps = [apps, ] for app in apps: + + app_settings = _get_app_settings(app) if not app_settings: continue @@ -927,6 +930,12 @@ def app_addaccess(auth, apps, users=[]): app_settings['mode'] = 'private' if app_settings['mode'] == 'private': + + # Start register change on system + related_to = [('app', app)] + uo= UnitOperation('app_addaccess', related_to) + uo.start() + allowed_users = set() if 'allowed_users' in app_settings: allowed_users = set(app_settings['allowed_users'].split(',')) @@ -939,11 +948,14 @@ def app_addaccess(auth, apps, users=[]): logger.warning(m18n.n('user_unknown', user=allowed_user)) continue allowed_users.add(allowed_user) + uo.related_to.add(('user', allowed_user)) new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) hook_callback('post_app_addaccess', args=[app, new_users]) + uo.success() + result[app] = allowed_users app_ssowatconf(auth) @@ -980,6 +992,12 @@ def app_removeaccess(auth, apps, users=[]): allowed_users = set() if app_settings.get('skipped_uris', '') != '/': + + # Start register change on system + related_to = [('app', app)] + uo= UnitOperation('app_removeaccess', related_to) + uo.start() + if remove_all: pass elif 'allowed_users' in app_settings: @@ -991,12 +1009,15 @@ def app_removeaccess(auth, apps, users=[]): if allowed_user not in users: allowed_users.add(allowed_user) + uo.related_to += [ ('user', x) for x in allowed_users ] new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) hook_callback('post_app_removeaccess', args=[app, new_users]) result[app] = allowed_users + uo.success() + app_ssowatconf(auth) return {'allowed_users': result} @@ -1020,6 +1041,11 @@ def app_clearaccess(auth, apps): if not app_settings: continue + # Start register change on system + related_to = [('app', app)] + uo= UnitOperation('app_clearaccess', related_to) + uo.start() + if 'mode' in app_settings: app_setting(app, 'mode', delete=True) @@ -1028,6 +1054,8 @@ def app_clearaccess(auth, apps): hook_callback('post_app_clearaccess', args=[app]) + uo.success() + app_ssowatconf(auth) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 15c793802..c4b61f1f2 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1125,9 +1125,14 @@ class RestoreManager(): if system_targets == []: return + # Start register change on system + uo = UnitOperation('backup_restore_system') + uo.start() + logger.info(m18n.n('restore_running_hooks')) env_dict = self._get_env_var() + uo.extra.env = env_dict ret = hook_callback('restore', system_targets, args=[self.work_dir], @@ -1137,9 +1142,16 @@ class RestoreManager(): for part in ret['succeed'].keys(): self.targets.set_result("system", part, "Success") + error_part = [] for part in ret['failed'].keys(): logger.error(m18n.n('restore_system_part_failed', part=part)) self.targets.set_result("system", part, "Error") + error_part.append(part) + + if ret['failed']: + uo.error(m18n.n('restore_system_part_failed', part=', '.join(error_part))) + else: + uo.success() service_regen_conf() @@ -1187,6 +1199,11 @@ class RestoreManager(): else: shutil.copy2(s, d) + # Start register change on system + related_to = [('app', app_instance_name)] + uo = UnitOperation('backup_restore_app', related_to) + uo.start() + # Check if the app is not already installed if _is_installed(app_instance_name): logger.error(m18n.n('restore_already_installed_app', @@ -1229,6 +1246,8 @@ class RestoreManager(): # Prepare env. var. to pass to script env_dict = self._get_env_var(app_instance_name) + uo.extra.env = env_dict + # Execute app restore script hook_exec(restore_script, args=[app_backup_in_archive, app_instance_name], @@ -1237,8 +1256,10 @@ class RestoreManager(): env=env_dict, user="root") except: - logger.exception(m18n.n('restore_app_failed', - app=app_instance_name)) + msg = m18n.n('restore_app_failed',app=app_instance_name) + logger.exception(msg) + uo.error(msg) + self.targets.set_result("apps", app_instance_name, "Error") remove_script = os.path.join(app_scripts_in_archive, 'remove') @@ -1250,12 +1271,20 @@ class RestoreManager(): env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + uo = UnitOperation('remove_on_failed_restore', + [('app', app_instance_name)], + env=env_dict_remove) + uo.start() + # Execute remove script # TODO: call app_remove instead if hook_exec(remove_script, args=[app_instance_name], env=env_dict_remove, user="root") != 0: - logger.warning(m18n.n('app_not_properly_removed', - app=app_instance_name)) + msg = m18n.n('app_not_properly_removed', app=app_instance_name) + logger.warning(msg) + uo.error(msg) + else: + uo.success() # Cleaning app directory shutil.rmtree(app_settings_new_path, ignore_errors=True) @@ -1263,6 +1292,7 @@ class RestoreManager(): # TODO Cleaning app hooks else: self.targets.set_result("apps", app_instance_name, "Success") + uo.success() finally: # Cleaning temporary scripts directory shutil.rmtree(tmp_folder_for_app_restore, ignore_errors=True) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index cc65fe237..d90965d3e 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -274,7 +274,8 @@ def service_log(name, number=50): return result -def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, +@is_unit_operation('names:service', auto=False) +def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False, list_pending=False): """ Regenerate the configuration file(s) for a service @@ -289,6 +290,8 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, """ result = {} + if not dry_run: + uo.start() # Return the list of pending conf if list_pending: pending_conf = _get_pending_conf(names) @@ -482,6 +485,8 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, return post_args + [regen_conf_files, ] hook_callback('conf_regen', names, pre_callback=_pre_call) + uo.success() + return result diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index eefe2d28d..8ffeaecf4 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -834,20 +834,31 @@ def tools_migrations_migrate(target=None, skip=False): # effectively run selected migrations for migration in migrations: + + # Start register change on system + uo= UnitOperation('tools_migrations_migrate_' + mode) + uo.start() + if not skip: logger.warn(m18n.n('migrations_show_currently_running_migration', **migration)) try: if mode == "forward": - migration["module"].MyMigration().migrate() + m = migration["module"].MyMigration() + m.uo = uo + m.migrate() elif mode == "backward": - migration["module"].MyMigration().backward() + m = migration["module"].MyMigration() + m.uo = uo + m.backward() else: # can't happen raise Exception("Illegal state for migration: '%s', should be either 'forward' or 'backward'" % mode) except Exception as e: # migration failed, let's stop here but still update state because # we managed to run the previous ones - logger.error(m18n.n('migrations_migration_has_failed', exception=e, **migration), exc_info=1) + msg = m18n.n('migrations_migration_has_failed', exception=e, **migration) + logger.error(msg, exc_info=1) + uo.error(msg) break else: # if skip @@ -859,6 +870,8 @@ def tools_migrations_migrate(target=None, skip=False): "name": migration["name"], } + uo.success() + # special case where we want to go back from the start if target == 0: state["last_run_migration"] = None From 793b4d2f88635e53a97b55a500700abb1f858da8 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 19:18:11 +0200 Subject: [PATCH 056/103] [enh] Add some translations and start uo --- locales/en.json | 9 +++++++++ src/yunohost/app.py | 3 +++ src/yunohost/backup.py | 2 ++ 3 files changed, 14 insertions(+) diff --git a/locales/en.json b/locales/en.json index 9d4267de6..50992e7f9 100644 --- a/locales/en.json +++ b/locales/en.json @@ -209,12 +209,18 @@ "log_category_404": "The log category '{category}' does not exist", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", + "log_app_addaccess": "Add access to '{}'", + "log_app_removeaccess": "Remove access to '{}'", + "log_app_clearaccess": "Remove all access to '{}'", "log_app_removelist": "Remove an application list", "log_app_change_url": "Change the url of '{}' application", "log_app_install": "Install '{}' application", "log_app_remove": "Remove '{}' application", "log_app_upgrade": "Upgrade '{}' application", "log_app_makedefault": "Make '{}' as default application", + "log_backup_restore_system": "Restore system from a backup archive", + "log_backup_restore_app": "Restore '{}' from a backup archive", + "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive", "log_domain_add": "Add '{}' domain into system configuration", "log_domain_remove": "Remove '{}' domain from system configuration", "log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'", @@ -222,10 +228,13 @@ "log_letsencrypt_cert_install": "Install Let's encrypt certificate on '{}' domain", "log_selfsigned_cert_install": "Install self signed certificate on '{}' domain", "log_letsencrypt_cert_renew": "Renew '{}' Let's encrypt certificate", + "log_service_regen_conf": "Regenerate system configurations '{}'", "log_user_create": "Add '{}' user", "log_user_delete": "Delete '{}' user", "log_user_update": "Update information of '{}' user", "log_tools_maindomain": "Make '{}' as main domain", + "log_tools_migrations_migrate_forward": "Migrate forward", + "log_tools_migrations_migrate_backward": "Migrate backward", "log_tools_postinstall": "Postinstall your YunoHost server", "log_tools_upgrade": "Upgrade debian packages", "log_tools_shutdown": "Shutdown your server", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 4273471a0..86fe1657a 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -800,6 +800,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False uo_remove = UnitOperation('remove_on_failed_install', [('app', app_instance_name)], env=env_dict_remove) + uo_remove.start() remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), @@ -950,6 +951,7 @@ def app_addaccess(auth, apps, users=[]): allowed_users.add(allowed_user) uo.related_to.add(('user', allowed_user)) + uo.flush() new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) hook_callback('post_app_addaccess', args=[app, new_users]) @@ -1010,6 +1012,7 @@ def app_removeaccess(auth, apps, users=[]): allowed_users.add(allowed_user) uo.related_to += [ ('user', x) for x in allowed_users ] + uo.flush() new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) hook_callback('post_app_removeaccess', args=[app, new_users]) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index c4b61f1f2..a95d75a45 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1133,6 +1133,7 @@ class RestoreManager(): env_dict = self._get_env_var() uo.extra.env = env_dict + uo.flush() ret = hook_callback('restore', system_targets, args=[self.work_dir], @@ -1247,6 +1248,7 @@ class RestoreManager(): env_dict = self._get_env_var(app_instance_name) uo.extra.env = env_dict + uo.flush() # Execute app restore script hook_exec(restore_script, From ab816f292e4067c0188d3f93a2bc1f6dc69240df Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 20:35:45 +0200 Subject: [PATCH 057/103] [enh] Display log message to help to get log --- locales/en.json | 5 +++++ src/yunohost/log.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 50992e7f9..606e224d2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -207,6 +207,11 @@ "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "log_corrupted_md_file": "The yaml metadata file associated with logs is corrupted : '{md_file}'", "log_category_404": "The log category '{category}' does not exist", + "log_link_to_log": "Complete log of this operation: '{desc}'", + "log_help_to_get_log": "To view complete log of the operation '{desc}', run: 'yunohost log display {name}'", + "log_link_to_failed_log": "The operation '{desc}' has failed ! To get help please give the complete log", + "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help please give the complete log of the operation, you can get it by running: 'yunohost log display {name}'", + "log_category_404": "The log category '{category}' does not exist", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", "log_app_addaccess": "Add access to '{}'", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 7adee7e73..9cfa0a7e6 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -38,6 +38,7 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger CATEGORIES_PATH = '/var/log/yunohost/categories/' +OPERATIONS_PATH = '/var/log/yunohost/categories/operation/' CATEGORIES = ['operation', 'history', 'package', 'system', 'access', 'service', \ 'app'] METADATA_FILE_EXT = '.yml' @@ -219,10 +220,10 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password entity = entity[0] if entity in kwargs and kwargs[entity] is not None: if isinstance(kwargs[entity], basestring): - related_to.append({entity_type: kwargs[entity]}) + related_to.append((entity_type, kwargs[entity])) else: for x in kwargs[entity]: - related_to.append({entity_type: kwargs[x]}) + related_to.append((entity_type, kwargs[x])) context = kwargs.copy() @@ -314,7 +315,7 @@ class UnitOperation(object): name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] if self.related_to: - name += self.related_to[0].values() + name += [self.related_to[0][1]] return '-'.join(name) @property @@ -361,6 +362,20 @@ class UnitOperation(object): self._success = error is None if self.logger is not None: self.logger.removeHandler(self.file_handler) + + is_api = msettings.get('interface') == 'api' + desc = _get_description_from_name(self.name) + if error is None: + if is_api: + logger.info(m18n.n('log_link_to_log', name=self.name, desc=desc)) + else: + logger.info(m18n.n('log_help_to_get_log', name=self.name, desc=desc)) + else: + if is_api: + logger.warning(m18n.n('log_link_to_failed_log', name=self.name, desc=desc)) + else: + logger.warning(m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc)) + self.flush() def __del__(self): @@ -371,3 +386,12 @@ class UnitOperation(object): """ self.error(m18n.n('log_operation_unit_unclosed_properly')) +def _get_description_from_name(name): + parts = name.split("-") + try: + datetime.strptime(" ".join(parts[:2]), "%Y%m%d %H%M%S") + except ValueError: + return m18n.n("log_" + parts[0], *parts[1:]) + else: + return m18n.n("log_" + parts[2], *parts[3:]) + From cad0b9ba1fa55ec323b02a8be627bc202d76b8c1 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 20:58:24 +0200 Subject: [PATCH 058/103] [enh] Improve help to get log message --- locales/en.json | 2 +- src/yunohost/log.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 606e224d2..495c95f82 100644 --- a/locales/en.json +++ b/locales/en.json @@ -207,7 +207,7 @@ "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "log_corrupted_md_file": "The yaml metadata file associated with logs is corrupted : '{md_file}'", "log_category_404": "The log category '{category}' does not exist", - "log_link_to_log": "Complete log of this operation: '{desc}'", + "log_link_to_log": "Complete log of this operation: '{desc}'", "log_help_to_get_log": "To view complete log of the operation '{desc}', run: 'yunohost log display {name}'", "log_link_to_failed_log": "The operation '{desc}' has failed ! To get help please give the complete log", "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help please give the complete log of the operation, you can get it by running: 'yunohost log display {name}'", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 9cfa0a7e6..9ced6cc22 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -372,7 +372,9 @@ class UnitOperation(object): logger.info(m18n.n('log_help_to_get_log', name=self.name, desc=desc)) else: if is_api: - logger.warning(m18n.n('log_link_to_failed_log', name=self.name, desc=desc)) + logger.warning("" + m18n.n('log_link_to_failed_log', + name=self.name, desc=desc) + + "") else: logger.warning(m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc)) From 9d59c8555a0afcbceb42120354a1ebaf22946208 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 21:36:28 +0200 Subject: [PATCH 059/103] [enh] Display log help at the end of logs --- locales/en.json | 1 + src/yunohost/app.py | 9 +++++---- src/yunohost/log.py | 17 +++++++++-------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/locales/en.json b/locales/en.json index 495c95f82..0ba225e79 100644 --- a/locales/en.json +++ b/locales/en.json @@ -226,6 +226,7 @@ "log_backup_restore_system": "Restore system from a backup archive", "log_backup_restore_app": "Restore '{}' from a backup archive", "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive", + "log_remove_on_failed_install": "Remove '{}' after a failed installation", "log_domain_add": "Add '{}' domain into system configuration", "log_domain_remove": "Remove '{}' domain from system configuration", "log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 86fe1657a..570c2173d 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -788,7 +788,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False logger.exception(m18n.n('unexpected_error')) finally: if install_retcode != 0: - uo.error(m18n.n('unexpected_error')) + error_msg = uo.error(m18n.n('unexpected_error')) if not no_remove_on_failure: # Setup environment for remove script env_dict_remove = {} @@ -821,9 +821,10 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False app_ssowatconf(auth) if install_retcode == -1: - raise MoulinetteError(errno.EINTR, - m18n.g('operation_interrupted')) - raise MoulinetteError(errno.EIO, m18n.n('installation_failed')) + msg = m18n.n('operation_interrupted') + " " + error_msg + raise MoulinetteError(errno.EINTR, msg) + msg = m18n.n('installation_failed') + " " + error_msg + raise MoulinetteError(errno.EIO, msg) # Clean hooks and add new ones hook_remove(app_instance_name) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 9ced6cc22..588052058 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -349,7 +349,7 @@ class UnitOperation(object): """ Declare the failure of the unit operation """ - self.close(error) + return self.close(error) def close(self, error=None): """ @@ -367,18 +367,19 @@ class UnitOperation(object): desc = _get_description_from_name(self.name) if error is None: if is_api: - logger.info(m18n.n('log_link_to_log', name=self.name, desc=desc)) + msg = m18n.n('log_link_to_log', name=self.name, desc=desc) else: - logger.info(m18n.n('log_help_to_get_log', name=self.name, desc=desc)) + msg = m18n.n('log_help_to_get_log', name=self.name, desc=desc) + logger.info(msg) else: if is_api: - logger.warning("" + m18n.n('log_link_to_failed_log', - name=self.name, desc=desc) + - "") + msg = "" + m18n.n('log_link_to_failed_log', + name=self.name, desc=desc) + "" else: - logger.warning(m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc)) - + msg = m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc) + logger.warning(msg) self.flush() + return msg def __del__(self): """ From 91caae665e5c678ef4c81d8b570e40be2a96417c Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 21:39:28 +0200 Subject: [PATCH 060/103] [enh] Improve end failed message on app_install --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 570c2173d..7ec1ddc80 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -823,7 +823,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False if install_retcode == -1: msg = m18n.n('operation_interrupted') + " " + error_msg raise MoulinetteError(errno.EINTR, msg) - msg = m18n.n('installation_failed') + " " + error_msg + msg = error_msg raise MoulinetteError(errno.EIO, msg) # Clean hooks and add new ones From 97010941eddc1ea11c0590185d61f39b773b7b64 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 22:54:51 +0200 Subject: [PATCH 061/103] [fix] Unit operation log decorator fail on internal call --- src/yunohost/log.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 588052058..bbe2346e2 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -213,11 +213,21 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password if op_key is None: op_key = func.__name__ + # In case the function is called directly from an other part of the + # code + if len(args) > 0: + from inspect import getargspec + keys = getargspec(func).args + for k, arg in enumerate(args): + kwargs[keys[k]] = arg + args = () + # Search related entity in arguments of the decorated function for entity in entities_list: entity = entity.split(':') entity_type = entity[-1] entity = entity[0] + if entity in kwargs and kwargs[entity] is not None: if isinstance(kwargs[entity], basestring): related_to.append((entity_type, kwargs[entity])) @@ -357,6 +367,8 @@ class UnitOperation(object): """ if self.ended_at is not None or self.started_at is None: return + if error is not None and not isinstance(error, basestring): + error = str(error) self.ended_at = datetime.now() self._error = error self._success = error is None From 63f8dfd87e73506988c27c1d9616cee85640b144 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 23:04:41 +0200 Subject: [PATCH 062/103] [fix] Be able to create uo name with dot like domain uo --- src/yunohost/log.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index bbe2346e2..d95e948a1 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -127,7 +127,10 @@ def log_display(path, number=50): if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT) : log_path = abs_path - base_path = os.path.splitext(abs_path)[0] + if abs_path.endswith(METADATA_FILE_EXT) or abs_path.endswith(LOG_FILE_EXT): + base_path = ''.join(os.path.splitext(abs_path)[:-1]) + else: + base_path = abs_path base_filename = os.path.basename(base_path) md_path = base_path + METADATA_FILE_EXT if log_path is None: From f64f0acb5b409688dc27f1e2ae41b3c743d9a5b7 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 00:16:37 +0200 Subject: [PATCH 063/103] [fix] Indices error due to bad yml files --- src/yunohost/log.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index d95e948a1..77f20795a 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -90,12 +90,12 @@ def log_list(category=[], limit=None): "name": base_filename, "path": md_path, } + entry["description"] = _get_description_from_name(base_filename) try: log_datetime = datetime.strptime(" ".join(log[:2]), "%Y%m%d %H%M%S") except ValueError: - entry["description"] = m18n.n("log_" + log[0], *log[1:]), + pass else: - entry["description"] = m18n.n("log_" + log[2], *log[3:]), entry["started_at"] = log_datetime result[category].append(entry) @@ -144,13 +144,7 @@ def log_display(path, number=50): # If it's a unit operation, display the name and the description if base_path.startswith(CATEGORIES_PATH): - log = base_filename.split("-") - try: - datetime.strptime(" ".join(log[:2]), "%Y%m%d %H%M%S") - except ValueError: - infos["description"] = m18n.n("log_" + log[0], *log[1:]), - else: - infos["description"] = m18n.n("log_" + log[2], *log[3:]), + infos["description"] = _get_description_from_name(base_filename) infos['name'] = base_filename # Display metadata if exist @@ -407,9 +401,11 @@ class UnitOperation(object): def _get_description_from_name(name): parts = name.split("-") try: - datetime.strptime(" ".join(parts[:2]), "%Y%m%d %H%M%S") - except ValueError: - return m18n.n("log_" + parts[0], *parts[1:]) - else: - return m18n.n("log_" + parts[2], *parts[3:]) - + try: + datetime.strptime(" ".join(parts[:2]), "%Y%m%d %H%M%S") + except ValueError: + return m18n.n("log_" + parts[0], *parts[1:]) + else: + return m18n.n("log_" + parts[2], *parts[3:]) + except IndexError: + return name From 9eee035a8d7d96c1454ccd5c4d7a89903d0780a3 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 00:32:28 +0200 Subject: [PATCH 064/103] [fix] Collect the last error for uo log --- src/yunohost/log.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 77f20795a..440a454fb 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -248,6 +248,9 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password if not auto: args = (uo,) + args result = func(*args, **kwargs) + except Exception as e: + uo.error(e) + raise e finally: # Close the unit operation if it hasn't been closed before uo.close(exc_info()[0]) From 5dbf7b567e5328a9cd2099fadb4a7439fa7f8653 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 00:58:41 +0200 Subject: [PATCH 065/103] [fix] Detect un declared success uo --- src/yunohost/log.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 440a454fb..6bf24eb9d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -251,9 +251,8 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password except Exception as e: uo.error(e) raise e - finally: - # Close the unit operation if it hasn't been closed before - uo.close(exc_info()[0]) + else: + uo.success() return result return func_wrapper return decorate From 65c18dcaa1ed3bdab154184d272ab8e3024c04e1 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 11:21:21 +0200 Subject: [PATCH 066/103] [fix] Internal call with uo args --- src/yunohost/log.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 6bf24eb9d..d09a943cf 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -215,6 +215,7 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password if len(args) > 0: from inspect import getargspec keys = getargspec(func).args + keys.remove('uo') for k, arg in enumerate(args): kwargs[keys[k]] = arg args = () @@ -250,6 +251,7 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password result = func(*args, **kwargs) except Exception as e: uo.error(e) + import pdb;pdb.set_trace() raise e else: uo.success() From 312429d92a0da466691645b61d4fbe9419750aae Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 11:25:37 +0200 Subject: [PATCH 067/103] [fix] Reraise correctly the error --- src/yunohost/log.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index d09a943cf..d55bf963d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -251,8 +251,8 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password result = func(*args, **kwargs) except Exception as e: uo.error(e) - import pdb;pdb.set_trace() - raise e + t, v, tb = exc_info() + raise t, v, tb else: uo.success() return result From 031ff9922376f89acebbeac6d0d7a6468a625cb5 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 11:36:11 +0200 Subject: [PATCH 068/103] [fix] List arguments not correctly managed --- src/yunohost/log.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index d55bf963d..ef675e3ef 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -215,7 +215,8 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password if len(args) > 0: from inspect import getargspec keys = getargspec(func).args - keys.remove('uo') + if 'uo' in keys: + keys.remove('uo') for k, arg in enumerate(args): kwargs[keys[k]] = arg args = () @@ -231,7 +232,7 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password related_to.append((entity_type, kwargs[entity])) else: for x in kwargs[entity]: - related_to.append((entity_type, kwargs[x])) + related_to.append((entity_type, x)) context = kwargs.copy() From a54f70d9f5d3e5ffe024582b573cf5124f7cb0a4 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 13:15:34 +0200 Subject: [PATCH 069/103] [fix] Display error correctly --- src/yunohost/domain.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 774ab928e..22b5a33ee 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -116,13 +116,16 @@ def domain_add(auth, domain, dyndns=False): service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'rmilter']) app_ssowatconf(auth) - except: + except Exception, e: + from sys import exc_info; + t, v, tb = exc_info() + # Force domain removal silently try: domain_remove(auth, domain, True) except: pass - raise + raise t, v, tb hook_callback('post_domain_add', args=[domain]) From 6601fb13fcac35900e9c44945e46ad5554c956db Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 13:16:40 +0200 Subject: [PATCH 070/103] [fix] Replace dict by tuple --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 69043bdbd..8fd9677a1 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -162,7 +162,7 @@ def _certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: - uo = UnitOperation('selfsigned_cert_install', [{'domain', domain}], + uo = UnitOperation('selfsigned_cert_install', [('domain', domain)], args={'force': force}) uo.start() From 65d8d88a097dea18cc78fe4e7cb7b4af130d4089 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 16:32:12 +0200 Subject: [PATCH 071/103] [fix] Some unit operation setup --- locales/en.json | 2 ++ src/yunohost/app.py | 11 +++++++---- src/yunohost/backup.py | 5 +++-- src/yunohost/certificate.py | 4 ++-- src/yunohost/log.py | 7 +++++-- src/yunohost/tools.py | 2 +- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/locales/en.json b/locales/en.json index 0ba225e79..1ca2dabae 100644 --- a/locales/en.json +++ b/locales/en.json @@ -217,6 +217,7 @@ "log_app_addaccess": "Add access to '{}'", "log_app_removeaccess": "Remove access to '{}'", "log_app_clearaccess": "Remove all access to '{}'", + "log_app_fetchlist": "Add an application list", "log_app_removelist": "Remove an application list", "log_app_change_url": "Change the url of '{}' application", "log_app_install": "Install '{}' application", @@ -234,6 +235,7 @@ "log_letsencrypt_cert_install": "Install Let's encrypt certificate on '{}' domain", "log_selfsigned_cert_install": "Install self signed certificate on '{}' domain", "log_letsencrypt_cert_renew": "Renew '{}' Let's encrypt certificate", + "log_service_enable": "Enable '{}' service", "log_service_regen_conf": "Regenerate system configurations '{}'", "log_user_create": "Add '{}' user", "log_user_delete": "Delete '{}' user", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 7ec1ddc80..9dc0cea6f 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -44,7 +44,7 @@ from moulinette.utils.log import getActionLogger from yunohost.service import service_log, _run_service_command from yunohost.utils import packages -from yunohost.log import is_unit_operation +from yunohost.log import is_unit_operation, UnitOperation logger = getActionLogger('yunohost.app') @@ -110,10 +110,13 @@ def app_fetchlist(url=None, name=None): # the fetch only this list if url is not None: if name: + uo = UnitOperation('app_fetchlist') + uo.start() _register_new_appslist(url, name) # Refresh the appslists dict appslists = _read_appslist_list() appslists_to_be_fetched = [name] + uo.success() else: raise MoulinetteError(errno.EINVAL, m18n.n('custom_appslist_name_required')) @@ -622,7 +625,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Start register change on system related_to = [('app', app_instance_name)] - uo = UnitOperation('app_upgrade', related_to, env=env_dict) + uo = unitoperation('app_upgrade', related_to, env=env_dict) uo.start() # Execute App upgrade script @@ -950,7 +953,7 @@ def app_addaccess(auth, apps, users=[]): logger.warning(m18n.n('user_unknown', user=allowed_user)) continue allowed_users.add(allowed_user) - uo.related_to.add(('user', allowed_user)) + uo.related_to.append(('user', allowed_user)) uo.flush() new_users = ','.join(allowed_users) @@ -1010,7 +1013,7 @@ def app_removeaccess(auth, apps, users=[]): else: for allowed_user in user_list(auth)['users'].keys(): if allowed_user not in users: - allowed_users.add(allowed_user) + allowed_users.append(allowed_user) uo.related_to += [ ('user', x) for x in allowed_users ] uo.flush() diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index a95d75a45..029f2d6b5 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -51,6 +51,7 @@ from yunohost.hook import ( from yunohost.monitor import binary_to_human from yunohost.tools import tools_postinstall from yunohost.service import service_regen_conf +from yunohost.log import UnitOperation, is_unit_operation BACKUP_PATH = '/home/yunohost.backup' ARCHIVES_PATH = '%s/archives' % BACKUP_PATH @@ -1132,7 +1133,7 @@ class RestoreManager(): logger.info(m18n.n('restore_running_hooks')) env_dict = self._get_env_var() - uo.extra.env = env_dict + uo.extra['env'] = env_dict uo.flush() ret = hook_callback('restore', system_targets, @@ -1247,7 +1248,7 @@ class RestoreManager(): # Prepare env. var. to pass to script env_dict = self._get_env_var(app_instance_name) - uo.extra.env = env_dict + uo.extra['env'] = env_dict uo.flush() # Execute app restore script diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 8fd9677a1..aada9de8f 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -287,7 +287,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F # Actual install steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_install', [{'domain', domain}], + uo = UnitOperation('letsencrypt_cert_install', [{'domain': domain}], args={'force': force, 'no_checks': no_checks, 'staging': staging}) uo.start() @@ -388,7 +388,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Actual renew steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_renew', [{'domain', domain}], + uo = UnitOperation('letsencrypt_cert_renew', [{'domain': domain}], args={'force': force, 'no_checks': no_checks, 'staging': staging, 'email': email}) uo.start() diff --git a/src/yunohost/log.py b/src/yunohost/log.py index ef675e3ef..bcfb93dfb 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -68,7 +68,7 @@ def log_list(category=[], limit=None): category_path = os.path.join(CATEGORIES_PATH, category) if not os.path.exists(category_path): - logger.warning(m18n.n('log_category_404', category=category)) + logger.debug(m18n.n('log_category_404', category=category)) continue @@ -327,7 +327,10 @@ class UnitOperation(object): name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] if self.related_to: - name += [self.related_to[0][1]] + if isinstance(self.related_to[0], tuple): + name += [self.related_to[0][1]] + else: + name += self.related_to[0].values() return '-'.join(name) @property diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 8ffeaecf4..649fc27a1 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -52,7 +52,7 @@ from yunohost.service import service_status, service_regen_conf, service_log, se from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version from yunohost.utils.network import get_public_ip -from yunohost.log import is_unit_operation +from yunohost.log import is_unit_operation, UnitOperation # FIXME this is a duplicate from apps.py APPS_SETTING_PATH = '/etc/yunohost/apps/' From 9e1dac99f723132a6c2fd652be726db093014ea8 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 17:38:46 +0200 Subject: [PATCH 072/103] [fix] Avoid double log if name change --- src/yunohost/log.py | 7 ++++++- src/yunohost/service.py | 13 +++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index bcfb93dfb..859e4b43a 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -278,6 +278,7 @@ class UnitOperation(object): self.started_at = None self.ended_at = None self.logger = None + self._name = None self.path = OPERATIONS_PATH @@ -324,6 +325,9 @@ class UnitOperation(object): Name of the operation This name is used as filename, so don't use space """ + if self._name is not None: + return self._name + name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] if self.related_to: @@ -331,7 +335,8 @@ class UnitOperation(object): name += [self.related_to[0][1]] else: name += self.related_to[0].values() - return '-'.join(name) + self._name = '-'.join(name) + return self._name @property def metadata(self): diff --git a/src/yunohost/service.py b/src/yunohost/service.py index d90965d3e..6306f0b8a 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -290,8 +290,6 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False """ result = {} - if not dry_run: - uo.start() # Return the list of pending conf if list_pending: pending_conf = _get_pending_conf(names) @@ -305,6 +303,12 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False } return pending_conf + if not dry_run: + uo.related_to = [('service', x) for x in names] + if not names: + uo.related_to = [('service', 'all')] + uo.start() + # Clean pending conf directory if os.path.isdir(PENDING_CONF_DIR): if not names: @@ -340,8 +344,13 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False # Set the processing method _regen = _process_regen_conf if not dry_run else lambda *a, **k: True + uo.related_to = [] + # Iterate over services and process pending conf for service, conf_files in _get_pending_conf(names).items(): + if not dry_run: + uo.related_to.append(('service', service)) + logger.info(m18n.n( 'service_regenconf_pending_applying' if not dry_run else 'service_regenconf_dry_pending_applying', From e56f8dafbb73ae9553a39e979e36e8840cebb2b0 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 6 Jun 2018 09:52:18 +0200 Subject: [PATCH 073/103] [fix] Bad call --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 9dc0cea6f..8ad57b636 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -625,7 +625,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Start register change on system related_to = [('app', app_instance_name)] - uo = unitoperation('app_upgrade', related_to, env=env_dict) + uo = UnitOperation('app_upgrade', related_to, env=env_dict) uo.start() # Execute App upgrade script From f1181540c67938d2081dbca774a9c86e935f5ba2 Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 22 Jun 2018 05:34:18 +0200 Subject: [PATCH 074/103] [mod] correct year for copyright stuff --- src/yunohost/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 859e4b43a..b9d31538a 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -2,7 +2,7 @@ """ License - Copyright (C) 2016 YunoHost + Copyright (C) 2018 YunoHost This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published From b2d07d1c6edd1583a483e4c855a3e6aacbdc4710 Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 22 Jun 2018 05:56:27 +0200 Subject: [PATCH 075/103] [mod] simplify code "raise" alone will do exactly the same thing. --- src/yunohost/log.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index b9d31538a..e9ce97bf8 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -252,8 +252,7 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password result = func(*args, **kwargs) except Exception as e: uo.error(e) - t, v, tb = exc_info() - raise t, v, tb + raise else: uo.success() return result From d328864a43c43c827264eceb8e1ac52c67350037 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 21 Jul 2018 16:26:27 +0000 Subject: [PATCH 076/103] Fix app name in case an url is provided (otherwise it tries to create a file with https://... in the name) --- src/yunohost/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 92f5584a6..e443f5178 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -753,6 +753,8 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False # Start register change on system uo.extra.update({'env':env_dict}) + uo.related_to = [s for s in uo.related_to if s[0] != "app"] + uo.related_to.append(("app", app_id)) uo.start() # Create app directory From fe48c37ce43b4e3817030560a3124c4a4ba5b7b2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 21 Jul 2018 16:34:23 +0000 Subject: [PATCH 077/103] Attempt to improve wording --- locales/en.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index fbfdb18b3..97425a4bd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -208,10 +208,10 @@ "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "log_corrupted_md_file": "The yaml metadata file associated with logs is corrupted : '{md_file}'", "log_category_404": "The log category '{category}' does not exist", - "log_link_to_log": "Complete log of this operation: '{desc}'", - "log_help_to_get_log": "To view complete log of the operation '{desc}', run: 'yunohost log display {name}'", - "log_link_to_failed_log": "The operation '{desc}' has failed ! To get help please give the complete log", - "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help please give the complete log of the operation, you can get it by running: 'yunohost log display {name}'", + "log_link_to_log": "Full log of this operation: '{desc}'", + "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log display {name}'", + "log_link_to_failed_log": "The operation '{desc}' has failed ! To get help, please provide the full log of this operation", + "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help, please provide the full log of this operation. It can be obtained with the command 'yunohost log display {name}'", "log_category_404": "The log category '{category}' does not exist", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", From eb42a25ada17dcbd421129924600b67451d5cb65 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 21 Jul 2018 17:55:05 +0000 Subject: [PATCH 078/103] Add python helper equivalent to yunopaste --- src/yunohost/utils/yunopaste.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/yunohost/utils/yunopaste.py diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py new file mode 100644 index 000000000..c084d78ce --- /dev/null +++ b/src/yunohost/utils/yunopaste.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +import requests +import json +import errno + +from moulinette.core import MoulinetteError + +def yunopaste(data): + + paste_server = "https://paste.yunohost.org" + + try: + r = requests.post("%s/documents" % paste_server, data=data) + except Exception as e: + raise MoulinetteError(errno.EIO, + "Something wrong happened while trying to paste data on paste.yunohost.org : %s" % str(e)) + + if r.status_code != 200: + raise MoulinetteError(errno.EIO, + "Something wrong happened while trying to paste data on paste.yunohost.org : %s" % r.text) + + try: + url = json.loads(r.text)["key"] + except: + raise MoulinetteError(errno.EIO, + "Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text) + + return "%s/raw/%s" % (paste_server, url) From 8eaceefbbd1d63a2eb8b18e60db1c55764f9e1c6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 21 Jul 2018 17:59:51 +0000 Subject: [PATCH 079/103] Add a --share option to 'log display' to share the log on yunopaste --- data/actionsmap/yunohost.yml | 3 +++ locales/en.json | 2 +- src/yunohost/log.py | 15 ++++++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 8e3c2ed54..209525de0 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1693,3 +1693,6 @@ log: help: Number of lines to display default: 50 type: int + --share: + help: Share the full log using yunopaste + action: store_true diff --git a/locales/en.json b/locales/en.json index 97425a4bd..c6310f6fb 100644 --- a/locales/en.json +++ b/locales/en.json @@ -211,7 +211,7 @@ "log_link_to_log": "Full log of this operation: '{desc}'", "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log display {name}'", "log_link_to_failed_log": "The operation '{desc}' has failed ! To get help, please provide the full log of this operation", - "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help, please provide the full log of this operation. It can be obtained with the command 'yunohost log display {name}'", + "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help, please share the full log of this operation using the command 'yunohost log display {name} --share'", "log_category_404": "The log category '{category}' does not exist", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index e9ce97bf8..f079a3894 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -36,6 +36,7 @@ from sys import exc_info from moulinette import m18n, msettings from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_file CATEGORIES_PATH = '/var/log/yunohost/categories/' OPERATIONS_PATH = '/var/log/yunohost/categories/operation/' @@ -103,7 +104,7 @@ def log_list(category=[], limit=None): return result -def log_display(path, number=50): +def log_display(path, number=50, share=False): """ Display a log file enriched with metadata if any. @@ -113,6 +114,7 @@ def log_display(path, number=50): Argument: file_name number + share """ # Normalize log/metadata paths and filenames @@ -147,6 +149,17 @@ def log_display(path, number=50): infos["description"] = _get_description_from_name(base_filename) infos['name'] = base_filename + if share: + from yunohost.utils.yunopaste import yunopaste + content = "" + if os.path.exists(md_path): + content += read_file(md_path) + content += "\n============\n\n" + if os.path.exists(log_path): + content += read_file(log_path) + + return yunopaste(content) + # Display metadata if exist if os.path.exists(md_path): with open(md_path, "r") as md_file: From f52f81cba64e4618e79dc655c6ef31590a10314b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Jul 2018 16:38:01 +0000 Subject: [PATCH 080/103] Fix _tail function, hopefully that's the right fix --- src/yunohost/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 21b74c559..b5a7a5c11 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -745,7 +745,8 @@ def _tail(file, n): previous_log_file = _find_previous_log_file(file) if previous_log_file is not None: lines = _tail(previous_log_file, to_read - len(lines)) + lines - return lines[-to_read:offset and -offset or None] + + return lines def _find_previous_log_file(file): From f89c41f3e472b13e9b51e31024e2465a546d17d9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Jul 2018 16:38:53 +0000 Subject: [PATCH 081/103] Improve behavior of yunopaste() for integration with the webadmin --- locales/en.json | 1 + src/yunohost/log.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index c6310f6fb..52e102af4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -225,6 +225,7 @@ "log_app_remove": "Remove '{}' application", "log_app_upgrade": "Upgrade '{}' application", "log_app_makedefault": "Make '{}' as default application", + "log_available_on_yunopaste": "This log is now available via {url}", "log_backup_restore_system": "Restore system from a backup archive", "log_backup_restore_app": "Restore '{}' from a backup archive", "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index f079a3894..5443c6beb 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -158,7 +158,13 @@ def log_display(path, number=50, share=False): if os.path.exists(log_path): content += read_file(log_path) - return yunopaste(content) + url = yunopaste(content) + + logger.info(m18n.n("log_available_on_yunopaste", url=url)) + if msettings.get('interface') == 'api': + return { "url" : url } + else: + return # Display metadata if exist if os.path.exists(md_path): From 6d6d778a7b041ea772373785b90e2257ec880ae2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Jul 2018 17:06:09 +0000 Subject: [PATCH 082/103] Display log list in chronological order in CLI to avoid having to scroll to get the last action --- src/yunohost/log.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 5443c6beb..35cd80fe2 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -57,10 +57,10 @@ def log_list(category=[], limit=None): """ categories = category + is_api = msettings.get('interface') == 'api' # In cli we just display `operation` logs by default if not categories: - is_api = msettings.get('interface') == 'api' categories = ["operation"] if not is_api else CATEGORIES result = collections.OrderedDict() @@ -101,6 +101,11 @@ def log_list(category=[], limit=None): result[category].append(entry) + # Reverse the order of log when in cli, more comfortable to read (avoid unecessary scrolling) + if not is_api: + for category in result: + result[category] = list(reversed(result[category])) + return result From 7ab13b56c9f598a4f57b1485b49097d534eec09e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Jul 2017 17:16:10 -0400 Subject: [PATCH 083/103] Add postsrsd as dependency --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 256038598..3c74fea76 100644 --- a/debian/control +++ b/debian/control @@ -18,7 +18,7 @@ Depends: ${python:Depends}, ${misc:Depends} , ca-certificates, netcat-openbsd, iproute , mariadb-server, php-mysql | php-mysqlnd , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd - , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils + , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils, postsrsd , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban , nginx-extras (>=1.6.2), php-fpm, php-ldap, php-intl From 240158ffaf1eab8e298c589e250858e3a4757c25 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Jul 2017 17:16:47 -0400 Subject: [PATCH 084/103] Link postfix to postsrsd for SRS --- data/templates/postfix/main.cf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf index 2cb1d8d72..c38896a3f 100644 --- a/data/templates/postfix/main.cf +++ b/data/templates/postfix/main.cf @@ -137,8 +137,10 @@ smtpd_recipient_restrictions = permit # SRS -sender_canonical_maps = regexp:/etc/postfix/sender_canonical +sender_canonical_maps = tcp:localhost:10001 sender_canonical_classes = envelope_sender +recipient_canonical_maps = tcp:localhost:10002 +recipient_canonical_classes= envelope_recipient,header_recipient # Ignore some headers smtp_header_checks = regexp:/etc/postfix/header_checks From b1fdb39880dd00afc2b1b38a7d1a6611e67445e2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Jul 2017 17:17:25 -0400 Subject: [PATCH 085/103] Tweaking postfix regen-conf to handle postsrsd conf --- data/hooks/conf_regen/19-postfix | 12 ++++++++- data/templates/postfix/postsrsd | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 data/templates/postfix/postsrsd diff --git a/data/hooks/conf_regen/19-postfix b/data/hooks/conf_regen/19-postfix index 3cb5cdf50..2133c1bd5 100755 --- a/data/hooks/conf_regen/19-postfix +++ b/data/hooks/conf_regen/19-postfix @@ -10,15 +10,25 @@ do_pre_regen() { postfix_dir="${pending_dir}/etc/postfix" mkdir -p "$postfix_dir" + default_dir="${pending_dir}/etc/default/" + mkdir -p "$default_dir" + # install plain conf files cp plain/* "$postfix_dir" # prepare main.cf conf file main_domain=$(cat /etc/yunohost/current_host) + domain_list=$(sudo yunohost domain list --output-as plain --quiet) + cat main.cf \ | sed "s/{{ main_domain }}/${main_domain}/g" \ > "${postfix_dir}/main.cf" + cat postsrsd \ + | sed "s/{{ main_domain }}/${main_domain}/g" \ + | sed "s/{{ domain_list }}/${domain_list}/g" \ + > "${default_dir}/postsrsd" + # adapt it for IPv4-only hosts if [ ! -f /proc/net/if_inet6 ]; then sed -i \ @@ -34,7 +44,7 @@ do_post_regen() { regen_conf_files=$1 [[ -z "$regen_conf_files" ]] \ - || sudo service postfix restart + || { sudo service postfix restart && sudo service postsrsd restart } } FORCE=${2:-0} diff --git a/data/templates/postfix/postsrsd b/data/templates/postfix/postsrsd new file mode 100644 index 000000000..56bfd091e --- /dev/null +++ b/data/templates/postfix/postsrsd @@ -0,0 +1,43 @@ +# Default settings for postsrsd + +# Local domain name. +# Addresses are rewritten to originate from this domain. The default value +# is taken from `postconf -h mydomain` and probably okay. +# +SRS_DOMAIN={{ main_domain }} + +# Exclude additional domains. +# You may list domains which shall not be subjected to address rewriting. +# If a domain name starts with a dot, it matches all subdomains, but not +# the domain itself. Separate multiple domains by space or comma. +# We have to put some "dummy" stuff at start and end... see this comment : +# https://github.com/roehling/postsrsd/issues/64#issuecomment-284003762 +SRS_EXCLUDE_DOMAINS=dummy {{ domain_list }} dummy + +# First separator character after SRS0 or SRS1. +# Can be one of: -+= +SRS_SEPARATOR== + +# Secret key to sign rewritten addresses. +# When postsrsd is installed for the first time, a random secret is generated +# and stored in /etc/postsrsd.secret. For most installations, that's just fine. +# +SRS_SECRET=/etc/postsrsd.secret + +# Local ports for TCP list. +# These ports are used to bind the TCP list for postfix. If you change +# these, you have to modify the postfix settings accordingly. The ports +# are bound to the loopback interface, and should never be exposed on +# the internet. +# +SRS_FORWARD_PORT=10001 +SRS_REVERSE_PORT=10002 + +# Drop root privileges and run as another user after initialization. +# This is highly recommended as postsrsd handles untrusted input. +# +RUN_AS=postsrsd + +# Jail daemon in chroot environment +CHROOT=/var/lib/postsrsd + From 8e49f9db6d5809815612bc15ee744283a67b924a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Jul 2017 17:35:17 -0400 Subject: [PATCH 086/103] Fix after test --- data/hooks/conf_regen/19-postfix | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/data/hooks/conf_regen/19-postfix b/data/hooks/conf_regen/19-postfix index 2133c1bd5..a3ad70327 100755 --- a/data/hooks/conf_regen/19-postfix +++ b/data/hooks/conf_regen/19-postfix @@ -18,7 +18,7 @@ do_pre_regen() { # prepare main.cf conf file main_domain=$(cat /etc/yunohost/current_host) - domain_list=$(sudo yunohost domain list --output-as plain --quiet) + domain_list=$(sudo yunohost domain list --output-as plain --quiet | tr '\n' ' ') cat main.cf \ | sed "s/{{ main_domain }}/${main_domain}/g" \ @@ -44,7 +44,8 @@ do_post_regen() { regen_conf_files=$1 [[ -z "$regen_conf_files" ]] \ - || { sudo service postfix restart && sudo service postsrsd restart } + || { sudo service postfix restart && sudo service postsrsd restart; } + } FORCE=${2:-0} From 226ee1abcbfeba863b93339882fef821dd87b074 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Jul 2017 18:03:12 -0400 Subject: [PATCH 087/103] Adding/removing a domain now affect postfix/postsrsd conf --- src/yunohost/domain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 913b7868e..52660bc48 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -110,7 +110,7 @@ def domain_add(auth, domain, dyndns=False): # Don't regen these conf if we're still in postinstall if os.path.exists('/etc/yunohost/installed'): - service_regen_conf(names=['nginx', 'metronome', 'dnsmasq']) + service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix']) app_ssowatconf(auth) except: @@ -162,7 +162,7 @@ def domain_remove(auth, domain, force=False): else: raise MoulinetteError(errno.EIO, m18n.n('domain_deletion_failed')) - service_regen_conf(names=['nginx', 'metronome', 'dnsmasq']) + service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix']) app_ssowatconf(auth) hook_callback('post_domain_remove', args=[domain]) From 3bd14aa6fd1ec423e55289440a9d4221c2935fe4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Aug 2018 16:22:36 +0200 Subject: [PATCH 088/103] Remove old 'list of dict' style, only use 'list of 2-tuples' --- src/yunohost/certificate.py | 4 ++-- src/yunohost/log.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index eeefc7a3f..4584203e6 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -284,7 +284,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F # Actual install steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_install', [{'domain': domain}], + uo = UnitOperation('letsencrypt_cert_install', [('domain': domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging}) uo.start() @@ -385,7 +385,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Actual renew steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_renew', [{'domain': domain}], + uo = UnitOperation('letsencrypt_cert_renew', [('domain': domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging, 'email': email}) uo.start() diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 35cd80fe2..f098e523d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -354,10 +354,7 @@ class UnitOperation(object): name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] if self.related_to: - if isinstance(self.related_to[0], tuple): - name += [self.related_to[0][1]] - else: - name += self.related_to[0].values() + name += [self.related_to[0][1]] self._name = '-'.join(name) return self._name From c069f8621c324aedf95ffacb611d57f8f168964d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Aug 2018 16:28:16 +0200 Subject: [PATCH 089/103] Since info is now always displayed, move this message to debug instead --- src/yunohost/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index f098e523d..cbde6ab9e 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -412,7 +412,7 @@ class UnitOperation(object): msg = m18n.n('log_link_to_log', name=self.name, desc=desc) else: msg = m18n.n('log_help_to_get_log', name=self.name, desc=desc) - logger.info(msg) + logger.debug(msg) else: if is_api: msg = "" + m18n.n('log_link_to_failed_log', From 934a19cea2ae9eaf93a0135aeb7edc79dbcb8ea1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Aug 2018 16:30:00 +0200 Subject: [PATCH 090/103] Display this as info, makes more sense --- src/yunohost/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index cbde6ab9e..50d48e9a0 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -419,7 +419,7 @@ class UnitOperation(object): name=self.name, desc=desc) + "" else: msg = m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc) - logger.warning(msg) + logger.info(msg) self.flush() return msg From 363dfcd0b289a164f5f2cb3561fa5e6af9be4d74 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 5 Aug 2018 22:17:15 +0000 Subject: [PATCH 091/103] Alex was drunk :| --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 4584203e6..741e66f1f 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -284,7 +284,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F # Actual install steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_install', [('domain': domain)], + uo = UnitOperation('letsencrypt_cert_install', [('domain', domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging}) uo.start() @@ -385,7 +385,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Actual renew steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_renew', [('domain': domain)], + uo = UnitOperation('letsencrypt_cert_renew', [('domain', domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging, 'email': email}) uo.start() From 5e5ac0be507542a4c9cc9c2dfb6eb1c7c0887bbe Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 5 Aug 2018 23:34:13 +0000 Subject: [PATCH 092/103] =?UTF-8?q?Clarify=20handling=20of=20'not-really-u?= =?UTF-8?q?nitary'=20operations=20=C3=B4.o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/yunohost/log.py | 21 ++++++++++++++++----- src/yunohost/service.py | 4 +++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 50d48e9a0..4c3a334a5 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -353,8 +353,16 @@ class UnitOperation(object): name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] - if self.related_to: - name += [self.related_to[0][1]] + + if hasattr(self, "name_parameter_override"): + # This is for special cases where the operation is not really unitary + # For instance, the regen conf cannot be logged "per service" because of + # the way it's built + name.append(self.name_parameter_override) + elif self.related_to: + # We use the name of the first related thing + name.append(self.related_to[0][1]) + self._name = '-'.join(name) return self._name @@ -432,13 +440,16 @@ class UnitOperation(object): self.error(m18n.n('log_operation_unit_unclosed_properly')) def _get_description_from_name(name): - parts = name.split("-") + parts = name.split("-", 3) try: try: datetime.strptime(" ".join(parts[:2]), "%Y%m%d %H%M%S") except ValueError: - return m18n.n("log_" + parts[0], *parts[1:]) + key = "log_" + parts[0] + args = parts[1:] else: - return m18n.n("log_" + parts[2], *parts[3:]) + key = "log_" + parts[2] + args = parts[3:] + return m18n.n(key, *args) except IndexError: return name diff --git a/src/yunohost/service.py b/src/yunohost/service.py index b5a7a5c11..9f135fe35 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -381,7 +381,9 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False if not dry_run: uo.related_to = [('service', x) for x in names] if not names: - uo.related_to = [('service', 'all')] + uo.name_parameter_override = 'all' + elif len(names) != 1: + uo.name_parameter_override = str(len(uo.related_to))+'_services' uo.start() # Clean pending conf directory From 37c55477553f75e7ebb0ee808f02be19ae6010c7 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 6 Aug 2018 02:19:23 +0200 Subject: [PATCH 093/103] [enh] Don't display log advertisement if not needed --- src/yunohost/certificate.py | 16 ++++++++++------ src/yunohost/utils/errors.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 src/yunohost/utils/errors.py diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 741e66f1f..ee7972249 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -42,6 +42,7 @@ from moulinette.utils.log import getActionLogger import yunohost.domain from yunohost.utils.network import get_public_ip +from yunohost.utils.errors import YunoHostError from moulinette import m18n from yunohost.app import app_ssowatconf @@ -839,13 +840,15 @@ def _check_domain_is_ready_for_ACME(domain): # Check if IP from DNS matches public IP if not _dns_ip_match_public_ip(public_ip, domain): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) + raise YunoHostError(m18n.n( + 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain), + log_advertisement=False) # Check if domain seems to be accessible through HTTP? if not _domain_is_accessible_through_HTTP(public_ip, domain): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_domain_http_not_working', domain=domain)) + raise YunoHostError(m18n.n( + 'certmanager_domain_http_not_working', domain=domain), + log_advertisement=False) def _get_dns_ip(domain): @@ -854,8 +857,9 @@ def _get_dns_ip(domain): resolver.nameservers = DNS_RESOLVERS answers = resolver.query(domain, "A") except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_error_no_A_record', domain=domain)) + raise YunoHostError(m18n.n( + 'certmanager_error_no_A_record', domain=domain), + log_advertisement=False) return str(answers[0]) diff --git a/src/yunohost/utils/errors.py b/src/yunohost/utils/errors.py new file mode 100644 index 000000000..9359c605b --- /dev/null +++ b/src/yunohost/utils/errors.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2018 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" +import errno + +from moulinette.core import MoulinetteError + + +class YunoHostError(MoulinetteError): + """ + YunoHostError allows to indicate if we should or shouldn't display a message + to users about how to display logs about this error. + """ + + def __init__(self, message, log_advertisement=True): + self.log_advertisement = log_advertisement + super(YunoHostError, self).__init__(errno.EINVAL, message) From 5e99db7c63fa8901abc558d1b1d02c756718925c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 8 Aug 2018 17:05:52 +0000 Subject: [PATCH 094/103] Explicitely tell when to start the uo logging to avoid displaying the message about logs when not needed --- src/yunohost/app.py | 6 ++++-- src/yunohost/certificate.py | 10 ++++++---- src/yunohost/domain.py | 11 +++++++---- src/yunohost/dyndns.py | 11 +++++------ src/yunohost/tools.py | 5 +++-- src/yunohost/user.py | 12 ++++++++---- 6 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index e443f5178..bd892fddc 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -191,8 +191,8 @@ def app_fetchlist(url=None, name=None): _write_appslist_list(appslists) -@is_unit_operation() -def app_removelist(name): +@is_unit_operation(auto=False) +def app_removelist(uo, name): """ Remove list from the repositories @@ -206,6 +206,8 @@ def app_removelist(name): if name not in appslists.keys(): raise MoulinetteError(errno.ENOENT, m18n.n('appslist_unknown', appslist=name)) + uo.start() + # Remove json json_path = '%s/%s.json' % (REPO_PATH, name) if os.path.exists(json_path): diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index ee7972249..9a3bf3a34 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -162,7 +162,6 @@ def _certificate_install_selfsigned(domain_list, force=False): uo = UnitOperation('selfsigned_cert_install', [('domain', domain)], args={'force': force}) - uo.start() # Paths of files and folder we'll need date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") @@ -186,6 +185,8 @@ def _certificate_install_selfsigned(domain_list, force=False): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_attempt_to_replace_valid_cert', domain=domain)) + uo.start() + # Create output folder for new certificate stuff os.makedirs(new_cert_folder) @@ -288,8 +289,6 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F uo = UnitOperation('letsencrypt_cert_install', [('domain', domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging}) - uo.start() - logger.info( "Now attempting install of certificate for domain %s!", domain) @@ -297,6 +296,8 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F if not no_checks: _check_domain_is_ready_for_ACME(domain) + uo.start() + _configure_for_acme_challenge(auth, domain) _fetch_and_enable_new_certificate(domain, staging) _install_cron() @@ -389,7 +390,6 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal uo = UnitOperation('letsencrypt_cert_renew', [('domain', domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging, 'email': email}) - uo.start() logger.info( "Now attempting renewing of certificate for domain %s !", domain) @@ -398,6 +398,8 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if not no_checks: _check_domain_is_ready_for_ACME(domain) + uo.start() + _fetch_and_enable_new_certificate(domain, staging) logger.success( diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index cf4093c2a..d9da30d26 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -62,8 +62,8 @@ def domain_list(auth): return {'domains': result_list} -@is_unit_operation() -def domain_add(auth, domain, dyndns=False): +@is_unit_operation(auto=False) +def domain_add(uo, auth, domain, dyndns=False): """ Create a custom domain @@ -80,6 +80,8 @@ def domain_add(auth, domain, dyndns=False): except MoulinetteError: raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) + uo.start() + # DynDNS domain if dyndns: @@ -131,8 +133,8 @@ def domain_add(auth, domain, dyndns=False): logger.success(m18n.n('domain_created')) -@is_unit_operation() -def domain_remove(auth, domain, force=False): +@is_unit_operation(auto=False) +def domain_remove(uo, auth, domain, force=False): """ Delete domains @@ -163,6 +165,7 @@ def domain_remove(auth, domain, force=False): raise MoulinetteError(errno.EPERM, m18n.n('domain_uninstall_app_first')) + uo.start() if auth.remove('virtualdomain=' + domain + ',ou=domains') or force: os.system('rm -rf /etc/yunohost/certs/%s' % domain) else: diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index d9860c318..d4e6a99b7 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -127,7 +127,6 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= if domain is None: domain = _get_maindomain() uo.related_to.append(('domain', domain)) - uo.start() # Verify if domain is provided by subscribe_host if not _dyndns_provides(subscribe_host, domain): @@ -140,6 +139,8 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= raise MoulinetteError(errno.ENOENT, m18n.n('dyndns_unavailable', domain=domain)) + uo.start() + if key is None: if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0: if not os.path.exists('/etc/yunohost/dyndns'): @@ -217,17 +218,12 @@ def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, return else: logger.info("Updated needed, going on...") - if domain is not None: - uo.related_to.append(('domain', domain)) # If domain is not given, try to guess it from keys available... if domain is None: (domain, key) = _guess_current_dyndns_domain(dyn_host) - uo.related_to.append(('domain', domain)) - uo.start() # If key is not given, pick the first file we find with the domain given else: - uo.start() if key is None: keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) @@ -236,6 +232,9 @@ def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, key = keys[0] + uo.related_to.append(('domain', domain)) + uo.start() + # This mean that hmac-md5 is used # (Re?)Trigger the migration to sha256 and return immediately. # The actual update will be done in next run. diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index db4946c38..f02ba0414 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -249,8 +249,8 @@ def _is_inside_container(): return out.split()[1] != "(1," -@is_unit_operation() -def tools_postinstall(domain, password, ignore_dyndns=False): +@is_unit_operation(auto=False) +def tools_postinstall(uo, domain, password, ignore_dyndns=False): """ YunoHost post-install @@ -299,6 +299,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): else: dyndns = False + uo.start() logger.info(m18n.n('yunohost_installing')) service_regen_conf(['nslcd', 'nsswitch'], force=True) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 8efabd9bb..4b83b3a3e 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -99,8 +99,8 @@ def user_list(auth, fields=None): return {'users': users} -@is_unit_operation('username:user') -def user_create(auth, username, firstname, lastname, mail, password, +@is_unit_operation('username:user', auto=False) +def user_create(uo, auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ Create user @@ -136,6 +136,8 @@ def user_create(auth, username, firstname, lastname, mail, password, m18n.n('mail_domain_unknown', domain=mail.split("@")[1])) + uo.start() + # Get random UID/GID all_uid = {x.pw_uid for x in pwd.getpwall()} all_gid = {x.pw_gid for x in pwd.getpwall()} @@ -257,8 +259,8 @@ def user_delete(auth, username, purge=False): logger.success(m18n.n('user_deleted')) -@is_unit_operation('username:user', exclude='auth,change_password') -def user_update(auth, username, firstname=None, lastname=None, mail=None, +@is_unit_operation('username:user', exclude='auth,change_password', auto=False) +def user_update(uo, auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): """ @@ -359,6 +361,8 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, if mailbox_quota is not None: new_attr_dict['mailuserquota'] = mailbox_quota + uo.start() + if auth.update('uid=%s,ou=users' % username, new_attr_dict): logger.success(m18n.n('user_updated')) app_ssowatconf(auth) From 77b7f96b331a09a839ae9ae83c3fe3c2e99a5ba0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 9 Aug 2018 00:24:31 +0000 Subject: [PATCH 095/103] Remove 'auto' option, essentially irrelevant if we want to properly manage errors --- src/yunohost/app.py | 10 +++++----- src/yunohost/backup.py | 2 +- src/yunohost/certificate.py | 2 +- src/yunohost/domain.py | 4 ++-- src/yunohost/dyndns.py | 4 ++-- src/yunohost/log.py | 14 ++++---------- src/yunohost/service.py | 5 +++-- src/yunohost/tools.py | 10 +++++----- src/yunohost/user.py | 7 ++++--- 9 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index bd892fddc..fa106d689 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -191,7 +191,7 @@ def app_fetchlist(url=None, name=None): _write_appslist_list(appslists) -@is_unit_operation(auto=False) +@is_unit_operation() def app_removelist(uo, name): """ Remove list from the repositories @@ -429,7 +429,7 @@ def app_map(app=None, raw=False, user=None): return result -@is_unit_operation(auto=False) +@is_unit_operation() def app_change_url(uo, auth, app, domain, path): """ Modify the URL at which an application is installed. @@ -681,7 +681,7 @@ def app_upgrade(auth, app=[], url=None, file=None): return {"log": service_log('yunohost-api', number="100").values()[0]} -@is_unit_operation(auto=False) +@is_unit_operation() def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False): """ Install apps @@ -862,7 +862,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False hook_callback('post_app_install', args=args_list, env=env_dict) -@is_unit_operation(auto=False) +@is_unit_operation() def app_remove(uo, auth, app): """ Remove app @@ -1103,7 +1103,7 @@ def app_debug(app): } -@is_unit_operation(auto=False) +@is_unit_operation() def app_makedefault(uo, auth, app, domain=None): """ Redirect domain root to an app diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 3880f94cc..eb2a91cab 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -51,7 +51,7 @@ from yunohost.hook import ( from yunohost.monitor import binary_to_human from yunohost.tools import tools_postinstall from yunohost.service import service_regen_conf -from yunohost.log import UnitOperation, is_unit_operation +from yunohost.log import UnitOperation BACKUP_PATH = '/home/yunohost.backup' ARCHIVES_PATH = '%s/archives' % BACKUP_PATH diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 9a3bf3a34..4e40fe0f5 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -47,7 +47,7 @@ from yunohost.utils.errors import YunoHostError from moulinette import m18n from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command, service_regen_conf -from yunohost.log import is_unit_operation, UnitOperation +from yunohost.log import UnitOperation logger = getActionLogger('yunohost.certmanager') diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index d9da30d26..a42b99a89 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -62,7 +62,7 @@ def domain_list(auth): return {'domains': result_list} -@is_unit_operation(auto=False) +@is_unit_operation() def domain_add(uo, auth, domain, dyndns=False): """ Create a custom domain @@ -133,7 +133,7 @@ def domain_add(uo, auth, domain, dyndns=False): logger.success(m18n.n('domain_created')) -@is_unit_operation(auto=False) +@is_unit_operation() def domain_remove(uo, auth, domain, force=False): """ Delete domains diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index d4e6a99b7..263e2a123 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -113,7 +113,7 @@ def _dyndns_available(provider, domain): return r == u"Domain %s is available" % domain -@is_unit_operation(auto=False) +@is_unit_operation() def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key=None): """ Subscribe to a DynDNS service @@ -175,7 +175,7 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= dyndns_installcron() -@is_unit_operation(auto=False) +@is_unit_operation() def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, ipv4=None, ipv6=None): """ diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 4c3a334a5..735ce75a3 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -197,7 +197,7 @@ def log_display(path, number=50, share=False): return infos -def is_unit_operation(entities='app,domain,service,user', exclude='auth,password', operation_key=None, auto=True): +def is_unit_operation(entities='app,domain,service,user', exclude='auth,password', operation_key=None): """ Configure quickly a unit operation @@ -218,9 +218,6 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password well formed description you should add a translation key like this "log_" + operation_key in locales files. - auto If true, start the recording. If False, the unit operation object - created is given to the decorated function as the first argument and you can - start recording at the good time. """ def decorate(func): def func_wrapper(*args, **kwargs): @@ -266,13 +263,10 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password context.pop(field, None) uo = UnitOperation(op_key, related_to, args=context) - # Start to record or give the unit operation in argument to let the - # developper start the record itself - if auto: - uo.start() try: - if not auto: - args = (uo,) + args + # Start the actual function, and give the unit operation + # in argument to let the developper start the record itself + args = (uo,) + args result = func(*args, **kwargs) except Exception as e: uo.error(e) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 9f135fe35..71b554785 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -152,7 +152,7 @@ def service_stop(names): logger.debug(m18n.n('service_already_stopped', service=name)) @is_unit_operation() -def service_enable(names): +def service_enable(uo, names): """ Enable one or more services @@ -160,6 +160,7 @@ def service_enable(names): names -- Services name to enable """ + uo.start() if isinstance(names, str): names = [names] for name in names: @@ -344,7 +345,7 @@ def service_log(name, number=50): return result -@is_unit_operation('names:service', auto=False) +@is_unit_operation('names:service') def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False, list_pending=False): """ diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f02ba0414..1e87d98e5 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -139,7 +139,7 @@ def tools_adminpw(auth, new_password): logger.success(m18n.n('admin_password_changed')) -@is_unit_operation(auto=False) +@is_unit_operation() def tools_maindomain(uo, auth, new_domain=None): """ Check the current main domain, or change it @@ -249,7 +249,7 @@ def _is_inside_container(): return out.split()[1] != "(1," -@is_unit_operation(auto=False) +@is_unit_operation() def tools_postinstall(uo, domain, password, ignore_dyndns=False): """ YunoHost post-install @@ -473,7 +473,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): return {'packages': packages, 'apps': apps} -@is_unit_operation(auto=False) +@is_unit_operation() def tools_upgrade(uo, auth, ignore_apps=False, ignore_packages=False): """ Update apps & package cache, then display changelog @@ -713,7 +713,7 @@ def tools_port_available(port): return False -@is_unit_operation(auto=False) +@is_unit_operation() def tools_shutdown(uo, force=False): shutdown = force if not shutdown: @@ -732,7 +732,7 @@ def tools_shutdown(uo, force=False): subprocess.check_call(['systemctl', 'poweroff']) -@is_unit_operation(auto=False) +@is_unit_operation() def tools_reboot(uo, force=False): reboot = force if not reboot: diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 4b83b3a3e..e92256994 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -99,7 +99,7 @@ def user_list(auth, fields=None): return {'users': users} -@is_unit_operation('username:user', auto=False) +@is_unit_operation('username:user') def user_create(uo, auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ @@ -224,7 +224,7 @@ def user_create(uo, auth, username, firstname, lastname, mail, password, @is_unit_operation('username:user') -def user_delete(auth, username, purge=False): +def user_delete(uo, auth, username, purge=False): """ Delete user @@ -236,6 +236,7 @@ def user_delete(auth, username, purge=False): from yunohost.app import app_ssowatconf from yunohost.hook import hook_callback + uo.start() if auth.remove('uid=%s,ou=users' % username): # Invalidate passwd to take user deletion into account subprocess.call(['nscd', '-i', 'passwd']) @@ -259,7 +260,7 @@ def user_delete(auth, username, purge=False): logger.success(m18n.n('user_deleted')) -@is_unit_operation('username:user', exclude='auth,change_password', auto=False) +@is_unit_operation('username:user', exclude='auth,change_password') def user_update(uo, auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): From 625e87000867affe188b616346b54ae7a9753630 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 10 Aug 2018 00:52:46 +0200 Subject: [PATCH 096/103] Revert "[enh] Don't display log advertisement if not needed" This reverts commit 37c55477553f75e7ebb0ee808f02be19ae6010c7. --- src/yunohost/certificate.py | 16 ++++++---------- src/yunohost/utils/errors.py | 34 ---------------------------------- 2 files changed, 6 insertions(+), 44 deletions(-) delete mode 100644 src/yunohost/utils/errors.py diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 4e40fe0f5..d530568dc 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -42,7 +42,6 @@ from moulinette.utils.log import getActionLogger import yunohost.domain from yunohost.utils.network import get_public_ip -from yunohost.utils.errors import YunoHostError from moulinette import m18n from yunohost.app import app_ssowatconf @@ -842,15 +841,13 @@ def _check_domain_is_ready_for_ACME(domain): # Check if IP from DNS matches public IP if not _dns_ip_match_public_ip(public_ip, domain): - raise YunoHostError(m18n.n( - 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain), - log_advertisement=False) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) # Check if domain seems to be accessible through HTTP? if not _domain_is_accessible_through_HTTP(public_ip, domain): - raise YunoHostError(m18n.n( - 'certmanager_domain_http_not_working', domain=domain), - log_advertisement=False) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_http_not_working', domain=domain)) def _get_dns_ip(domain): @@ -859,9 +856,8 @@ def _get_dns_ip(domain): resolver.nameservers = DNS_RESOLVERS answers = resolver.query(domain, "A") except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): - raise YunoHostError(m18n.n( - 'certmanager_error_no_A_record', domain=domain), - log_advertisement=False) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_error_no_A_record', domain=domain)) return str(answers[0]) diff --git a/src/yunohost/utils/errors.py b/src/yunohost/utils/errors.py deleted file mode 100644 index 9359c605b..000000000 --- a/src/yunohost/utils/errors.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" -import errno - -from moulinette.core import MoulinetteError - - -class YunoHostError(MoulinetteError): - """ - YunoHostError allows to indicate if we should or shouldn't display a message - to users about how to display logs about this error. - """ - - def __init__(self, message, log_advertisement=True): - self.log_advertisement = log_advertisement - super(YunoHostError, self).__init__(errno.EINVAL, message) From 6c18e51b823ebd986e6e9c7484f340ddac06570a Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 10 Aug 2018 15:40:00 +0200 Subject: [PATCH 097/103] [enh] Replace string strange list --- src/yunohost/log.py | 46 +++++++++++++++++++++-------------------- src/yunohost/service.py | 2 +- src/yunohost/user.py | 6 +++--- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 735ce75a3..707a0b356 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -197,42 +197,41 @@ def log_display(path, number=50, share=False): return infos -def is_unit_operation(entities='app,domain,service,user', exclude='auth,password', operation_key=None): +def is_unit_operation(entities=['app', 'domain', 'service', 'user'], + exclude=['auth', 'password'], operation_key=None): """ Configure quickly a unit operation This decorator help you to configure quickly the record of a unit operations. Argument: - entities A list seperated by coma of entity types related to the unit - operation. The entity type is searched inside argument's names of the - decorated function. If something match, the argument value is added as - related entity. + entities A list of entity types related to the unit operation. The entity + type is searched inside argument's names of the decorated function. If + something match, the argument value is added as related entity. If the + argument name is different you can specify it with a tuple + (argname, entity_type) instead of just put the entity type. exclude Remove some arguments from the context. By default, arguments called 'password' and 'auth' are removed. If an argument is an object, you need to exclude it or create manually the unit operation without this decorator. - operation_key Key describing the unit operation. If you want to display a - well formed description you should add a translation key like this - "log_" + operation_key in locales files. + operation_key A key to describe the unit operation log used to create the + filename and search a translation. Please ensure that this key prefixed by + 'log_' is present in locales/en.json otherwise it won't be translatable. """ def decorate(func): def func_wrapper(*args, **kwargs): - # For a strange reason we can't use directly the arguments from - # is_unit_operation function. We need to store them in a var before. - entities_list = entities.split(',') - exclude_list = exclude.split(',') op_key = operation_key - related_to = [] - if op_key is None: op_key = func.__name__ - # In case the function is called directly from an other part of the - # code + # If the function is called directly from an other part of the code + # and not by the moulinette framework, we need to complete kwargs + # dictionnary with the args list. + # Indeed, we use convention naming in this decorator and we need to + # know name of each args (so we need to use kwargs instead of args) if len(args) > 0: from inspect import getargspec keys = getargspec(func).args @@ -243,10 +242,13 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password args = () # Search related entity in arguments of the decorated function - for entity in entities_list: - entity = entity.split(':') - entity_type = entity[-1] - entity = entity[0] + related_to = [] + for entity in entities: + if isinstance(entity, tuple): + entity_type = entity[1] + entity = entity[0] + else: + entity_type = entity if entity in kwargs and kwargs[entity] is not None: if isinstance(kwargs[entity], basestring): @@ -258,13 +260,13 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password context = kwargs.copy() # Exclude unappropriate data from the context - for field in exclude_list: + for field in exclude: if field in context: context.pop(field, None) uo = UnitOperation(op_key, related_to, args=context) try: - # Start the actual function, and give the unit operation + # Start the actual function, and give the unit operation # in argument to let the developper start the record itself args = (uo,) + args result = func(*args, **kwargs) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 71b554785..ba9c4450e 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -345,7 +345,7 @@ def service_log(name, number=50): return result -@is_unit_operation('names:service') +@is_unit_operation([('names', 'service')]) def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False, list_pending=False): """ diff --git a/src/yunohost/user.py b/src/yunohost/user.py index e92256994..9fb3f184a 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -99,7 +99,7 @@ def user_list(auth, fields=None): return {'users': users} -@is_unit_operation('username:user') +@is_unit_operation([('username', 'user')]) def user_create(uo, auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ @@ -223,7 +223,7 @@ def user_create(uo, auth, username, firstname, lastname, mail, password, raise MoulinetteError(169, m18n.n('user_creation_failed')) -@is_unit_operation('username:user') +@is_unit_operation([('username', 'user')]) def user_delete(uo, auth, username, purge=False): """ Delete user @@ -260,7 +260,7 @@ def user_delete(uo, auth, username, purge=False): logger.success(m18n.n('user_deleted')) -@is_unit_operation('username:user', exclude='auth,change_password') +@is_unit_operation([('username', 'user')], exclude=['auth', 'change_password']) def user_update(uo, auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): From a43b3dfa18529f698a285073d624ba42e759ddbf Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 10 Aug 2018 15:59:36 +0200 Subject: [PATCH 098/103] [fix] Pep8 --- src/yunohost/log.py | 51 +++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 707a0b356..ecf0e5e9e 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -40,7 +40,7 @@ from moulinette.utils.filesystem import read_file CATEGORIES_PATH = '/var/log/yunohost/categories/' OPERATIONS_PATH = '/var/log/yunohost/categories/operation/' -CATEGORIES = ['operation', 'history', 'package', 'system', 'access', 'service', \ +CATEGORIES = ['operation', 'history', 'package', 'system', 'access', 'service', 'app'] METADATA_FILE_EXT = '.yml' LOG_FILE_EXT = '.log' @@ -48,6 +48,7 @@ RELATED_CATEGORIES = ['app', 'domain', 'service', 'user'] logger = getActionLogger('yunohost.log') + def log_list(category=[], limit=None): """ List available logs @@ -73,7 +74,8 @@ def log_list(category=[], limit=None): continue - logs = filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(category_path)) + logs = filter(lambda x: x.endswith(METADATA_FILE_EXT), + os.listdir(category_path)) logs = reversed(sorted(logs)) if limit is not None: @@ -93,7 +95,8 @@ def log_list(category=[], limit=None): } entry["description"] = _get_description_from_name(base_filename) try: - log_datetime = datetime.strptime(" ".join(log[:2]), "%Y%m%d %H%M%S") + log_datetime = datetime.strptime(" ".join(log[:2]), + "%Y%m%d %H%M%S") except ValueError: pass else: @@ -101,7 +104,8 @@ def log_list(category=[], limit=None): result[category].append(entry) - # Reverse the order of log when in cli, more comfortable to read (avoid unecessary scrolling) + # Reverse the order of log when in cli, more comfortable to read (avoid + # unecessary scrolling) if not is_api: for category in result: result[category] = list(reversed(result[category])) @@ -131,7 +135,7 @@ def log_display(path, number=50, share=False): if os.path.exists(abs_path) or os.path.exists(abs_path + METADATA_FILE_EXT): break - if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT) : + if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT): log_path = abs_path if abs_path.endswith(METADATA_FILE_EXT) or abs_path.endswith(LOG_FILE_EXT): @@ -167,7 +171,7 @@ def log_display(path, number=50, share=False): logger.info(m18n.n("log_available_on_yunopaste", url=url)) if msettings.get('interface') == 'api': - return { "url" : url } + return {"url": url} else: return @@ -191,27 +195,27 @@ def log_display(path, number=50, share=False): if os.path.exists(log_path): from yunohost.service import _tail logs = _tail(log_path, int(number)) - #logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs if x] infos['log_path'] = log_path infos['logs'] = logs return infos + def is_unit_operation(entities=['app', 'domain', 'service', 'user'], exclude=['auth', 'password'], operation_key=None): """ Configure quickly a unit operation - This decorator help you to configure quickly the record of a unit operations. + This decorator help you to configure the record of a unit operations. Argument: - entities A list of entity types related to the unit operation. The entity + entities A list of entity types related to the unit operation. The entity type is searched inside argument's names of the decorated function. If something match, the argument value is added as related entity. If the argument name is different you can specify it with a tuple (argname, entity_type) instead of just put the entity type. - exclude Remove some arguments from the context. By default, arguments + exclude Remove some arguments from the context. By default, arguments called 'password' and 'auth' are removed. If an argument is an object, you need to exclude it or create manually the unit operation without this decorator. @@ -279,14 +283,15 @@ def is_unit_operation(entities=['app', 'domain', 'service', 'user'], return func_wrapper return decorate + class UnitOperation(object): """ - Instances of this class represents unit operation the yunohost admin as done. + Instances of this class represents unit operation done on the ynh instance. - Each time an action of the yunohost cli/api change the system, one or several - unit operations should be registered. + Each time an action of the yunohost cli/api change the system, one or + several unit operations should be registered. - This class record logs and some metadata like context or start time/end time. + This class record logs and metadata like context or start time/end time. """ def __init__(self, operation, related_to=None, **kwargs): @@ -351,9 +356,9 @@ class UnitOperation(object): name += [self.operation] if hasattr(self, "name_parameter_override"): - # This is for special cases where the operation is not really unitary - # For instance, the regen conf cannot be logged "per service" because of - # the way it's built + # This is for special cases where the operation is not really + # unitary. For instance, the regen conf cannot be logged "per + # service" because of the way it's built name.append(self.name_parameter_override) elif self.related_to: # We use the name of the first related thing @@ -379,7 +384,7 @@ class UnitOperation(object): data['success'] = self._success if self.error is not None: data['error'] = self._error - # TODO: detect if 'extra' erase some key of 'data' + # TODO: detect if 'extra' erase some key of 'data' data.update(self.extra) return data @@ -420,9 +425,10 @@ class UnitOperation(object): else: if is_api: msg = "" + m18n.n('log_link_to_failed_log', - name=self.name, desc=desc) + "" + name=self.name, desc=desc) + "" else: - msg = m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc) + msg = m18n.n('log_help_to_get_failed_log', name=self.name, + desc=desc) logger.info(msg) self.flush() return msg @@ -435,7 +441,12 @@ class UnitOperation(object): """ self.error(m18n.n('log_operation_unit_unclosed_properly')) + def _get_description_from_name(name): + """ + Return the translated description from the filename + """ + parts = name.split("-", 3) try: try: From b09d71f40ba5450dfa9f7c27c8c198c87a45d052 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 22 Aug 2018 16:26:44 +0000 Subject: [PATCH 099/103] Add timeout to POST request --- src/yunohost/utils/yunopaste.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index c084d78ce..6df4d0ca6 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -11,7 +11,7 @@ def yunopaste(data): paste_server = "https://paste.yunohost.org" try: - r = requests.post("%s/documents" % paste_server, data=data) + r = requests.post("%s/documents" % paste_server, data=data, timeout=30) except Exception as e: raise MoulinetteError(errno.EIO, "Something wrong happened while trying to paste data on paste.yunohost.org : %s" % str(e)) From 93cb07ed61f17a1debbe353963120ab117598f3f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 22 Aug 2018 16:27:11 +0000 Subject: [PATCH 100/103] Add status code to error message --- src/yunohost/utils/yunopaste.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index 6df4d0ca6..2b53062d1 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -18,7 +18,7 @@ def yunopaste(data): if r.status_code != 200: raise MoulinetteError(errno.EIO, - "Something wrong happened while trying to paste data on paste.yunohost.org : %s" % r.text) + "Something wrong happened while trying to paste data on paste.yunohost.org : %s, %s" % (r.status_code, r.text)) try: url = json.loads(r.text)["key"] From 860f7408103dab24bd89d5428224490176c17d92 Mon Sep 17 00:00:00 2001 From: Bram Date: Wed, 22 Aug 2018 19:30:58 +0200 Subject: [PATCH 101/103] [fix] bad call --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2c737da82..e3e5bb959 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2201,7 +2201,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): app_label=app_label, )) - raise MoulinetteError(errno.EINVAL, m18n.n('app_location_unavailable', "\n".join(apps=apps))) + raise MoulinetteError(errno.EINVAL, m18n.n('app_location_unavailable', apps="\n".join(apps))) # (We save this normalized path so that the install script have a # standard path format to deal with no matter what the user inputted) From 775a16d883694293f17bd2a17ecb212510a22c9e Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 23 Aug 2018 18:59:44 +0200 Subject: [PATCH 102/103] [enh] Rename uo in operation_logger --- src/yunohost/app.py | 92 ++++++++++++++++++------------------- src/yunohost/backup.py | 34 +++++++------- src/yunohost/certificate.py | 26 +++++------ src/yunohost/domain.py | 8 ++-- src/yunohost/dyndns.py | 12 ++--- src/yunohost/log.py | 14 +++--- src/yunohost/service.py | 20 ++++---- src/yunohost/tools.py | 40 ++++++++-------- src/yunohost/user.py | 12 ++--- 9 files changed, 129 insertions(+), 129 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0c35fa810..86abac7b3 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -44,7 +44,7 @@ from moulinette.utils.filesystem import read_json from yunohost.service import service_log, _run_service_command from yunohost.utils import packages -from yunohost.log import is_unit_operation, UnitOperation +from yunohost.log import is_unit_operation, OperationLogger logger = getActionLogger('yunohost.app') @@ -110,13 +110,13 @@ def app_fetchlist(url=None, name=None): # the fetch only this list if url is not None: if name: - uo = UnitOperation('app_fetchlist') - uo.start() + operation_logger = OperationLogger('app_fetchlist') + operation_logger.start() _register_new_appslist(url, name) # Refresh the appslists dict appslists = _read_appslist_list() appslists_to_be_fetched = [name] - uo.success() + operation_logger.success() else: raise MoulinetteError(errno.EINVAL, m18n.n('custom_appslist_name_required')) @@ -193,7 +193,7 @@ def app_fetchlist(url=None, name=None): @is_unit_operation() -def app_removelist(uo, name): +def app_removelist(operation_logger, name): """ Remove list from the repositories @@ -207,7 +207,7 @@ def app_removelist(uo, name): if name not in appslists.keys(): raise MoulinetteError(errno.ENOENT, m18n.n('appslist_unknown', appslist=name)) - uo.start() + operation_logger.start() # Remove json json_path = '%s/%s.json' % (REPO_PATH, name) @@ -433,7 +433,7 @@ def app_map(app=None, raw=False, user=None): @is_unit_operation() -def app_change_url(uo, auth, app, domain, path): +def app_change_url(operation_logger, auth, app, domain, path): """ Modify the URL at which an application is installed. @@ -491,9 +491,9 @@ def app_change_url(uo, auth, app, domain, path): env_dict["YNH_APP_NEW_PATH"] = path.rstrip("/") if domain != old_domain: - uo.related_to.append(('domain', old_domain)) - uo.extra.update({'env': env_dict}) - uo.start() + operation_logger.related_to.append(('domain', old_domain)) + operation_logger.extra.update({'env': env_dict}) + operation_logger.start() if os.path.exists(os.path.join(APP_TMP_FOLDER, "scripts")): shutil.rmtree(os.path.join(APP_TMP_FOLDER, "scripts")) @@ -515,7 +515,7 @@ def app_change_url(uo, auth, app, domain, path): if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root") != 0: msg = "Failed to change '%s' url." % app logger.error(msg) - uo.error(msg) + operation_logger.error(msg) # restore values modified by app_checkurl # see begining of the function @@ -630,8 +630,8 @@ def app_upgrade(auth, app=[], url=None, file=None): # Start register change on system related_to = [('app', app_instance_name)] - uo = UnitOperation('app_upgrade', related_to, env=env_dict) - uo.start() + operation_logger = OperationLogger('app_upgrade', related_to, env=env_dict) + operation_logger.start() # Apply dirty patch to make php5 apps compatible with php7 _patch_php5(extracted_app_folder) @@ -641,7 +641,7 @@ def app_upgrade(auth, app=[], url=None, file=None): if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root") != 0: msg = m18n.n('app_upgrade_failed', app=app_instance_name) logger.error(msg) - uo.error(msg) + operation_logger.error(msg) else: now = int(time.time()) # TODO: Move install_time away from app_setting @@ -671,7 +671,7 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.success(m18n.n('app_upgraded', app=app_instance_name)) hook_callback('post_app_upgrade', args=args_list, env=env_dict) - uo.success() + operation_logger.success() if not upgraded_apps: raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) @@ -686,7 +686,7 @@ def app_upgrade(auth, app=[], url=None, file=None): @is_unit_operation() -def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False): +def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on_failure=False): """ Install apps @@ -698,7 +698,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback - from yunohost.log import UnitOperation + from yunohost.log import OperationLogger # Fetch or extract sources @@ -758,10 +758,10 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False env_dict["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) # Start register change on system - uo.extra.update({'env':env_dict}) - uo.related_to = [s for s in uo.related_to if s[0] != "app"] - uo.related_to.append(("app", app_id)) - uo.start() + operation_logger.extra.update({'env':env_dict}) + operation_logger.related_to = [s for s in operation_logger.related_to if s[0] != "app"] + operation_logger.related_to.append(("app", app_id)) + operation_logger.start() # Create app directory app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) @@ -806,7 +806,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False logger.exception(m18n.n('unexpected_error')) finally: if install_retcode != 0: - error_msg = uo.error(m18n.n('unexpected_error')) + error_msg = operation_logger.error(m18n.n('unexpected_error')) if not no_remove_on_failure: # Setup environment for remove script env_dict_remove = {} @@ -815,10 +815,10 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) # Execute remove script - uo_remove = UnitOperation('remove_on_failed_install', + operation_logger_remove = OperationLogger('remove_on_failed_install', [('app', app_instance_name)], env=env_dict_remove) - uo_remove.start() + operation_logger_remove.start() remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), @@ -828,9 +828,9 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False msg = m18n.n('app_not_properly_removed', app=app_instance_name) logger.warning(msg) - uo_remove.error(msg) + operation_logger_remove.error(msg) else: - uo_remove.success() + operation_logger_remove.success() # Clean tmp folders shutil.rmtree(app_setting_path) @@ -868,7 +868,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False @is_unit_operation() -def app_remove(uo, auth, app): +def app_remove(operation_logger, auth, app): """ Remove app @@ -881,7 +881,7 @@ def app_remove(uo, auth, app): raise MoulinetteError(errno.EINVAL, m18n.n('app_not_installed', app=app)) - uo.start() + operation_logger.start() app_setting_path = APPS_SETTING_PATH + app @@ -906,8 +906,8 @@ def app_remove(uo, auth, app): env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - uo.extra.update({'env': env_dict}) - uo.flush() + operation_logger.extra.update({'env': env_dict}) + operation_logger.flush() if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0: logger.success(m18n.n('app_removed', app=app)) @@ -957,8 +957,8 @@ def app_addaccess(auth, apps, users=[]): # Start register change on system related_to = [('app', app)] - uo= UnitOperation('app_addaccess', related_to) - uo.start() + operation_logger= OperationLogger('app_addaccess', related_to) + operation_logger.start() allowed_users = set() if 'allowed_users' in app_settings: @@ -972,14 +972,14 @@ def app_addaccess(auth, apps, users=[]): logger.warning(m18n.n('user_unknown', user=allowed_user)) continue allowed_users.add(allowed_user) - uo.related_to.append(('user', allowed_user)) + operation_logger.related_to.append(('user', allowed_user)) - uo.flush() + operation_logger.flush() new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) hook_callback('post_app_addaccess', args=[app, new_users]) - uo.success() + operation_logger.success() result[app] = allowed_users @@ -1020,8 +1020,8 @@ def app_removeaccess(auth, apps, users=[]): # Start register change on system related_to = [('app', app)] - uo= UnitOperation('app_removeaccess', related_to) - uo.start() + operation_logger= OperationLogger('app_removeaccess', related_to) + operation_logger.start() if remove_all: pass @@ -1034,15 +1034,15 @@ def app_removeaccess(auth, apps, users=[]): if allowed_user not in users: allowed_users.append(allowed_user) - uo.related_to += [ ('user', x) for x in allowed_users ] - uo.flush() + operation_logger.related_to += [ ('user', x) for x in allowed_users ] + operation_logger.flush() new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) hook_callback('post_app_removeaccess', args=[app, new_users]) result[app] = allowed_users - uo.success() + operation_logger.success() app_ssowatconf(auth) @@ -1069,8 +1069,8 @@ def app_clearaccess(auth, apps): # Start register change on system related_to = [('app', app)] - uo= UnitOperation('app_clearaccess', related_to) - uo.start() + operation_logger= OperationLogger('app_clearaccess', related_to) + operation_logger.start() if 'mode' in app_settings: app_setting(app, 'mode', delete=True) @@ -1080,7 +1080,7 @@ def app_clearaccess(auth, apps): hook_callback('post_app_clearaccess', args=[app]) - uo.success() + operation_logger.success() app_ssowatconf(auth) @@ -1109,7 +1109,7 @@ def app_debug(app): @is_unit_operation() -def app_makedefault(uo, auth, app, domain=None): +def app_makedefault(operation_logger, auth, app, domain=None): """ Redirect domain root to an app @@ -1126,11 +1126,11 @@ def app_makedefault(uo, auth, app, domain=None): if domain is None: domain = app_domain - uo.related_to.append(('domain',domain)) + operation_logger.related_to.append(('domain',domain)) elif domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) - uo.start() + operation_logger.start() if '/' in app_map(raw=True)[domain]: raise MoulinetteError(errno.EEXIST, m18n.n('app_make_default_location_already_used', diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index eb2a91cab..88959cc2f 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -51,7 +51,7 @@ from yunohost.hook import ( from yunohost.monitor import binary_to_human from yunohost.tools import tools_postinstall from yunohost.service import service_regen_conf -from yunohost.log import UnitOperation +from yunohost.log import OperationLogger BACKUP_PATH = '/home/yunohost.backup' ARCHIVES_PATH = '%s/archives' % BACKUP_PATH @@ -1174,14 +1174,14 @@ class RestoreManager(): return # Start register change on system - uo = UnitOperation('backup_restore_system') - uo.start() + operation_logger = OperationLogger('backup_restore_system') + operation_logger.start() logger.debug(m18n.n('restore_running_hooks')) env_dict = self._get_env_var() - uo.extra['env'] = env_dict - uo.flush() + operation_logger.extra['env'] = env_dict + operation_logger.flush() ret = hook_callback('restore', system_targets, args=[self.work_dir], @@ -1198,9 +1198,9 @@ class RestoreManager(): error_part.append(part) if ret['failed']: - uo.error(m18n.n('restore_system_part_failed', part=', '.join(error_part))) + operation_logger.error(m18n.n('restore_system_part_failed', part=', '.join(error_part))) else: - uo.success() + operation_logger.success() service_regen_conf() @@ -1250,8 +1250,8 @@ class RestoreManager(): # Start register change on system related_to = [('app', app_instance_name)] - uo = UnitOperation('backup_restore_app', related_to) - uo.start() + operation_logger = OperationLogger('backup_restore_app', related_to) + operation_logger.start() # Check if the app is not already installed if _is_installed(app_instance_name): @@ -1302,8 +1302,8 @@ class RestoreManager(): # Prepare env. var. to pass to script env_dict = self._get_env_var(app_instance_name) - uo.extra['env'] = env_dict - uo.flush() + operation_logger.extra['env'] = env_dict + operation_logger.flush() # Execute app restore script hook_exec(restore_script, @@ -1315,7 +1315,7 @@ class RestoreManager(): except: msg = m18n.n('restore_app_failed',app=app_instance_name) logger.exception(msg) - uo.error(msg) + operation_logger.error(msg) self.targets.set_result("apps", app_instance_name, "Error") @@ -1328,10 +1328,10 @@ class RestoreManager(): env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - uo = UnitOperation('remove_on_failed_restore', + operation_logger = OperationLogger('remove_on_failed_restore', [('app', app_instance_name)], env=env_dict_remove) - uo.start() + operation_logger.start() # Execute remove script # TODO: call app_remove instead @@ -1339,9 +1339,9 @@ class RestoreManager(): env=env_dict_remove, user="root") != 0: msg = m18n.n('app_not_properly_removed', app=app_instance_name) logger.warning(msg) - uo.error(msg) + operation_logger.error(msg) else: - uo.success() + operation_logger.success() # Cleaning app directory shutil.rmtree(app_settings_new_path, ignore_errors=True) @@ -1349,7 +1349,7 @@ class RestoreManager(): # TODO Cleaning app hooks else: self.targets.set_result("apps", app_instance_name, "Success") - uo.success() + operation_logger.success() finally: # Cleaning temporary scripts directory shutil.rmtree(tmp_folder_for_app_restore, ignore_errors=True) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 091c20d1a..1b80b6b49 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -45,7 +45,7 @@ from yunohost.utils.network import get_public_ip from moulinette import m18n from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command, service_regen_conf -from yunohost.log import UnitOperation +from yunohost.log import OperationLogger logger = getActionLogger('yunohost.certmanager') @@ -160,7 +160,7 @@ def _certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: - uo = UnitOperation('selfsigned_cert_install', [('domain', domain)], + operation_logger = OperationLogger('selfsigned_cert_install', [('domain', domain)], args={'force': force}) # Paths of files and folder we'll need @@ -185,7 +185,7 @@ def _certificate_install_selfsigned(domain_list, force=False): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_attempt_to_replace_valid_cert', domain=domain)) - uo.start() + operation_logger.start() # Create output folder for new certificate stuff os.makedirs(new_cert_folder) @@ -243,11 +243,11 @@ def _certificate_install_selfsigned(domain_list, force=False): if status and status["CA_type"]["code"] == "self-signed" and status["validity"] > 3648: logger.success( m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) - uo.success() + operation_logger.success() else: msg = "Installation of self-signed certificate installation for %s failed !" % (domain) logger.error(msg) - uo.error(msg) + operation_logger.error(msg) def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False, staging=False): @@ -288,7 +288,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F # Actual install steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_install', [('domain', domain)], + operation_logger = OperationLogger('letsencrypt_cert_install', [('domain', domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging}) logger.info( @@ -298,7 +298,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F if not no_checks: _check_domain_is_ready_for_ACME(domain) - uo.start() + operation_logger.start() _configure_for_acme_challenge(auth, domain) _fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks) @@ -307,12 +307,12 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F logger.success( m18n.n("certmanager_cert_install_success", domain=domain)) - uo.success() + operation_logger.success() except Exception as e: _display_debug_information(domain) msg = "Certificate installation for %s failed !\nException: %s" % (domain, e) logger.error(msg) - uo.error(msg) + operation_logger.error(msg) def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): """ @@ -391,7 +391,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Actual renew steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_renew', [('domain', domain)], + operation_logger = OperationLogger('letsencrypt_cert_renew', [('domain', domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging, 'email': email}) @@ -402,14 +402,14 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if not no_checks: _check_domain_is_ready_for_ACME(domain) - uo.start() + operation_logger.start() _fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks) logger.success( m18n.n("certmanager_cert_renew_success", domain=domain)) - uo.success() + operation_logger.success() except Exception as e: import traceback @@ -418,7 +418,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal traceback.print_exc(file=stack) msg = "Certificate renewing for %s failed !" % (domain) logger.error(msg) - uo.error(msg) + operation_logger.error(msg) logger.error(stack.getvalue()) logger.error(str(e)) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 0881619ee..ddb046569 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -63,7 +63,7 @@ def domain_list(auth): @is_unit_operation() -def domain_add(uo, auth, domain, dyndns=False): +def domain_add(operation_logger, auth, domain, dyndns=False): """ Create a custom domain @@ -80,7 +80,7 @@ def domain_add(uo, auth, domain, dyndns=False): except MoulinetteError: raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) - uo.start() + operation_logger.start() # DynDNS domain if dyndns: @@ -134,7 +134,7 @@ def domain_add(uo, auth, domain, dyndns=False): @is_unit_operation() -def domain_remove(uo, auth, domain, force=False): +def domain_remove(operation_logger, auth, domain, force=False): """ Delete domains @@ -165,7 +165,7 @@ def domain_remove(uo, auth, domain, force=False): raise MoulinetteError(errno.EPERM, m18n.n('domain_uninstall_app_first')) - uo.start() + operation_logger.start() if auth.remove('virtualdomain=' + domain + ',ou=domains') or force: os.system('rm -rf /etc/yunohost/certs/%s' % domain) else: diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 263e2a123..88547b4db 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -114,7 +114,7 @@ def _dyndns_available(provider, domain): @is_unit_operation() -def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key=None): +def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", domain=None, key=None): """ Subscribe to a DynDNS service @@ -126,7 +126,7 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= """ if domain is None: domain = _get_maindomain() - uo.related_to.append(('domain', domain)) + operation_logger.related_to.append(('domain', domain)) # Verify if domain is provided by subscribe_host if not _dyndns_provides(subscribe_host, domain): @@ -139,7 +139,7 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= raise MoulinetteError(errno.ENOENT, m18n.n('dyndns_unavailable', domain=domain)) - uo.start() + operation_logger.start() if key is None: if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0: @@ -176,7 +176,7 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= @is_unit_operation() -def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, +def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, key=None, ipv4=None, ipv6=None): """ Update IP on DynDNS platform @@ -232,8 +232,8 @@ def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, key = keys[0] - uo.related_to.append(('domain', domain)) - uo.start() + operation_logger.related_to.append(('domain', domain)) + operation_logger.start() # This mean that hmac-md5 is used # (Re?)Trigger the migration to sha256 and return immediately. diff --git a/src/yunohost/log.py b/src/yunohost/log.py index ecf0e5e9e..c105b8279 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -239,8 +239,8 @@ def is_unit_operation(entities=['app', 'domain', 'service', 'user'], if len(args) > 0: from inspect import getargspec keys = getargspec(func).args - if 'uo' in keys: - keys.remove('uo') + if 'operation_logger' in keys: + keys.remove('operation_logger') for k, arg in enumerate(args): kwargs[keys[k]] = arg args = () @@ -267,24 +267,24 @@ def is_unit_operation(entities=['app', 'domain', 'service', 'user'], for field in exclude: if field in context: context.pop(field, None) - uo = UnitOperation(op_key, related_to, args=context) + operation_logger = OperationLogger(op_key, related_to, args=context) try: # Start the actual function, and give the unit operation # in argument to let the developper start the record itself - args = (uo,) + args + args = (operation_logger,) + args result = func(*args, **kwargs) except Exception as e: - uo.error(e) + operation_logger.error(e) raise else: - uo.success() + operation_logger.success() return result return func_wrapper return decorate -class UnitOperation(object): +class OperationLogger(object): """ Instances of this class represents unit operation done on the ynh instance. diff --git a/src/yunohost/service.py b/src/yunohost/service.py index ba9c4450e..66ae837a9 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -152,7 +152,7 @@ def service_stop(names): logger.debug(m18n.n('service_already_stopped', service=name)) @is_unit_operation() -def service_enable(uo, names): +def service_enable(operation_logger, names): """ Enable one or more services @@ -160,7 +160,7 @@ def service_enable(uo, names): names -- Services name to enable """ - uo.start() + operation_logger.start() if isinstance(names, str): names = [names] for name in names: @@ -346,7 +346,7 @@ def service_log(name, number=50): @is_unit_operation([('names', 'service')]) -def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False, +def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run=False, list_pending=False): """ Regenerate the configuration file(s) for a service @@ -380,12 +380,12 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False return pending_conf if not dry_run: - uo.related_to = [('service', x) for x in names] + operation_logger.related_to = [('service', x) for x in names] if not names: - uo.name_parameter_override = 'all' + operation_logger.name_parameter_override = 'all' elif len(names) != 1: - uo.name_parameter_override = str(len(uo.related_to))+'_services' - uo.start() + operation_logger.name_parameter_override = str(len(operation_logger.related_to))+'_services' + operation_logger.start() # Clean pending conf directory if os.path.isdir(PENDING_CONF_DIR): @@ -425,12 +425,12 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False # Set the processing method _regen = _process_regen_conf if not dry_run else lambda *a, **k: True - uo.related_to = [] + operation_logger.related_to = [] # Iterate over services and process pending conf for service, conf_files in _get_pending_conf(names).items(): if not dry_run: - uo.related_to.append(('service', service)) + operation_logger.related_to.append(('service', service)) logger.debug(m18n.n( 'service_regenconf_pending_applying' if not dry_run else @@ -580,7 +580,7 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False hook_callback('conf_regen', names, pre_callback=_pre_call) - uo.success() + operation_logger.success() return result diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index e0e041886..321a18d5b 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -52,7 +52,7 @@ from yunohost.service import service_status, service_regen_conf, service_log, se from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version from yunohost.utils.network import get_public_ip -from yunohost.log import is_unit_operation, UnitOperation +from yunohost.log import is_unit_operation, OperationLogger # FIXME this is a duplicate from apps.py APPS_SETTING_PATH = '/etc/yunohost/apps/' @@ -140,7 +140,7 @@ def tools_adminpw(auth, new_password): @is_unit_operation() -def tools_maindomain(uo, auth, new_domain=None): +def tools_maindomain(operation_logger, auth, new_domain=None): """ Check the current main domain, or change it @@ -157,8 +157,8 @@ def tools_maindomain(uo, auth, new_domain=None): if new_domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) - uo.related_to.append(('domain', new_domain)) - uo.start() + operation_logger.related_to.append(('domain', new_domain)) + operation_logger.start() # Apply changes to ssl certs ssl_key = "/etc/ssl/private/yunohost_key.pem" @@ -250,7 +250,7 @@ def _is_inside_container(): @is_unit_operation() -def tools_postinstall(uo, domain, password, ignore_dyndns=False): +def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False): """ YunoHost post-install @@ -299,7 +299,7 @@ def tools_postinstall(uo, domain, password, ignore_dyndns=False): else: dyndns = False - uo.start() + operation_logger.start() logger.info(m18n.n('yunohost_installing')) service_regen_conf(['nslcd', 'nsswitch'], force=True) @@ -476,7 +476,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): @is_unit_operation() -def tools_upgrade(uo, auth, ignore_apps=False, ignore_packages=False): +def tools_upgrade(operation_logger, auth, ignore_apps=False, ignore_packages=False): """ Update apps & package cache, then display changelog @@ -517,7 +517,7 @@ def tools_upgrade(uo, auth, ignore_apps=False, ignore_packages=False): if cache.get_changes(): logger.info(m18n.n('upgrading_packages')) - uo.start() + operation_logger.start() try: # Apply APT changes # TODO: Logs output for the API @@ -527,10 +527,10 @@ def tools_upgrade(uo, auth, ignore_apps=False, ignore_packages=False): failure = True logger.warning('unable to upgrade packages: %s' % str(e)) logger.error(m18n.n('packages_upgrade_failed')) - uo.error(m18n.n('packages_upgrade_failed')) + operation_logger.error(m18n.n('packages_upgrade_failed')) else: logger.info(m18n.n('done')) - uo.success() + operation_logger.success() else: logger.info(m18n.n('packages_no_upgrade')) @@ -716,7 +716,7 @@ def tools_port_available(port): @is_unit_operation() -def tools_shutdown(uo, force=False): +def tools_shutdown(operation_logger, force=False): shutdown = force if not shutdown: try: @@ -729,13 +729,13 @@ def tools_shutdown(uo, force=False): shutdown = True if shutdown: - uo.start() + operation_logger.start() logger.warn(m18n.n('server_shutdown')) subprocess.check_call(['systemctl', 'poweroff']) @is_unit_operation() -def tools_reboot(uo, force=False): +def tools_reboot(operation_logger, force=False): reboot = force if not reboot: try: @@ -747,7 +747,7 @@ def tools_reboot(uo, force=False): if i.lower() == 'y' or i.lower() == 'yes': reboot = True if reboot: - uo.start() + operation_logger.start() logger.warn(m18n.n('server_reboot')) subprocess.check_call(['systemctl', 'reboot']) @@ -870,8 +870,8 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai for migration in migrations: # Start register change on system - uo= UnitOperation('tools_migrations_migrate_' + mode) - uo.start() + operation_logger= OperationLogger('tools_migrations_migrate_' + mode) + operation_logger.start() if not skip: @@ -881,11 +881,11 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai try: if mode == "forward": m = migration["module"].MyMigration() - m.uo = uo + m.operation_logger = operation_logger m.migrate() elif mode == "backward": m = migration["module"].MyMigration() - m.uo = uo + m.operation_logger = operation_logger m.backward() else: # can't happen raise Exception("Illegal state for migration: '%s', should be either 'forward' or 'backward'" % mode) @@ -897,7 +897,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai number=migration.number, name=migration.name) logger.error(msg, exc_info=1) - uo.error(msg) + operation_logger.error(msg) break else: # if skip @@ -911,7 +911,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai "name": migration.name } - uo.success() + operation_logger.success() # special case where we want to go back from the start if target == 0: diff --git a/src/yunohost/user.py b/src/yunohost/user.py index c3fdf266c..48065f70a 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -99,7 +99,7 @@ def user_list(auth, fields=None): @is_unit_operation([('username', 'user')]) -def user_create(uo, auth, username, firstname, lastname, mail, password, +def user_create(operation_logger, auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ Create user @@ -134,7 +134,7 @@ def user_create(uo, auth, username, firstname, lastname, mail, password, m18n.n('mail_domain_unknown', domain=mail.split("@")[1])) - uo.start() + operation_logger.start() # Get random UID/GID all_uid = {x.pw_uid for x in pwd.getpwall()} @@ -222,7 +222,7 @@ def user_create(uo, auth, username, firstname, lastname, mail, password, @is_unit_operation([('username', 'user')]) -def user_delete(uo, auth, username, purge=False): +def user_delete(operation_logger, auth, username, purge=False): """ Delete user @@ -234,7 +234,7 @@ def user_delete(uo, auth, username, purge=False): from yunohost.app import app_ssowatconf from yunohost.hook import hook_callback - uo.start() + operation_logger.start() if auth.remove('uid=%s,ou=users' % username): # Invalidate passwd to take user deletion into account subprocess.call(['nscd', '-i', 'passwd']) @@ -259,7 +259,7 @@ def user_delete(uo, auth, username, purge=False): @is_unit_operation([('username', 'user')], exclude=['auth', 'change_password']) -def user_update(uo, auth, username, firstname=None, lastname=None, mail=None, +def user_update(operation_logger, auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): """ @@ -360,7 +360,7 @@ def user_update(uo, auth, username, firstname=None, lastname=None, mail=None, if mailbox_quota is not None: new_attr_dict['mailuserquota'] = mailbox_quota - uo.start() + operation_logger.start() if auth.update('uid=%s,ou=users' % username, new_attr_dict): logger.success(m18n.n('user_updated')) From dc19646504f3bc097bf2f56896934fe76a7c9b8e Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 23 Aug 2018 19:16:27 +0200 Subject: [PATCH 103/103] [fix] Remove old migration call --- src/yunohost/tools.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 321a18d5b..f9ee14994 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -879,14 +879,11 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai number=migration.number, name=migration.name)) try: + migration.operation_logger = operation_logger if mode == "forward": - m = migration["module"].MyMigration() - m.operation_logger = operation_logger - m.migrate() + migration.migrate() elif mode == "backward": - m = migration["module"].MyMigration() - m.operation_logger = operation_logger - m.backward() + migration.backward() else: # can't happen raise Exception("Illegal state for migration: '%s', should be either 'forward' or 'backward'" % mode) except Exception as e: