Salt : Supervision par courriel, premiers pas

Publié le sam. 21 juin 2014 par Feth Arezki

Introduction

Salt est un outil de gestion de configuration pour votre infrastructure informatique programmé en Python, on en a déjà parlé par ici.

Il contient tous les ingrédients nécessaires à un outil de supervision. D'autres ont largement débuté des interfaces graphiques pour cela.

Notre besoin du jour était de se rendre compte quand un service est en panne, ce qui est habituellement le travail de Shinken ou autres.

La solution mise en place avec salt consiste à utiliser son scheduler (programmateur) pour lancer un script maison qui testera le service en question, puis à remonter les informations pertinentes (ça marche, ça marche pas) à l'administrateur, par courriel pour l'instant.

Nous créerons un état et un returner maison, et vous allez voir pourquoi.

Enfin, pardon pour certains manques de rigueur, ceci est plus une preuve de concept qu'une implémentation robuste (en particulier pour les mutex à base de fichiers…).

Rangement dans salt

le présent article décrit trois fichiers tous rangés dans test_sftp :

  • requirements.txt

    fichier utilisé pour installer des paquets Python dans un virtualenv

  • init.sls

    états salt

  • test_sftp.py

    script Python qui sera exécuté pour effectuer le test.

Tu peux test

Le script

