Commandes Shell

Utilisez self.commands.invoke ou self.commands.invoke_async pour exécuter des commandes shell et des programmes en ligne de commande. Les commandes peuvent s'exécuter en avant-plan, ce qui signifie que votre service attendra leur sortie, ou elles peuvent s'exécuter en arrière-plan, ce qui ne bloque pas votre service.

Cela permet de concevoir des services d'API et des flux de travail dont la logique de base implique ou tourne autour de l'utilisation d'outils en ligne de commande - accepter des données provenant de sources externes, les traiter à l'aide d'outils invoqués comme s'ils s'exécutaient en ligne de commande et notifier les systèmes ou applications externes lorsque les résultats sont prêts.

Plusieurs options peuvent être fournies pour configurer les détails de l'exécution des commandes, par exemple les délais à utiliser ou les rappels à invoquer lorsque la commande est terminée.

# -*- 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

Objets de résultat

Chaque invocation de commande produit un objet CommandResult qui encapsule des informations sur les résultats produits par la commande et d'autres métadonnées utiles, par exemple, il contiendra le code de sortie, stdout, stderr ou le temps total que la commande a pris pour se terminer.

Attribut Type de données Valeur par défaut Notes
stdout str --- Stdout produit par la commande.
stderr str --- Stderr produit par la commande.
exit_code int -1 Code de sortie que la commande a renvoyé. Une valeur de -1 signifie que le code de sortie est inconnu, par exemple que la commande a expiré.
timeout float 600.0 Timeout en secondes utilisé pour l'exécution de la commande. La valeur par défaut est de 10 minutes.
is_timeout bool False Vrai si la commande timed out.
timeout_msg str --- Si is_timeout est True, un message lisible par l'homme sur le fait que un délai d'attente s'est produit.
total_time_sec float --- Temps total en secondes que la commande a pris pour se terminer, par exemple 1.35.
total_time str -- Comme ci-dessus, sous forme de chaîne, par exemple '0:00:01.35'.
command str --- The body of the command that was executed.
is_ok bool --- Vrai si le code de sortie est 0 ou -1, Faux sinon.
cid str --- ID de corrélation attribué à la commande. Si vous ne fournissez pas le vôtre, il sera généré automatiquement.
callback --- --- Un callback qui a été invoqué lorsque la commande s'est terminée. Il peut s'agir d'une fonction ou d'une méthode Python ordinaire, d'un service ou d'un sujet pub/sub. Décrit plus en détail plus loin dans ce chapitre.
stdin str --- Si la commande exigeait que stdin soit fourni, ce qu'il était.
is_async bool False Vrai si la commande a été exécutée en arrière-plan, Faux sinon.
use_pubsub str False Vrai si l'objet résultat a été livré à la callback en utilisant un sujet pub/sub.
len_stdout_bytes int -1 Combien d'octets de stdout la commande a produit.
len_stderr_bytes int -1 Combien d'octets de stderr la commande a produit.
len_stdout_human str --- Une représentation lisible par l'homme de len_stdout_bytes, par exemple "146,1 kB" au lieu de 146102.
len_stderr_human str --- Comme ci-dessus, pour stderr.
encoding str utf8 Quel encodage a été utilisé pour convertir les octets produits par la commande en un objet chaîne de caractères.
replace_char str � (U+FFFD) Quel caractère a été appliqué pour remplacer les octets stdout qui ne pouvaient pas être utilisés par l'encodage spécifié. Par exemple, vous pouvez utiliser le caractère " ?" au lieu du caractère par défaut pour que tout reste codé en ASCII. La valeur par défaut est U+FFFD REPLACEMENT CHARACTER, selon la spécification Unicode.
start_time datetime --- Date de début de la commande (en UTC).
start_time_iso str --- Comme ci-dessus, sous forme de chaîne.
end_time datetime --- Quand la commande s'est terminée (en UTC).
end_time_iso str --- Comme ci-dessus, sous forme de chaîne.

Exécution de commandes en arrière-plan

Utilisez self.commands.invoke_async pour invoquer une commande de manière asynchrone, en arrière-plan. Vous pouvez spécifier un callback optionnel qui sera invoqué à la fin de la commande. Vous en saurez plus sur les callbacks plus tard dans ce chapitre.

# -*- 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)

Commandes sur plusieurs lignes

Les commandes peuvent s'étendre sur plusieurs lignes. Utilisez && - la syntaxe régulière de continuation de ligne de Bash - pour diviser une seule ligne en plusieurs.

# -*- 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)

Options d'invocation

Lors de l'invocation d'une commande, qu'elle soit exécutée en avant-plan ou en arrière-plan, les options suivantes peuvent être spécifiées à l'aide d'arguments sous forme de mots-clés:

