File: //proc/self/root/usr/lib/python2.7/site-packages/salt/modules/runit.py
# -*- coding: utf-8 -*-
'''
runit service module
(http://smarden.org/runit)
This module is compatible with the :mod:`service <salt.states.service>` states,
so it can be used to maintain services using the ``provider`` argument:
.. code-block:: yaml
myservice:
service:
- running
- provider: runit
Provides virtual `service` module on systems using runit as init.
Service management rules (`sv` command):
service $n is ENABLED if file SERVICE_DIR/$n/run exists
service $n is AVAILABLE if ENABLED or if file AVAIL_SVR_DIR/$n/run exists
service $n is DISABLED if AVAILABLE but not ENABLED
SERVICE_DIR/$n is normally a symlink to a AVAIL_SVR_DIR/$n folder
Service auto-start/stop mechanism:
`sv` (auto)starts/stops service as soon as SERVICE_DIR/<service> is
created/deleted, both on service creation or a boot time.
autostart feature is disabled if file SERVICE_DIR/<n>/down exists. This
does not affect the current's service status (if already running) nor
manual service management.
Service's alias:
Service `sva` is an alias of service `svc` when `AVAIL_SVR_DIR/sva` symlinks
to folder `AVAIL_SVR_DIR/svc`. `svc` can't be enabled if it is already
enabled through an alias already enabled, since `sv` files are stored in
folder `SERVICE_DIR/svc/`.
XBPS package management uses a service's alias to provides service
alternative(s), such as chrony and openntpd both aliased to ntpd.
'''
from __future__ import absolute_import, unicode_literals, print_function
# Import python libs
import os
import glob
import logging
import time
log = logging.getLogger(__name__)
# Import salt libs
from salt.exceptions import CommandExecutionError
import salt.utils.files
import salt.utils.path
# Function alias to not shadow built-ins.
__func_alias__ = {
'reload_': 'reload'
}
# which dir sv works with
VALID_SERVICE_DIRS = [
'/service',
'/var/service',
'/etc/service',
]
SERVICE_DIR = None
for service_dir in VALID_SERVICE_DIRS:
if os.path.exists(service_dir):
SERVICE_DIR = service_dir
break
# available service directory(ies)
AVAIL_SVR_DIRS = []
# Define the module's virtual name
__virtualname__ = 'runit'
__virtual_aliases__ = ('runit',)
def __virtual__():
'''
Virtual service only on systems using runit as init process (PID 1).
Otherwise, use this module with the provider mechanism.
'''
if __grains__.get('init') == 'runit':
if __grains__['os'] == 'Void':
add_svc_avail_path('/etc/sv')
global __virtualname__
__virtualname__ = 'service'
return __virtualname__
if salt.utils.path.which('sv'):
return __virtualname__
return (False, 'Runit not available. Please install sv')
def _service_path(name):
'''
Return SERVICE_DIR+name if possible
name
the service's name to work on
'''
if not SERVICE_DIR:
raise CommandExecutionError('Could not find service directory.')
return os.path.join(SERVICE_DIR, name)
#-- states.service compatible args
def start(name):
'''
Start service
name
the service's name
CLI Example:
.. code-block:: bash
salt '*' runit.start <service name>
'''
cmd = 'sv start {0}'.format(_service_path(name))
return not __salt__['cmd.retcode'](cmd)
#-- states.service compatible args
def stop(name):
'''
Stop service
name
the service's name
CLI Example:
.. code-block:: bash
salt '*' runit.stop <service name>
'''
cmd = 'sv stop {0}'.format(_service_path(name))
return not __salt__['cmd.retcode'](cmd)
#-- states.service compatible
def reload_(name):
'''
Reload service
name
the service's name
CLI Example:
.. code-block:: bash
salt '*' runit.reload <service name>
'''
cmd = 'sv reload {0}'.format(_service_path(name))
return not __salt__['cmd.retcode'](cmd)
#-- states.service compatible
def restart(name):
'''
Restart service
name
the service's name
CLI Example:
.. code-block:: bash
salt '*' runit.restart <service name>
'''
cmd = 'sv restart {0}'.format(_service_path(name))
return not __salt__['cmd.retcode'](cmd)
#-- states.service compatible
def full_restart(name):
'''
Calls runit.restart()
name
the service's name
CLI Example:
.. code-block:: bash
salt '*' runit.full_restart <service name>
'''
restart(name)
#-- states.service compatible
def status(name, sig=None):
'''
Return ``True`` if service is running
name
the service's name
sig
signature to identify with ps
CLI Example:
.. code-block:: bash
salt '*' runit.status <service name>
'''
if sig:
# usual way to do by others (debian_service, netbsdservice).
# XXX probably does not work here (check 'runsv sshd' instead of 'sshd' ?)
return bool(__salt__['status.pid'](sig))
svc_path = _service_path(name)
if not os.path.exists(svc_path):
# service does not exist
return False
# sv return code is not relevant to get a service status.
# Check its output instead.
cmd = 'sv status {0}'.format(svc_path)
try:
out = __salt__['cmd.run_stdout'](cmd)
return out.startswith('run: ')
except Exception: # pylint: disable=broad-except
# sv (as a command) returned an error
return False
def _is_svc(svc_path):
'''
Return ``True`` if directory <svc_path> is really a service:
file <svc_path>/run exists and is executable
svc_path
the (absolute) directory to check for compatibility
'''
run_file = os.path.join(svc_path, 'run')
if (os.path.exists(svc_path)
and os.path.exists(run_file)
and os.access(run_file, os.X_OK)):
return True
return False
def status_autostart(name):
'''
Return ``True`` if service <name> is autostarted by sv
(file $service_folder/down does not exist)
NB: return ``False`` if the service is not enabled.
name
the service's name
CLI Example:
.. code-block:: bash
salt '*' runit.status_autostart <service name>
'''
return not os.path.exists(os.path.join(_service_path(name), 'down'))
def get_svc_broken_path(name='*'):
'''
Return list of broken path(s) in SERVICE_DIR that match ``name``
A path is broken if it is a broken symlink or can not be a runit service
name
a glob for service name. default is '*'
CLI Example:
.. code-block:: bash
salt '*' runit.get_svc_broken_path <service name>
'''
if not SERVICE_DIR:
raise CommandExecutionError('Could not find service directory.')
ret = set()
for el in glob.glob(os.path.join(SERVICE_DIR, name)):
if not _is_svc(el):
ret.add(el)
return sorted(ret)
def get_svc_avail_path():
'''
Return list of paths that may contain available services
'''
return AVAIL_SVR_DIRS
def add_svc_avail_path(path):
'''
Add a path that may contain available services.
Return ``True`` if added (or already present), ``False`` on error.
path
directory to add to AVAIL_SVR_DIRS
'''
if os.path.exists(path):
if path not in AVAIL_SVR_DIRS:
AVAIL_SVR_DIRS.append(path)
return True
return False
def _get_svc_path(name='*', status=None):
'''
Return a list of paths to services with ``name`` that have the specified ``status``
name
a glob for service name. default is '*'
status
None : all services (no filter, default choice)
'DISABLED' : available service(s) that is not enabled
'ENABLED' : enabled service (whether started on boot or not)
'''
# This is the core routine to work with services, called by many
# other functions of this module.
#
# The name of a service is the "apparent" folder's name that contains its
# "run" script. If its "folder" is a symlink, the service is an "alias" of
# the targeted service.
if not SERVICE_DIR:
raise CommandExecutionError('Could not find service directory.')
# path list of enabled services as /AVAIL_SVR_DIRS/$service,
# taking care of any service aliases (do not use os.path.realpath()).
ena = set()
for el in glob.glob(os.path.join(SERVICE_DIR, name)):
if _is_svc(el):
ena.add(os.readlink(el))
log.trace('found enabled service path: %s', el)
if status == 'ENABLED':
return sorted(ena)
# path list of available services as /AVAIL_SVR_DIRS/$service
ava = set()
for d in AVAIL_SVR_DIRS:
for el in glob.glob(os.path.join(d, name)):
if _is_svc(el):
ava.add(el)
log.trace('found available service path: %s', el)
if status == 'DISABLED':
# service available but not enabled
ret = ava.difference(ena)
else:
# default: return available services
ret = ava.union(ena)
return sorted(ret)
def _get_svc_list(name='*', status=None):
'''
Return list of services that have the specified service ``status``
name
a glob for service name. default is '*'
status
None : all services (no filter, default choice)
'DISABLED' : available service that is not enabled
'ENABLED' : enabled service (whether started on boot or not)
'''
return sorted([os.path.basename(el) for el in _get_svc_path(name, status)])
def get_svc_alias():
'''
Returns the list of service's name that are aliased and their alias path(s)
'''
ret = {}
for d in AVAIL_SVR_DIRS:
for el in glob.glob(os.path.join(d, '*')):
if not os.path.islink(el):
continue
psvc = os.readlink(el)
if not os.path.isabs(psvc):
psvc = os.path.join(d, psvc)
nsvc = os.path.basename(psvc)
if nsvc not in ret:
ret[nsvc] = []
ret[nsvc].append(el)
return ret
def available(name):
'''
Returns ``True`` if the specified service is available, otherwise returns
``False``.
name
the service's name
CLI Example:
.. code-block:: bash
salt '*' runit.available <service name>
'''
return name in _get_svc_list(name)
def missing(name):
'''
The inverse of runit.available.
Returns ``True`` if the specified service is not available, otherwise returns
``False``.
name
the service's name
CLI Example:
.. code-block:: bash
salt '*' runit.missing <service name>
'''
return name not in _get_svc_list(name)
def get_all():
'''
Return a list of all available services
CLI Example:
.. code-block:: bash
salt '*' runit.get_all
'''
return _get_svc_list()
def get_enabled():
'''
Return a list of all enabled services
CLI Example:
.. code-block:: bash
salt '*' service.get_enabled
'''
return _get_svc_list(status='ENABLED')
def get_disabled():
'''
Return a list of all disabled services
CLI Example:
.. code-block:: bash
salt '*' service.get_disabled
'''
return _get_svc_list(status='DISABLED')
def enabled(name):
'''
Return ``True`` if the named service is enabled, ``False`` otherwise
name
the service's name
CLI Example:
.. code-block:: bash
salt '*' service.enabled <service name>
'''
# exhaustive check instead of (only) os.path.exists(_service_path(name))
return name in _get_svc_list(name, 'ENABLED')
def disabled(name):
'''
Return ``True`` if the named service is disabled, ``False`` otherwise
name
the service's name
CLI Example:
.. code-block:: bash
salt '*' service.disabled <service name>
'''
# return True for a non-existent service
return name not in _get_svc_list(name, 'ENABLED')
def show(name):
'''
Show properties of one or more units/jobs or the manager
name
the service's name
CLI Example:
salt '*' service.show <service name>
'''
ret = {}
ret['enabled'] = False
ret['disabled'] = True
ret['running'] = False
ret['service_path'] = None
ret['autostart'] = False
ret['command_path'] = None
ret['available'] = available(name)
if not ret['available']:
return ret
ret['enabled'] = enabled(name)
ret['disabled'] = not ret['enabled']
ret['running'] = status(name)
ret['autostart'] = status_autostart(name)
ret['service_path'] = _get_svc_path(name)[0]
if ret['service_path']:
ret['command_path'] = os.path.join(ret['service_path'], 'run')
# XXX provide info about alias ?
return ret
def enable(name, start=False, **kwargs):
'''
Start service ``name`` at boot.
Returns ``True`` if operation is successful
name
the service's name
start : False
If ``True``, start the service once enabled.
CLI Example:
.. code-block:: bash
salt '*' service.enable <name> [start=True]
'''
# non-existent service
if not available(name):
return False
# if service is aliased, refuse to enable it
alias = get_svc_alias()
if name in alias:
log.error('This service is aliased, enable its alias instead')
return False
# down_file: file that disables sv autostart
svc_realpath = _get_svc_path(name)[0]
down_file = os.path.join(svc_realpath, 'down')
# if service already enabled, remove down_file to
# let service starts on boot (as requested)
if enabled(name):
if os.path.exists(down_file):
try:
os.unlink(down_file)
except OSError:
log.error('Unable to remove file %s', down_file)
return False
return True
# let's enable the service
if not start:
# create a temp 'down' file BEFORE enabling service.
# will prevent sv from starting this service automatically.
log.trace('need a temporary file %s', down_file)
if not os.path.exists(down_file):
try:
salt.utils.files.fopen(down_file, "w").close() # pylint: disable=resource-leakage
except IOError:
log.error('Unable to create file {0}'.format(down_file))
return False
# enable the service
try:
os.symlink(svc_realpath, _service_path(name))
except IOError:
# (attempt to) remove temp down_file anyway
log.error('Unable to create symlink {0}'.format(down_file))
if not start:
os.unlink(down_file)
return False
# ensure sv is aware of this new service before continuing.
# if not, down_file might be removed too quickly,
# before 'sv' have time to take care about it.
# Documentation indicates that a change is handled within 5 seconds.
cmd = 'sv status {0}'.format(_service_path(name))
retcode_sv = 1
count_sv = 0
while retcode_sv != 0 and count_sv < 10:
time.sleep(0.5)
count_sv += 1
call = __salt__['cmd.run_all'](cmd)
retcode_sv = call['retcode']
# remove the temp down_file in any case.
if (not start) and os.path.exists(down_file):
try:
os.unlink(down_file)
except OSError:
log.error('Unable to remove temp file %s', down_file)
retcode_sv = 1
# if an error happened, revert our changes
if retcode_sv != 0:
os.unlink(os.path.join([_service_path(name), name]))
return False
return True
def disable(name, stop=False, **kwargs):
'''
Don't start service ``name`` at boot
Returns ``True`` if operation is successful
name
the service's name
stop
if True, also stops the service
CLI Example:
.. code-block:: bash
salt '*' service.disable <name> [stop=True]
'''
# non-existent as registrered service
if not enabled(name):
return False
# down_file: file that prevent sv autostart
svc_realpath = _get_svc_path(name)[0]
down_file = os.path.join(svc_realpath, 'down')
if stop:
stop(name)
if not os.path.exists(down_file):
try:
salt.utils.files.fopen(down_file, "w").close() # pylint: disable=resource-leakage
except IOError:
log.error('Unable to create file %s', down_file)
return False
return True
def remove(name):
'''
Remove the service <name> from system.
Returns ``True`` if operation is successful.
The service will be also stopped.
name
the service's name
CLI Example:
.. code-block:: bash
salt '*' service.remove <name>
'''
if not enabled(name):
return False
svc_path = _service_path(name)
if not os.path.islink(svc_path):
log.error('%s is not a symlink: not removed', svc_path)
return False
if not stop(name):
log.error('Failed to stop service %s', name)
return False
try:
os.remove(svc_path)
except IOError:
log.error('Unable to remove symlink %s', svc_path)
return False
return True
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4