Merge pull request #115 from YunoHost/logging

Make use of logging instead of msignals.display
This commit is contained in:
Jérôme Lebleu 2015-11-14 23:49:14 +01:00
commit 9389797ceb
9 changed files with 328 additions and 226 deletions

View file

@ -7,27 +7,28 @@ import os
# Either we are in a development environment or not
IN_DEVEL = False
# Either cache has to be used inside the moulinette or not
USE_CACHE = True
# Either the output has to be encoded as a JSON encoded string or not
PRINT_JSON = False
# Either the output has to printed for scripting usage or not
PRINT_PLAIN = False
# Level for which loggers will log
LOGGERS_LEVEL = 'INFO'
TTY_LOG_LEVEL = 'SUCCESS'
# Handlers that will be used by loggers
# - file: log to the file LOG_DIR/LOG_FILE
# - console: log to stderr
LOGGERS_HANDLERS = ['file']
# - tty: log to current tty
LOGGERS_HANDLERS = ['file', 'tty']
# Directory and file to be used by logging
LOG_DIR = '/var/log/yunohost'
LOG_FILE = 'yunohost-cli.log'
# Check and load - as needed - development environment
if not __file__.startswith('/usr/'):
IN_DEVEL = True
if IN_DEVEL:
basedir = os.path.abspath('%s/../' % os.path.dirname(__file__))
if os.path.isdir(os.path.join(basedir, 'moulinette')):
sys.path.insert(0, basedir)
LOG_DIR = os.path.join(basedir, 'log')
# Initialization & helpers functions -----------------------------------
@ -40,83 +41,114 @@ def _die(message, title='Error:'):
print('%s %s' % (colorize(title, 'red'), message))
sys.exit(1)
def _check_in_devel():
"""Check and load if needed development environment"""
global IN_DEVEL, LOG_DIR
basedir = os.path.abspath('%s/../' % os.path.dirname(__file__))
if os.path.isdir('%s/moulinette' % basedir) and not IN_DEVEL:
# Add base directory to python path
sys.path.insert(0, basedir)
def _parse_cli_args():
"""Parse additional arguments for the cli"""
import argparse
# Update global variables
IN_DEVEL = True
LOG_DIR = '%s/log' % basedir
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--no-cache',
action='store_false', default=True, dest='use_cache',
help="Don't use actions map cache",
)
parser.add_argument('--output-as',
choices=['json', 'plain'], default=None,
help="Output result in another format",
)
parser.add_argument('--debug',
action='store_true', default=False,
help="Log and print debug messages",
)
parser.add_argument('--verbose',
action='store_true', default=False,
help="Be more verbose in the output",
)
parser.add_argument('--quiet',
action='store_true', default=False,
help="Don't produce any output",
)
# deprecated arguments
parser.add_argument('--plain',
action='store_true', default=False, help=argparse.SUPPRESS
)
parser.add_argument('--json',
action='store_true', default=False, help=argparse.SUPPRESS
)
def _parse_argv():
"""Parse additional arguments and return remaining ones"""
argv = list(sys.argv)
argv.pop(0)
opts, args = parser.parse_known_args()
if '--no-cache' in argv:
global USE_CACHE
USE_CACHE = False
argv.remove('--no-cache')
if '--json' in argv:
global PRINT_JSON
PRINT_JSON = True
argv.remove('--json')
if '--plain' in argv:
global PRINT_PLAIN
PRINT_PLAIN = True
argv.remove('--plain')
if '--debug' in argv:
global LOGGERS_LEVEL
LOGGERS_LEVEL = 'DEBUG'
argv.remove('--debug')
if '--verbose' in argv:
global LOGGERS_HANDLERS
if 'console' not in LOGGERS_HANDLERS:
LOGGERS_HANDLERS.append('console')
argv.remove('--verbose')
return argv
# output compatibility
if opts.plain:
opts.output_as = 'plain'
elif opts.json:
opts.output_as = 'json'
def _init_moulinette():
return (parser, opts, args)
def _init_moulinette(debug=False, verbose=False, quiet=False):
"""Configure logging and initialize the moulinette"""
from moulinette import init
# Define loggers handlers
handlers = set(LOGGERS_HANDLERS)
if quiet and 'tty' in handlers:
handlers.remove('tty')
elif verbose and 'tty' not in handlers:
handlers.append('tty')
root_handlers = handlers - set(['tty'])
# Define loggers level
level = LOGGERS_LEVEL
tty_level = TTY_LOG_LEVEL
if verbose:
tty_level = 'INFO'
if debug:
tty_level = level = 'DEBUG'
# Custom logging configuration
logging = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'simple': {
'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s - %(message)s'
'tty-debug': {
'format': '%(relativeCreated)-4d %(fmessage)s'
},
'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(message)s'
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
},
'filters': {
'action': {
'()': 'moulinette.utils.log.ActionFilter',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'simple',
'stream': 'ext://sys.stderr',
'tty': {
'level': tty_level,
'class': 'moulinette.interfaces.cli.TTYHandler',
'formatter': 'tty-debug' if debug else '',
},
'file': {
'class': 'logging.FileHandler',
'formatter': 'precise',
'filename': '%s/%s' % (LOG_DIR, LOG_FILE),
'filters': ['action'],
},
},
'loggers': {
'moulinette': {
'level': LOGGERS_LEVEL,
'handlers': LOGGERS_HANDLERS,
},
'yunohost': {
'level': LOGGERS_LEVEL,
'handlers': LOGGERS_HANDLERS,
'level': level,
'handlers': handlers,
'propagate': False,
},
'moulinette': {
'level': level,
'handlers': [],
'propagate': True,
},
},
'root': {
'level': level,
'handlers': root_handlers,
},
}
@ -144,9 +176,8 @@ def _retrieve_namespaces():
# Main action ----------------------------------------------------------
if __name__ == '__main__':
_check_in_devel()
args = _parse_argv()
_init_moulinette()
parser, opts, args = _parse_cli_args()
_init_moulinette(opts.debug, opts.verbose, opts.quiet)
# Check that YunoHost is installed
if not os.path.isfile('/etc/yunohost/installed') and \
@ -163,6 +194,8 @@ if __name__ == '__main__':
# Execute the action
from moulinette import cli
ret = cli(_retrieve_namespaces(), args, use_cache=USE_CACHE,
print_json=PRINT_JSON, print_plain=PRINT_PLAIN)
ret = cli(_retrieve_namespaces(), args,
use_cache=opts.use_cache, output_as=opts.output_as,
parser_kwargs={'top_parser': parser}
)
sys.exit(ret)

