Run shell commands from services

Run shell commands from services, in the foreground or the background.

Your services run shell commands and command-line programs through self.commands.invoke and self.commands.invoke_async. A foreground command makes the service wait for its output, a background command does not block the service.

This is how command-line tools become API services - a service accepts data from external sources, processes it with the tools and notifies other systems when the results are ready.

Keyword arguments configure the details of the execution, e.g. what timeouts to use or what callbacks to invoke when the command completes.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class MyService(Service):

    def handle(self):

        # Command to execute ..
        command = 'cd /tmp && ls -la && whoami && uname -a'

        # .. invoke it now ..
        result = self.commands.invoke(command)

        # .. log the data returned ..
        self.logger.info('Stdout -> %s', result.stdout)
        self.logger.info('Stderr -> %s', result.stderr)

        # .. log exit code and other useful information as well.
        self.logger.info('Exit code -> %s', result.exit_code)
        self.logger.info('Timed out -> %s', result.is_timeout)

        # Consult the documentation below for more usage examples

Result objects

Each command invocation produces a CommandResult object that encapsulates information about the results produced by the command and other useful metadata, e.g. it will contain the exit code, stdout, stderr or the total time the command took to complete.

AttributeDatatypeDefault valueNotes
stdoutstr---Stdout produced by the command.
stderrstr---Stderr produced by the command.
exit_codeint-1Exit code that the command returned. A value of -1 means that the exit code is unknown, e.g. the command timed out.
timeoutfloat600.0Timeout in seconds used for the command's execution. Defaults to 10 minutes.
is_timeoutboolFalseTrue if the command timed out.
timeout_msgstr---If is_timeout is True, a human-readable message about the fact that a timeout has occurred.
total_time_secfloat---Total time in seconds the command took to complete, e.g. 1.35.
total_timestr--As above, as a string, e.g. '0:00:01.35'.
commandstr---The body of the command that was executed.
is_okbool---True if exit code is 0 or -1, False otherwise.
cidstr---Correlation ID assigned to the command. If you do not provide your own, one will be auto-generated.
callback------A callback that was invoked when the command completed. It could be a regular Python function or method, a service or a pub/sub topic. Described fuller later in the chapter.
stdinstr---If the command required for stdin to be provided, what it was.
is_asyncboolFalseTrue if the command ran in background, False otherwise.
use_pubsubstrFalseTrue if the result object was delivered to the callback using a pub/sub topic.
len_stdout_bytesint-1How many bytes of stdout the command produced.
len_stderr_bytesint-1How many bytes of stderr the command produced.
len_stdout_humanstr---A human-readable representation of len_stdout_bytes, e.g. '146.1 kB' instead of 146102
len_stderr_humanstr---As above, for stderr.
encodingstrutf8What encoding was used to convert bytes that the command produced to a string object.
replace_charstr� (U+FFFD)What character was applied to replace stdout bytes that could not be used by the encoding specified. For instance, you can use the "?" character instead of the default one to keep everything encoded in ASCII. The default value is U+FFFD REPLACEMENT CHARACTER, per the Unicode specification.
start_timedatetime---When the command started (in UTC).
start_time_isostr---As above, as a string.
end_timedatetime---When the command completed (in UTC).
end_time_isostr---As above, as a string.

Run commands in the background

Use self.commands.invoke_async to invoke a command asynchronously, in the background, with an optional callback invoked when the command finishes. See Callbacks for what a callback can be.

# -*- coding: utf-8 -*-

# Zato
from zato.server.commands import CommandResult
from zato.server.service import Service

class MyService(Service):

    def on_completed(self, result:CommandResult) -> None:

        # This will run when the command has finished
        self.logger.info('Received result: %s', result)

    def handle(self):

        # A command that will take a longer time to complete ..
        command = 'sleep 5'

        # .. invoke it in background, specifying a callback.
        self.commands.invoke_async(command, callback=self.on_completed)

Multi-line commands

Commands can span multiple lines. Use && to chain the steps - each one runs when the step before it succeeds:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class MyService(Service):

    def handle(self):

        # A multi-line command to execute ..
        command = """
        cd ~    &&
        ls -la  &&
        cd /tmp &&
        ls -la
        """

        # .. invoke it now ..
        result = self.commands.invoke(command)

        # .. log the result for inspection.
        self.logger.info('Result: %s', result)

Invocation options

When invoking a command, regardless of whether it runs in foreground or background, the following options can be specified using keyword arguments:

