"""
Service management for Alpine Linux via OpenRC.
This module registers under Salt's ``service`` virtual name and is loaded
automatically on Alpine minions. Existing states such as ``service.running``
and ``service.dead`` work without any changes to your state files.
.. important::
If Salt is not using this module on an Alpine minion (or you see an error
such as *'service.start' is not available*), see :ref:`here
<module-provider-override>`.
Runlevels
---------
OpenRC organises services into named runlevels rather than systemd targets.
The two runlevels relevant to most service management work are:
``default``
Services that should start when the system reaches a normal running state.
This is the runlevel used by :py:func:`enable` and :py:func:`disable` when
no ``runlevel`` argument is supplied.
``boot``
Services that must start earlier in the boot sequence, such as
``networking``, ``syslog``, and ``hostname``. To manage a service in this
runlevel, pass ``runlevel=boot`` explicitly.
Other runlevels (``sysinit``, ``shutdown``, ``nonetwork``) exist but are
rarely targeted directly by Salt states.
"""
import logging
from salt.exceptions import CommandExecutionError
log = logging.getLogger(__name__)
__virtualname__ = "service"
# The runlevel used by default for enable/disable operations.
# Alpine systems use 'default' for services that should start at boot.
_DEFAULT_RUNLEVEL = "default"
[docs]
def __virtual__():
"""
Only load on Alpine Linux systems with OpenRC present.
"""
if __grains__.get("os_family", False) != "Alpine":
return (False, "Module openrc only works on Alpine Linux based systems")
return __virtualname__
def _run(cmd):
"""
Run a command list and return the full result dict from cmd.run_all.
"""
return __salt__["cmd.run_all"](cmd, python_shell=False)
[docs]
def start(name, **_):
"""
Start the named service.
CLI Example:
.. code-block:: bash
salt '*' service.start <service name>
"""
ret = _run(["rc-service", name, "start"])
return ret["retcode"] == 0
[docs]
def stop(name, **_):
"""
Stop the named service.
CLI Example:
.. code-block:: bash
salt '*' service.stop <service name>
"""
ret = _run(["rc-service", name, "stop"])
return ret["retcode"] == 0
[docs]
def restart(name, **_):
"""
Restart the named service.
CLI Example:
.. code-block:: bash
salt '*' service.restart <service name>
"""
ret = _run(["rc-service", name, "restart"])
return ret["retcode"] == 0
[docs]
def reload_(name, **_):
"""
Send a reload signal to the named service. Not all services support
reload; consult the service's init script to confirm.
CLI Example:
.. code-block:: bash
salt '*' service.reload <service name>
"""
ret = _run(["rc-service", name, "reload"])
return ret["retcode"] == 0
[docs]
def status(name, sig=None, **_): # pylint: disable=unused-argument
"""
Return True if the named service is running, False otherwise.
The sig parameter is accepted for interface compatibility with Salt's
generic service state but is not used; rc-service status is authoritative.
CLI Example:
.. code-block:: bash
salt '*' service.status <service name>
"""
ret = _run(["rc-service", name, "status"])
return ret["retcode"] == 0
[docs]
def enabled(name, runlevel=None, **_):
"""
Return True if the named service is enabled in the given runlevel.
runlevel
The runlevel to check. Defaults to 'default'.
CLI Example:
.. code-block:: bash
salt '*' service.enabled <service name>
salt '*' service.enabled <service name> runlevel=boot
"""
runlevel = runlevel or _DEFAULT_RUNLEVEL
return name in get_enabled(runlevel=runlevel)
[docs]
def disabled(name, **kwargs):
"""
Return True if the named service is disabled in the default runlevel.
CLI Example:
.. code-block:: bash
salt '*' service.disabled <service name>
"""
return not enabled(name, **kwargs)
[docs]
def enable(name, runlevel=None, **_):
"""
Enable the named service in the given runlevel so it starts at boot.
runlevel
The runlevel to add the service to. Defaults to 'default'.
CLI Example:
.. code-block:: bash
salt '*' service.enable <service name>
salt '*' service.enable <service name> runlevel=boot
"""
runlevel = runlevel or _DEFAULT_RUNLEVEL
ret = _run(["rc-update", "add", name, runlevel])
return ret["retcode"] == 0
[docs]
def disable(name, runlevel=None, **_):
"""
Disable the named service in the given runlevel.
runlevel
The runlevel to remove the service from. Defaults to 'default'.
CLI Example:
.. code-block:: bash
salt '*' service.disable <service name>
salt '*' service.disable <service name> runlevel=boot
"""
runlevel = runlevel or _DEFAULT_RUNLEVEL
ret = _run(["rc-update", "del", name, runlevel])
return ret["retcode"] == 0
[docs]
def available(name, **_):
"""
Return True if the named service exists on the system.
CLI Example:
.. code-block:: bash
salt '*' service.available <service name>
"""
return name in get_all()
[docs]
def missing(name, **_):
"""
Return True if the named service is not available on the system.
CLI Example:
.. code-block:: bash
salt '*' service.missing <service name>
"""
return not available(name)
[docs]
def get_all(**_):
"""
Return a sorted list of all services known to rc-service.
CLI Example:
.. code-block:: bash
salt '*' service.get_all
"""
ret = _run(["rc-service", "--list"])
if ret["retcode"] != 0:
raise CommandExecutionError(
"Failed to list services",
info={"stderr": ret.get("stderr", ""), "stdout": ret.get("stdout", "")},
)
return sorted(line.strip() for line in ret["stdout"].splitlines() if line.strip())
[docs]
def get_enabled(runlevel=None, **_):
"""
Return a sorted list of services enabled in the given runlevel.
runlevel
The runlevel to query. Defaults to 'default'.
CLI Example:
.. code-block:: bash
salt '*' service.get_enabled
salt '*' service.get_enabled runlevel=boot
"""
runlevel = runlevel or _DEFAULT_RUNLEVEL
ret = _run(["rc-update", "show", runlevel])
if ret["retcode"] != 0:
raise CommandExecutionError(
f"Failed to list enabled services for runlevel '{runlevel}'",
info={"stderr": ret.get("stderr", ""), "stdout": ret.get("stdout", "")},
)
# Each output line is of the form " sshd | default"
# Lines from rc-update that are not service entries (status messages)
# do not contain the pipe character, so we filter on that.
result = []
for line in ret["stdout"].splitlines():
if "|" not in line:
continue
svc = line.split("|")[0].strip()
if svc:
result.append(svc)
return sorted(result)
[docs]
def get_running(**_):
"""
Return a sorted list of services that are currently started.
CLI Example:
.. code-block:: bash
salt '*' service.get_running
"""
# rc-status -a shows services across all runlevels with their status.
# Service lines look like:
# " sshd [ started ]"
# Runlevel header lines look like:
# "Runlevel: default" or "Dynamic Runlevel: hotplugged"
# We filter for lines that contain "started" and begin with whitespace,
# which excludes headers and blank lines.
ret = _run(["rc-status", "--nocolor", "-a"])
if ret["retcode"] != 0:
raise CommandExecutionError(
"Failed to query running services",
info={"stderr": ret.get("stderr", ""), "stdout": ret.get("stdout", "")},
)
result = []
for line in ret["stdout"].splitlines():
if line and line[0] == " " and "started" in line:
svc = line.strip().split()[0]
result.append(svc)
return sorted(result)