Le script qui teste notre service sftp, dans sa forme la plus simple, est celui-ci (ensuite, il faut l'assouplir avec jinja) :

#!/home/unixtestuser/sftp_test/bin/python

"""
test_sftp.py : tests a remote sftp server.

asserts that the sftp connexion is succesful
and that they were files on the other side.
"""

import pysftp


_HOST = "sftphost.example.com",
# ça ne sera pas difficile de mettre tout ceci dans pillar, si ?
srv = pysftp.Connection(
    host=_HOST,
    port=42,
    username="testuser",
    password="testpassword"
    )

data = srv.listdir()
srv.close()

assert data, "test of {0} sftp: Failure!".format(_HOST)

print(
        "{0} sftp: remotely found contents: {1}".format(
            _HOST,
            ', '.join(data),
        )

Ce script très simple sortira avec un code différent de 0 en cas de problème de connexion, d'authentification, ou s'il n'y a pas de fichiers à l'autre bout. Il donnera des informations pertinentes sur les sorties standard et erreur.

Les états

Ce script doit être exécuté depuis un virtualenv situé dans /home/unixtestuser/sftp_test ; j'ai par ailleurs un state qui crée l'utilisateur demandé ; voici les états créant le virtualenv et exécutant le script de test.

# init.sls
{% set test_file = '/home/unixtestuser/sftp_test/test_sftp.py' %}
{% set venv = '/home/unixtestuser/sftp_test' %}
sftp_testing:
  virtualenv.managed:
    - no_site_packages: True
    - name: {{ venv }}
    - user: temp
    - python: /usr/bin/python3
    - requirements: salt://test_sftp/requirements.txt

sftp_assert:
  cmd.script:
    - source: salt://test_sftp/test_sftp.py
    - cwd: {{ venv }}
    - user: 'temp'
    - require:
      - virtualenv: sftp_testing

Prérequis Python

Pour être complet, requirements.txt

pysftp

La programmation

Pour l'instant, je programme le lancement régulier de l'état sftp_assert directement dans le minion. J'ai ajouté deux sections à /etc/salt/minion :

Configuration de smtp_returner

Je souhaite être notifié par courriel.

smtp.from: 'robots@example.com'
smtp.to: 'admins@example.com'
smtp.host: 'smtp.example.com'
smtp.tls: True
smtp.subject: 'salt talks to you'
smtp.fields: id

Exécution régulière du script :

schedule:
  test_sftp:
    function: state.sls
    seconds: 30
    returner: cond_smtp
    args:
      - test_sftp

Pourquoi ``cond_smtp`` ? Comme je le disais, je souhaite être informé par courriel du moindre souci, ou du fait que ça fonctionne, et smtp_returner semble tout indiqué pour cela, à deux détails près :

  1. il ne sait pas faire la différence entre un succès et un échec du point de vue du test qui m'intéresse (il sait seulement si la commande programmée a bien été exécutée).
  2. Il m'enverrait un courriel à chaque exécution. Toutes les 30 secondes ? ça va pas non !

Je vais pré-traiter l'information avant de la faire passer à smtp_returner, et donc créer mon propre returner, que j'appellerai cond_smtp, voyons cela maintenant.

Créer un returner

Il suffit de créer un fichier Python dans _returners et d'y créer une fonction returner acceptant un dictionnaire (ainsi qu'une fonction __virtual__ comme pour tout module Salt, qui retourne le nom du module ou False s'il ne peut se charger.

La logique de mon returner est un peu plus compliquée, mais dans le principe, j'espère rien de trop sorcier.

Voici le fichier dans son intégralité - son comportement est détaillé plus bas.

import collections
import logging
import os
import pprint
import time

import salt.returners.smtp_return


log = logging.getLogger(__name__)


_ALERT_LOCK_FILES = {}


def __virtual__():
    return 'cond_smtp'


def returner(ret):
    log.debug(pprint.pformat(ret))
    function = ret['fun']
    success = _analyze_success(ret)

    if _alert_locked(function, True) and _alert_locked(function, False):
        log.info("Coherence problem: resetting locks {0} {1}".format(
            _alert_lock_filename(function, True),
            _alert_lock_filename(function, False),
            ))
        _unlock_alert(function, True)
        _unlock_alert(function, False)

    if _alert_locked(function, not success):
        log.info('Status for {2} change: from {0} to {1}'.format(
            success, not success, function
            ))
        _unlock_alert(function, not success)

    if _alert_locked(function, success):
        log.debug('Alert already sent for {0} / {1}'.format(function, success))
        return

    log.debug('sending mail for {0} / {1}'.format(function, success))

    _send_mail(ret)
    _lock_alert(function, success)


def _send_mail(ret):
    log.debug("Sending mail")
    setattr(salt.returners.smtp_return, '__salt__', __salt__)
    salt.returners.smtp_return.returner(ret)


def _alert_lock_filename(function, success):
    prefix = '{0}_{1}'.format(
        function,
        'success' if success else 'failure'
        )
    if prefix in _ALERT_LOCK_FILES:
        return _ALERT_LOCK_FILES['prefix']

    filename = '/var/lock/{0}_{1}.lock'.format(__name__, prefix)
    _ALERT_LOCK_FILES['prefix'] = filename
    return filename


def _analyze_success(return_values):
    if not return_values['success']:
        return False

    if 'return' in 'return_values':
        for subreturn in return_values['return'].values():
            if subreturn['result'] in (False, None, 0):
                return False
            if isinstance(subreturn['result'], collections.Mapping):
                message = "Recursivity further than 1 lvl is not handled yet"
                log.error(message)
                return_values['cond_returner_message'] = message
                return False

    return True


def _alert_locked(function, success):
    """
    :param bool success: whether call succeeded
    """
    filename = _alert_lock_filename(function, success)
    return os.path.exists(filename)


def _lock_alert(function, success):
    filename = _alert_lock_filename(function, success)
    open(filename, 'w').write('{0}'.format(
        time.time()
        ))


def _unlock_alert(function, success):
    filename = _alert_lock_filename(function, success)
    log.debug(os.unlink(filename))

Détermination d'un succès ou d'un échec

Voir _analyze_success. Ici, je reçois un dictionnaire ret qui contient une clef valeur immuable (sauf si j'avais un souci lors du lancement de state.sls) :

'success': True

Nous testons tout de même cette clef.

Ensuite, l'information d'exécution des états exécutés se trouve dans les valeurs d'un dictionnaire à la clef return, et prend la forme de dictionnaires à son tour, avec booléen sous la clef result. Ouf.

{
    'success': True,
    'return': {
        'result': True,
        ...
        },
    ...
}

Notification sur changement d'état seulement

Mon returner est exécuté dans un contexte différent à chaque fois par Salt.

Pas moyen de stocker des variables en mémoire : j'ai opté, temporairement, pour des fichiers verrous dont le nom est prédictible et entraînera des collisions si je programme deux appels à state.sls (le nom contient le nom de la fonction exécutée et pas de ses arguments).

Si le fichier témoignant d'un échec passé et celui témoignant d'un succès passé existent tous deux, je les supprime et on repart à zéro.

On ne notifie pas un succès/échec si l'état précédent était identique.

Enfin, envoyer un courriel

Comme le module que j'enveloppe (wrappe) n'a pas été importé par Salt lui-même, je dois lui affecter l'attribut __salt__ que mon module a bien reçu, lui. Cet attribut magique permet d'avoir accès à l'instance en cours d'exécution.

Je n'ai ensuite qu'à lui confier les valeurs de retour.

setattr(salt.returners.smtp_return, '__salt__', __salt__)
salt.returners.smtp_return.returner(ret)

Conclusion

J'espère que ce qui précède n'a pas paru trop barbare. Je n'ai pas eu de difficulté particulière à venir à bout de mon besoin, et j'attribue cela à l'architecture souple de Salt, et à la lisibilité du code source, judicieusement en Python (qui remplace un peu la doc, il faut l'avouer).