AttributeDatatypeDefault valueNotes
timeoutfloat600.0In seconds, how long to wait for the command to complete. Use None to wait indefinitely.
callbackany---What callback to invoke when the command finishes. It can be provided both if the invocation is in foreground or background. Read below for a detailed description of callbacks.
use_pubsubboolFalseWhether callbacks should be invoked by publishing messages to their publish/subscribe topics.
cidstr---Correlation ID. An arbitrary string that can be used to correlate the execution of the command with other events in the system, e.g. the same CID can be used elsewhere for identifying the REST request that accepted the command's input, in this way leading to a chain of events, all of them correlated using this ID. A random value will be generated if one is not given on input.
stdinstr---Some commands may require you to send stdin to them and this is how it can be provided.
encodingstrutf8What encoding to use for the command and its output.
replace_charstr� (U+FFFD)If stdout or stderr cannot be decoded into the encoding given on input, what character to use for the characters that could not be decoded. The default value of is what the Unicode specification uses but it may be overridden in this way in case another character is more suitable.

Sample code using several invocation options:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class MyService(Service):

    def handle(self):

        # Command to execute ..
        command = 'ls -la /tmp'

        # .. invoke it with a few options.
        self.commands.invoke(command, cid='abc123', timeout=90, encoding='iso-8859-1')

Callbacks

A callback is a Python function, method, a Zato service or a publish/subscribe topic that is notified about the result of the command's execution.

Callbacks can be used to form arbitrary series of information exchanges, e.g. it is possible to run a command, have its stdout published to a REST endpoint, an AMQP queue and a topic. In turn, all subscribers of messages from that topic will receive their own notifications. Other types of endpoints can be used freely.

Callbacks can be specified for both foreground and background commands. If it is the former, the callback runs before the service executing the command receives the command's result. If it is the latter, the invoking service receives a basic results object that specifies the invocation's correlation ID and other metadata. The command then runs in background and the callback is invoked when the command completes.

With commands that run in background, it is possible that the callback will run before the service that runs the command receives the results object - this is possible because the service and callback run independently, as separate tasks, and, if the command exits very quickly, its callback will execute first.

# -*- coding: utf-8 -*-

# Zato
from zato.server.commands import CommandResult
from zato.server.service import Service

# App
from myapi import MyCallbackService

class MyService(Service):

    def on_completed(self, result:CommandResult) -> None:

        # This will run when the command has finished
        self.logger.info('Received result: %s', result)

    def handle(self):

        # A command that will take a longer time to complete ..
        command = 'sleep 5'

        # Invokes the command in background and then a callback method is invoked
        self.commands.invoke_async(command, callback=self.on_completed)

        # Invokes the command in foreground and then invokes another service by its name
        self.commands.invoke(command, callback='my.callback.service')

        # Invokes the command in foreground and then invokes a previously imported service
        self.commands.invoke(command, callback=MyCallbackService)

        # These two calls invoke a command and then use publish/subscribe
        # to invoke its callbacks. The first command runs in foreground,
        # the other in background.
        self.commands.invoke(command, callback=MyCallbackService, use_pubsub=True)
        self.commands.invoke_async(command, callback='my.callback.service', use_pubsub=False)

        # This background invocation publishes a message to a named pub/sub topic as its callback
        self.commands.invoke_async(command, callback='/commands/results', use_pubsub=True)

Security considerations

All commands execute in a shell of the same operating system user that a given Zato server runs under, typically, it is zato. It means that all the permissions, environment variables or other shell constructs apply to each command as well.

It means that commands can do everything that the user can do, for instance, if the user has specific SSH keys, the commands can use them, if the user is allowed to access /etc, the commands executed by services will also be allowed to access that directory.

It also means that you must ensure that commands use only sanitized and vetted data from trusted sources only. As an example, if there is a command called 'myprogram' that accepts an option called '--email', you need to ensure that the value provided for this option truly is an email as, otherwise, if sanitization were insufficient, an attacker could use a value such as 'user@example.com ; rm -rf ~', which would result in the command of myprogram --email user@example.com ; rm -rf ~, which would first invoke the command and then delete the user's home directory.

Note also that when you log the results of the command's execution, the information saved to logs will contain all the details about the command, including its stdin, stdout and stderr. Some of this information may be sensitive - make sure that you log only the very specific information that you need.

For instance, during the initial development it is useful to simply use this:

self.logger.info('Result: %s', result)

.. but as soon as possible you may want to log only more specific details, e.g.:

self.logger.info('CID: %s; Exit code: %s', result.cid, result.exit_code)

See also

FeatureWhat it does
SchedulerRun commands on an interval through a scheduled service
Config filesKeep command paths and options outside code
MessagesBuild the responses that carry a command's results

Learn more