Attribute Type de donnée Valeur par défaut Notes
timeout float 600.0 En secondes, le temps d'attente pour que la commande se termine. Utilisez None pour attendre indéfiniment.
callback any --- Le callback à invoquer lorsque la commande se termine. Elle peut être fournie à la fois si l'invocation est en avant-plan ou en arrière-plan. Lisez ci-dessous pour une description détaillée des callbacks.
use_pubsub bool False Si les callbacks doivent être invoqués en publiant les messages dans leurs sujets publish/subscribe.
cid str --- ID de corrélation. Une chaîne arbitraire qui peut être utilisée pour corréler l'exécution de la commande avec d'autres événements dans le système, par exemple, le même CID peut être utilisé ailleurs pour identifier la demande REST qui a accepté l'entrée de la commande, conduisant ainsi à une chaîne d'événements, tous corrélés en utilisant cet ID. Une valeur aléatoire sera générée si aucune valeur n'est donnée en entrée.
stdin str --- Certaines commandes peuvent nécessiter que vous leur envoyiez stdin et c'est ainsi qu'elles peuvent être fournies.
encoding str utf8 Quel encodage utiliser pour la commande et sa sortie.
replace_char str � (U+FFFD) Si stdout ou stderr ne peut pas être décodé dans l'encodage donné en entrée, quel caractère utiliser pour les caractères qui n'ont pas pu être décodés. La valeur par défaut de est celle utilisée par la spécification Unicode, mais elle peut être remplacée de cette manière si un autre caractère est plus approprié.

Exemple de code utilisant plusieurs options d'invocation:

# -*- 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')

Utilisation des callbacks

Un callback est une fonction ou une méthode Python, un service Zato ou un sujet publish/subscribe qui est notifié du résultat de l'exécution de la commande.

Les rappels peuvent être utilisés pour former des séries arbitraires d'échanges d'informations, par exemple, il est possible d'exécuter une commande, de publier son stdout vers un endpoint REST, une file d'attente AMQP et un sujet. À leur tour, tous les abonnés aux messages de ce sujet recevront leurs propres notifications. D'autres types d'endpoints peuvent être utilisés librement.

Les rappels peuvent être spécifiés à la fois pour les commandes de premier plan et d'arrière-plan. Dans le premier cas, le rappel est exécuté avant que le service qui exécute la commande ne reçoive le résultat de celle-ci. Dans le second cas, le service qui l'invoque reçoit un objet de résultats de base qui spécifie l'ID de corrélation de l'invocation et d'autres métadonnées. La commande s'exécute alors en arrière-plan et la fonction de rappel est appelée lorsque la commande existe.

Avec les commandes qui s'exécutent en arrière-plan, il est possible que la fonction de rappel s'exécute avant que le service qui exécute la commande ne reçoive l'objet de résultats - cela est possible parce que le service et la fonction de rappel s'exécutent indépendamment, comme des tâches distinctes, et, si la commande se termine très rapidement, sa fonction de rappel s'exécutera en premier.

# -*- 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)

Considérations de sécurité

Toutes les commandes s'exécutent dans un shell du même utilisateur du système d'exploitation sous lequel tourne un serveur Zato donné, généralement, il s'agit de zato. Cela signifie que toutes les permissions, variables d'environnement ou autres constructions du shell s'appliquent également à chaque commande.

Cela signifie que les commandes peuvent faire tout ce que l'utilisateur peut faire, par exemple, si l'utilisateur possède des clés SSH spécifiques, les commandes peuvent les utiliser, si l'utilisateur est autorisé à accéder à /etc, les commandes exécutées par les services seront également autorisées à accéder à ce répertoire.

Cela signifie également que vous devez vous assurer que les commandes n'utilisent que des données aseptisées et vérifiées provenant uniquement de sources fiables. Par exemple, si une commande appelée "myprogram" accepte une option appelée "--email", vous devez vous assurer que la valeur fournie pour cette option est réellement un courrier électronique. Dans le cas contraire, si la désinfection est insuffisante, un attaquant pourrait utiliser une valeur telle que "mallory@example.com ; rm -rf ~", ce qui entraînerait la commande myprogram --email mallory@example.com ; rm -rf ~, qui invoquerait d'abord la commande, puis supprimerait le répertoire personnel de l'utilisateur.

Notez également que lorsque vous enregistrez les résultats de l'exécution de la commande, les informations enregistrées dans les logs contiendront tous les détails de la commande, y compris ses stdin, stdout et stderr. Certaines de ces informations peuvent être sensibles - assurez-vous que vous n'enregistrez que les informations très spécifiques dont vous avez besoin.

Par exemple, pendant le développement initial, il est utile d'utiliser simplement ceci:

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

.. mais dès que possible, vous voudrez peut-être enregistrer uniquement des détails plus spécifiques, par exemple :

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

Sujets connexes