"""
The service module defines the ``Service`` helper class.
"""
# pylint: disable=r1705, r0902
from .commands import Run
from .deferred import Deferred
[docs]
class Service:
"""
The ``Service`` class is a helper class, that allows to deal with services
that an experiment scheduler needs to start and stop over the course
of its execution. It leverages ``systemd-run``, which thus needs
to be available on the remote box.
Typical examples include starting and stopping a netcat server,
or a tcpdump session.
A ``Service`` instance is then able to generate a Command instance for
starting or stopping the service, that should be inserted in an SshJob,
just like e.g. a usual Run instance.
Parameters:
command(str): the command to start the service;
a ``Deferred`` instance is acceptable too
service_id(str): this mandatory id is passed to ``systemd-run``
to monitor the associated transient service;
should be unique on a given host,
in particular so that ``reset-failed`` can work reliably
tty(bool): some services require a pseudo-tty to work properly
systemd_type(str): a systemd service unit can have several values
for its ``type`` setting, depending on the forking strategy implemented
in the main command. The default used in ``Service`` is ``simple``,
which is correct for a command that hangs (does not fork
or go in the background). If on the contrary the command already
handles forking, then it may be appropriate to use the ``forking``
systemd type instead. Refer to systemd documentation for more details,
at
https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=
environ: a dictionary that defines additional environment variables
to be made visible to the running service. In contrast with what happens
with regular `Run` commands, processes forked by systemd have a very
limited set of environment variables defined - typically only
``LANG`` and ``PATH``. If your program rely on, for example,
the ``USER`` variable to be defined as well, you may specify it here,
for example ``environ={'USER': 'root'}``
stop_if_running: by default, prior to starting the service using systemd-run,
``start_command`` will ensure that no service of that name is currently
running; this is especially useful when running the same experiment
over and over, if you cannot be sure that your experiment code properly
stops that service.
Setting this attribute to ``False`` prevents this behaviour, and in
that case the `start_command` will issue a mere invokation of
``systemd-run``.
Example:
To start a remote service that triggers a tcpdump session::
service = Service(
"tcpdump -i eth0 -w /root/ethernet.pcap",
service_id='tcpdump',
tty=True)
SshJob(
remotenode,
commands=[
Run(service.start_command()),
],
scheduler=scheduler,
)
# and down the road when you're done
SshJob(
remotenode,
commands=[
Run(service.stop_command()),
],
scheduler=scheduler,
)
"""
def __init__(self, command, *, service_id,
tty=False,
systemd_type='simple',
environ=None,
stop_if_running=True,
verbose=False):
self.command = command
self.service_id = service_id
self.tty = tty
self.systemd_type = systemd_type
self.environ = environ if environ else {}
self.stop_if_running = stop_if_running
self.verbose = verbose
def _start(self):
# the -t option is a.k.a. --pty but on ubuntu-16.04 at least
# the long version is broken
commands = []
if self.stop_if_running:
# stop if running
is_active = self._manage("is-active", trash_output=True)
stop = self._manage("stop")
commands.append(f"{is_active} && {stop}")
# reset-failed
commands.append(self._manage("reset-failed", trash_output=True))
# it seems safer to avoid affecting global state
# so we avoid systemctl set-environment
# and use --setenv option instead
environ_options = " ".join(
f"--setenv {var}='{value}'" for var, value in self.environ.items()
)
tty_option = "" if not self.tty else "--pty"
commands.append(f"systemd-run {tty_option}"
f" {environ_options}"
f" --unit={self.service_id}"
f" --service-type={self.systemd_type}"
f" {self.command}")
# support deferred evaluation
# here we need to preserve the type of self.command
intermediate = " ; ".join(commands)
if isinstance(self.command, Deferred):
# create a deferred with the same variables environment
return self.command.dup_from_string(intermediate)
else:
return intermediate
def _manage(self, subcommand, trash_output=False):
"""
subcommand is sent to systemctl, be it status or stop
"""
trash_part = " >& /dev/null" if trash_output else ""
return f"systemctl {subcommand} {self.service_id}{trash_part}"
def _mode_label(self, mode, user_defined):
if user_defined:
return user_defined
if mode != 'start' or not self.verbose:
return f"Service: {mode} {self.service_id}"
# start & verbose: show command
multiline = self._start().replace(';', '\n')
return fr"Service: {self.service_id}\n{multiline}"
[docs]
def start_command(self, *, label=None, **kwds):
"""
Returns:
a Run instance suitable to be inserted in a SshJob object
"""
label = self._mode_label("start", label)
return Run(self._start(), label=label, **kwds)
[docs]
def stop_command(self, *, label=None, **kwds):
"""
Returns:
a Run instance suitable to be inserted in a SshJob object
"""
label = self._mode_label("stop", label)
return Run(self._manage('stop'), label=label, **kwds)
[docs]
def status_command(self, *, output=None, label=None, **kwds):
"""
Returns:
a Run instance suitable to be inserted in a SshJob object
"""
command = self._manage('status')
if output:
command += f" > {output}"
label = self._mode_label("status", label)
return Run(command, label=label, **kwds)
# since : see journalctl options
# e.g. since="10 seconds ago"
[docs]
def journal_command(self, *, label=None, since=None, **kwds):
"""
the command to run
"""
command = f"journalctl --unit {self.service_id}"
if since:
command += f' --since "{since}"'
return Run(command, label=label, **kwds)