View file

@ -7,24 +7,32 @@ import os.path
# Either we are in a development environment or not
IN_DEVEL = False
# Either cache has to be used inside the moulinette or not
USE_CACHE = True
# Either WebSocket has to be installed by the moulinette or not
USE_WEBSOCKET = True
# Default server configuration
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 6787
# Level for which loggers will log
LOGGERS_LEVEL = 'INFO'
# Handlers that will be used by loggers
# - file: log to the file LOG_DIR/LOG_FILE
# - api: serve logs through the api
# - console: log to stderr
LOGGERS_HANDLERS = ['file']
LOGGERS_HANDLERS = ['file', 'api']
# Directory and file to be used by logging
LOG_DIR = '/var/log/yunohost'
LOG_FILE = 'yunohost-api.log'
# Check and load - as needed - development environment
if not __file__.startswith('/usr/'):
IN_DEVEL = True
if IN_DEVEL:
basedir = os.path.abspath('%s/../' % os.path.dirname(__file__))
if os.path.isdir(os.path.join(basedir, 'moulinette')):
sys.path.insert(0, basedir)
LOG_DIR = os.path.join(basedir, 'log')
# Initialization & helpers functions -----------------------------------
@ -37,79 +45,112 @@ def _die(message, title='Error:'):
print('%s %s' % (colorize(title, 'red'), message))
sys.exit(1)
def _check_in_devel():
"""Check and load if needed development environment"""
global IN_DEVEL, LOG_DIR
basedir = os.path.abspath('%s/../' % os.path.dirname(__file__))
if os.path.isdir('%s/moulinette' % basedir) and not IN_DEVEL:
# Add base directory to python path
sys.path.insert(0, basedir)
def _parse_api_args():
"""Parse main arguments for the api"""
import argparse
# Update global variables
IN_DEVEL = True
LOG_DIR = '%s/log' % basedir
parser = argparse.ArgumentParser(add_help=False,
description="Run the YunoHost API to manage your server.",
)
srv_group = parser.add_argument_group('server configuration')
srv_group.add_argument('-h', '--host',
action='store', default=DEFAULT_HOST,
help="Host to listen on (default: %s)" % DEFAULT_HOST,
)
srv_group.add_argument('-p', '--port',
action='store', default=DEFAULT_PORT, type=int,
help="Port to listen on (default: %d)" % DEFAULT_PORT,
)
srv_group.add_argument('--no-websocket',
action='store_true', default=True, dest='use_websocket',
help="Serve without WebSocket support, used to handle "
"asynchronous responses such as the messages",
)
glob_group = parser.add_argument_group('global arguments')
glob_group.add_argument('--no-cache',
action='store_false', default=True, dest='use_cache',
help="Don't use actions map cache",
)
glob_group.add_argument('--debug',
action='store_true', default=False,
help="Set log level to DEBUG",
)
glob_group.add_argument('--verbose',
action='store_true', default=False,
help="Be verbose in the output",
)
glob_group.add_argument('--help',
action='help', help="Show this help message and exit",
)
def _parse_argv():
"""Parse additional arguments and return remaining ones"""
argv = list(sys.argv)
argv.pop(0)
return parser.parse_args()
if '--no-cache' in argv:
global USE_CACHE
USE_CACHE = False
argv.remove('--no-cache')
if '--no-websocket' in argv:
global USE_WEBSOCKET
USE_WEBSOCKET = False
argv.remove('--no-websocket')
if '--debug' in argv:
global LOGGERS_LEVEL
LOGGERS_LEVEL = 'DEBUG'
argv.remove('--debug')
if '--verbose' in argv:
global LOGGERS_HANDLERS
if 'console' not in LOGGERS_HANDLERS:
LOGGERS_HANDLERS.append('console')
argv.remove('--verbose')
return argv
def _init_moulinette():
def _init_moulinette(use_websocket=True, debug=False, verbose=False):
"""Configure logging and initialize the moulinette"""
from moulinette import init
# Define loggers handlers
handlers = set(LOGGERS_HANDLERS)
if not use_websocket and 'api' in handlers:
handlers.remove('api')
if verbose and 'console' not in handlers:
handlers.add('console')
root_handlers = handlers - set(['api'])
# Define loggers level
level = LOGGERS_LEVEL
if debug:
level = 'DEBUG'
# Custom logging configuration
logging = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'simple': {
'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s - %(message)s'
'console': {
'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(message)s'
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
},
'filters': {
'action': {
'()': 'moulinette.utils.log.ActionFilter',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'simple',
'stream': 'ext://sys.stderr',
'api': {
'class': 'moulinette.interfaces.api.APIQueueHandler',
},
'file': {
'class': 'logging.handlers.WatchedFileHandler',
'formatter': 'precise',
'filename': '%s/%s' % (LOG_DIR, LOG_FILE),
'filters': ['action'],
},
'console': {
'class': 'logging.StreamHandler',
'formatter': 'console',
'stream': 'ext://sys.stdout',
'filters': ['action'],
},
},
'loggers': {
'moulinette': {
'level': LOGGERS_LEVEL,
'handlers': LOGGERS_HANDLERS,
},
'yunohost': {
'level': LOGGERS_LEVEL,
'handlers': LOGGERS_HANDLERS,
'level': level,
'handlers': handlers,
'propagate': False,
},
'moulinette': {
'level': level,
'handlers': [],
'propagate': True,
},
},
'root': {
'level': level,
'handlers': root_handlers,
},
}
@ -150,20 +191,18 @@ def is_installed():
# Main action ----------------------------------------------------------
if __name__ == '__main__':
_check_in_devel()
_parse_argv()
_init_moulinette()
opts = _parse_api_args()
_init_moulinette(opts.use_websocket, opts.debug, opts.verbose)
from moulinette import (api, MoulinetteError)
# Run the server
from moulinette import api, MoulinetteError
from yunohost import get_versions
try:
# Run the server
api(_retrieve_namespaces(), port=6787,
ret = api(_retrieve_namespaces(),
host=opts.host, port=opts.port,
routes={
('GET', '/installed'): is_installed,
('GET', '/version'): get_versions,
},
use_cache=USE_CACHE, use_websocket=USE_WEBSOCKET)
except MoulinetteError as e:
_die(e.strerror, m18n.g('error'))
sys.exit(0)
use_cache=opts.use_cache, use_websocket=opts.use_websocket
)
sys.exit(ret)

View file

@ -1355,11 +1355,15 @@ hook:
action_help: Execute hook from a file with arguments
api: GET /hook
arguments:
file:
help: Script to execute
path:
help: Path of the script to execute
-a:
full: --args
help: Arguments to pass to the script
--raise-on-error:
help: Raise if the script returns a non-zero exit code
action: store_true
-q:
full: --no-trace
help: Do not print each command that will be executed
action: store_true

View file

@ -0,0 +1,12 @@
#
# Bash completion for yunohost
#
_python_argcomplete() {
local IFS=' '
COMPREPLY=( $(IFS="$IFS" COMP_LINE="$COMP_LINE" COMP_POINT="$COMP_POINT" _ARGCOMPLETE_COMP_WORDBREAKS="$COMP_WORDBREAKS" _ARGCOMPLETE=1 "$1" 8>&1 9>&2 1>/dev/null 2>/dev/null) )
if [[ $? != 0 ]]; then
unset COMPREPLY
fi
}
complete -o nospace -o default -F _python_argcomplete "yunohost"

1
debian/install vendored
View file

@ -1,4 +1,5 @@
bin/* /usr/bin/
data/bash-completion.d/yunohost /etc/bash_completion.d/
data/actionsmap/* /usr/share/moulinette/actionsmap/
data/hooks/* /usr/share/yunohost/hooks/
data/other/* /usr/share/yunohost/yunohost-config/moulinette/

View file

@ -37,7 +37,8 @@
"mysql_db_initialized" : "MySQL database successfully initialized",
"extracting" : "Extracting...",
"downloading" : "Downloading...",
"executing_script": "Executing script...",
"executing_script": "Executing script '{script:s}'...",
"executing_command": "Executing command '{command:s}'...",
"done" : "Done.",
"path_removal_failed" : "Unable to remove path {:s}",
@ -87,6 +88,7 @@
"hook_choice_invalid" : "Invalid choice '{:s}'",
"hook_argument_missing" : "Missing argument '{:s}'",
"hook_exec_failed" : "Script execution failed",
"hook_exec_not_terminated" : "Script execution hasnt terminated",
"mountpoint_unknown" : "Unknown mountpoint",
"unit_unknown" : "Unknown unit '{:s}'",
@ -118,7 +120,7 @@
"service_no_log" : "No log to display for service '{:s}'",
"service_cmd_exec_failed" : "Unable to execute command '{:s}'",
"services_configured": "Configuration successfully generated",
"service_configuration_conflict": "The file {file:s} has been changed since its last generation. Please apply the modifications manually or use the option --force (it will erase all the modifications previously done to the file)",
"service_configuration_conflict": "The file {file:s} has been changed since its last generation. Please apply the modifications manually or use the option --force (it will erase all the modifications previously done to the file). Here are the differences:\n{diff:s}",
"no_such_conf_file": "Unable to copy the file {file:s}: the file does not exist",
"service_add_configuration": "Adding the configuration file {file:s}",

View file

@ -83,8 +83,7 @@ def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False,
firewall[i][p].append(port)
else:
ipv = "IPv%s" % i[3]
msignals.display(m18n.n('port_already_opened', port, ipv),
'warning')
logger.warning(m18n.n('port_already_opened', port, ipv))
# Add port forwarding with UPnP
if not no_upnp and port not in firewall['uPnP'][p]:
firewall['uPnP'][p].append(port)
@ -141,8 +140,7 @@ def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False,
firewall[i][p].remove(port)
else:
ipv = "IPv%s" % i[3]
msignals.display(m18n.n('port_already_closed', port, ipv),
'warning')
logger.warning(m18n.n('port_already_closed', port, ipv))
# Remove port forwarding with UPnP
if upnp and port in firewall['uPnP'][p]:
firewall['uPnP'][p].remove(port)
@ -214,9 +212,9 @@ def firewall_reload(skip_upnp=False):
try:
process.check_output("iptables -L")
except process.CalledProcessError as e:
logger.info('iptables seems to be not available, it outputs:\n%s',
prependlines(e.output.rstrip(), '> '))
msignals.display(m18n.n('iptables_unavailable'), 'info')
logger.debug('iptables seems to be not available, it outputs:\n%s',
prependlines(e.output.rstrip(), '> '))
logger.warning(m18n.n('iptables_unavailable'))
else:
rules = [
"iptables -F",
@ -243,9 +241,9 @@ def firewall_reload(skip_upnp=False):
try:
process.check_output("ip6tables -L")
except process.CalledProcessError as e:
logger.info('ip6tables seems to be not available, it outputs:\n%s',
prependlines(e.output.rstrip(), '> '))
msignals.display(m18n.n('ip6tables_unavailable'), 'info')
logger.debug('ip6tables seems to be not available, it outputs:\n%s',
prependlines(e.output.rstrip(), '> '))
logger.warning(m18n.n('ip6tables_unavailable'))
else:
rules = [
"ip6tables -F",
@ -282,9 +280,9 @@ def firewall_reload(skip_upnp=False):
os.system("service fail2ban restart")
if errors:
msignals.display(m18n.n('firewall_rules_cmd_failed'), 'warning')
logger.warning(m18n.n('firewall_rules_cmd_failed'))
else:
msignals.display(m18n.n('firewall_reloaded'), 'success')
logger.success(m18n.n('firewall_reloaded'))
return firewall_list()
@ -306,7 +304,7 @@ def firewall_upnp(action='status', no_refresh=False):
# Compatibility with previous version
if action == 'reload':
logger.warning("'reload' action is deprecated and will be removed")
logger.info("'reload' action is deprecated and will be removed")
try:
# Remove old cron job
os.remove('/etc/cron.d/yunohost-firewall')
@ -349,14 +347,14 @@ def firewall_upnp(action='status', no_refresh=False):
nb_dev = upnpc.discover()
logger.debug('found %d UPnP device(s)', int(nb_dev))
if nb_dev < 1:
msignals.display(m18n.n('upnp_dev_not_found'), 'error')
logger.error(m18n.n('upnp_dev_not_found'))
enabled = False
else:
try:
# Select UPnP device
upnpc.selectigd()
except:
logger.exception('unable to select UPnP device')
logger.info('unable to select UPnP device', exc_info=1)
enabled = False
else:
# Iterate over ports
@ -374,8 +372,8 @@ def firewall_upnp(action='status', no_refresh=False):
upnpc.addportmapping(port, protocol, upnpc.lanaddr,
port, 'yunohost firewall: port %d' % port, '')
except:
logger.exception('unable to add port %d using UPnP',
port)
logger.info('unable to add port %d using UPnP',
port, exc_info=1)
enabled = False
if enabled != firewall['uPnP']['enabled']:
@ -390,9 +388,9 @@ def firewall_upnp(action='status', no_refresh=False):
if not no_refresh:
# Display success message if needed
if action == 'enable' and enabled:
msignals.display(m18n.n('upnp_enabled'), 'success')
logger.success(m18n.n('upnp_enabled'))
elif action == 'disable' and not enabled:
msignals.display(m18n.n('upnp_disabled'), 'success')
logger.success(m18n.n('upnp_disabled'))
# Make sure to disable UPnP
elif action != 'disable' and not enabled:
firewall_upnp('disable', no_refresh=True)
@ -455,6 +453,6 @@ def _update_firewall_file(rules):
def _on_rule_command_error(returncode, cmd, output):
"""Callback for rules commands error"""
# Log error and continue commands execution
logger.error('"%s" returned non-zero exit status %d:\n%s',
cmd, returncode, prependlines(output.rstrip(), '> '))
logger.info('"%s" returned non-zero exit status %d:\n%s',
cmd, returncode, prependlines(output.rstrip(), '> '))
return True

View file

@ -32,12 +32,12 @@ import subprocess
from glob import iglob
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
from moulinette.utils import log
hook_folder = '/usr/share/yunohost/hooks/'
custom_hook_folder = '/etc/yunohost/hooks.d/'
logger = getActionLogger('yunohost.hook')
logger = log.getActionLogger('yunohost.hook')
def hook_add(app, file):
@ -264,14 +264,9 @@ def hook_callback(action, hooks=[], args=None):
state = 'succeed'
filename = '%s-%s' % (priority, name)
try:
ret = hook_exec(info['path'], args=args)
except:
logger.exception("error while executing hook '%s'",
info['path'])
state = 'failed'
if ret != 0:
logger.error("error while executing hook '%s', retcode: %d",
info['path'], ret)
hook_exec(info['path'], args=args, raise_on_error=True)
except MoulinetteError as e:
logger.error(e.strerror)
state = 'failed'
try:
result[state][name].append(info['path'])
@ -301,23 +296,31 @@ def hook_check(file):
return {}
def hook_exec(file, args=None, raise_on_error=False):
def hook_exec(path, args=None, raise_on_error=False, no_trace=False):
"""
Execute hook from a file with arguments
Keyword argument:
file -- Script to execute
path -- Path of the script to execute
args -- Arguments to pass to the script
raise_on_error -- Raise if the script returns a non-zero exit code
no_trace -- Do not print each command that will be executed
"""
from moulinette.utils.stream import NonBlockingStreamReader
import time
from moulinette.utils.stream import start_async_file_reading
from yunohost.app import _value_for_locale
# Validate hook path
if path[0] != '/':
path = os.path.realpath(path)
if not os.path.isfile(path):
raise MoulinetteError(errno.EIO, m18n.g('file_not_exist'))
if isinstance(args, list):
arg_list = args
else:
required_args = hook_check(file)
required_args = hook_check(path)
if args is None:
args = {}
@ -351,42 +354,60 @@ def hook_exec(file, args=None, raise_on_error=False):
raise MoulinetteError(errno.EINVAL,
m18n.n('hook_argument_missing', arg['name']))
file_path = "./"
if "/" in file and file[0:2] != file_path:
file_path = os.path.dirname(file)
file = file.replace(file_path +"/", "")
# Construct command variables
cmd_fdir, cmd_fname = os.path.split(path)
cmd_fname = './{0}'.format(cmd_fname)
#TODO: Allow python script
arg_str = ''
cmd_args = ''
if arg_list:
# Concatenate arguments and escape them with double quotes to prevent
# bash related issue if an argument is empty and is not the last
arg_str = '"{:s}"'.format('" "'.join(str(s) for s in arg_list))
cmd_args = '"{:s}"'.format('" "'.join(str(s) for s in arg_list))
msignals.display(m18n.n('executing_script'))
# Construct command to execute
command = ['sudo', '-u', 'admin', '-H', 'sh', '-c']
if no_trace:
cmd = 'cd "{0:s}" && /bin/bash "{1:s}" {2:s}'
else:
# use xtrace on fd 7 which is redirected to stdout
cmd = 'cd "{0:s}" && BASH_XTRACEFD=7 /bin/bash -x "{1:s}" {2:s} 7>&1'
command.append(cmd.format(cmd_fdir, cmd_fname, cmd_args))
p = subprocess.Popen(
['sudo', '-u', 'admin', '-H', 'sh', '-c', 'cd "{:s}" && ' \
'/bin/bash -x "{:s}" {:s}'.format(
file_path, file, arg_str)],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
if logger.isEnabledFor(log.DEBUG):
logger.info(m18n.n('executing_command', command=' '.join(command)))
else:
logger.info(m18n.n('executing_script', script='{0}/{1}'.format(
cmd_fdir, cmd_fname)))
process = subprocess.Popen(command,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=False)
# Wrap and get process ouput
stream = NonBlockingStreamReader(p.stdout)
while True:
line = stream.readline(True, 0.1)
if not line:
# Check if process has terminated
returncode = p.poll()
if returncode is not None:
break
else:
msignals.display(line.rstrip(), 'log')
stream.close()
# Wrap and get process outputs
stdout_reader, stdout_queue = start_async_file_reading(process.stdout)
stderr_reader, stderr_queue = start_async_file_reading(process.stderr)
while not stdout_reader.eof() or not stderr_reader.eof():
while not stdout_queue.empty():
line = stdout_queue.get()
logger.info(line.rstrip())
while not stderr_queue.empty():
line = stderr_queue.get()
logger.warning(line.rstrip())
time.sleep(.1)
if raise_on_error and returncode != 0:
# Terminate outputs readers
stdout_reader.join()
stderr_reader.join()
# Get and return process' return code
returncode = process.poll()
if returncode is None:
if raise_on_error:
raise MoulinetteError(m18n.n('hook_exec_not_terminated'))
else:
logger.error(m18n.n('hook_exec_not_terminated'))
return 1
elif raise_on_error and returncode != 0:
raise MoulinetteError(m18n.n('hook_exec_failed'))
return returncode

View file

@ -34,6 +34,7 @@ import difflib
import hashlib
from moulinette.core import MoulinetteError
from moulinette.utils import log
template_dir = os.getenv(
'YUNOHOST_TEMPLATE_DIR',
@ -44,6 +45,9 @@ conf_backup_dir = os.getenv(
'/home/yunohost.backup/conffiles'
)
logger = log.getActionLogger('yunohost.service')
def service_add(name, status=None, log=None, runlevel=None):
"""
Add a custom service
@ -73,7 +77,7 @@ def service_add(name, status=None, log=None, runlevel=None):
except:
raise MoulinetteError(errno.EIO, m18n.n('service_add_failed', name))
msignals.display(m18n.n('service_added'), 'success')
logger.success(m18n.n('service_added'))
def service_remove(name):
@ -96,7 +100,7 @@ def service_remove(name):
except:
raise MoulinetteError(errno.EIO, m18n.n('service_remove_failed', name))
msignals.display(m18n.n('service_removed'), 'success')
logger.success(m18n.n('service_removed'))
def service_start(names):
@ -111,12 +115,12 @@ def service_start(names):
names = [names]
for name in names:
if _run_service_command('start', name):
msignals.display(m18n.n('service_started', name), 'success')
logger.success(m18n.n('service_started', name))
else:
if service_status(name)['status'] != 'running':
raise MoulinetteError(errno.EPERM,
m18n.n('service_start_failed', name))
msignals.display(m18n.n('service_already_started', name))
logger.info(m18n.n('service_already_started', name))
def service_stop(names):
@ -131,12 +135,12 @@ def service_stop(names):
names = [names]
for name in names:
if _run_service_command('stop', name):
msignals.display(m18n.n('service_stopped', name), 'success')
logger.success(m18n.n('service_stopped', name))
else:
if service_status(name)['status'] != 'inactive':
raise MoulinetteError(errno.EPERM,
m18n.n('service_stop_failed', name))
msignals.display(m18n.n('service_already_stopped', name))
logger.info(m18n.n('service_already_stopped', name))
def service_enable(names):
@ -151,7 +155,7 @@ def service_enable(names):
names = [names]
for name in names:
if _run_service_command('enable', name):
msignals.display(m18n.n('service_enabled', name), 'success')
logger.success(m18n.n('service_enabled', name))
else:
raise MoulinetteError(errno.EPERM,
m18n.n('service_enable_failed', name))
@ -169,7 +173,7 @@ def service_disable(names):
names = [names]
for name in names:
if _run_service_command('disable', name):
msignals.display(m18n.n('service_disabled', name), 'success')
logger.success(m18n.n('service_disabled', name))
else:
raise MoulinetteError(errno.EPERM,
m18n.n('service_disable_failed', name))
@ -217,8 +221,7 @@ def service_status(names=[]):
shell=True)
except subprocess.CalledProcessError as e:
if 'usage:' in e.output.lower():
msignals.display(m18n.n('service_status_failed', name),
'warning')
logger.warning(m18n.n('service_status_failed', name))
else:
result[name]['status'] = 'inactive'
else:
@ -288,7 +291,7 @@ def service_regenconf(service=None, force=False):
hook_callback('conf_regen', [service], args=[force])
else:
hook_callback('conf_regen', args=[force])
msignals.display(m18n.n('services_configured'), 'success')
logger.success(m18n.n('services_configured'))
def _run_service_command(action, service):
@ -317,8 +320,7 @@ def _run_service_command(action, service):
ret = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
# TODO: Log output?
msignals.display(m18n.n('service_cmd_exec_failed', ' '.join(e.cmd)),
'warning')
logger.warning(m18n.n('service_cmd_exec_failed', ' '.join(e.cmd)))
return False
return True
@ -467,12 +469,9 @@ def service_saferemove(service, conf_file, force=False):
else:
services[service]['conffiles'][conf_file] = previous_hash
os.remove(conf_backup_file)
if os.isatty(1) and \
(len(previous_hash) == 32 or previous_hash[-32:] != current_hash):
msignals.display(
m18n.n('service_configuration_conflict', file=conf_file),
'warning'
)
if len(previous_hash) == 32 or previous_hash[-32:] != current_hash:
logger.warning(m18n.n('service_configuration_conflict',
file=conf_file))
_save_services(services)
@ -509,8 +508,7 @@ def service_safecopy(service, new_conf_file, conf_file, force=False):
)
process.wait()
else:
msignals.display(m18n.n('service_add_configuration', file=conf_file),
'info')
logger.info(m18n.n('service_add_configuration', file=conf_file))
# Add the service if it does not exist
if service not in services.keys():
@ -539,15 +537,9 @@ def service_safecopy(service, new_conf_file, conf_file, force=False):
else:
new_hash = previous_hash
if (len(previous_hash) == 32 or previous_hash[-32:] != current_hash):
msignals.display(
m18n.n('service_configuration_conflict', file=conf_file),
'warning'
)
print('\n' + conf_file)
for line in diff:
print(line.strip())
print('')
logger.warning(m18n.n('service_configuration_conflict',
file=conf_file, diff=''.join(diff)))
# Remove the backup file if the configuration has not changed
if new_hash == previous_hash:
try: