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):