Intégration de Zabbix dans Zendesk (ou l'inverse)

Intégration de Zabbix dans Zendesk (ou l'inverse)

Introduction

Ce billet a pour but de répondre à la problématique suivante : comment intégrer Zabbix à Zendesk ?

Zendesk est un outil SaaS de gestion d'incidents (gestion de tickets). Zabbix est quant à lui un outil de supervision.

Tout est parti du constat suivant : les outils de supervision, en général, utilisent le mail comme moyen de communication principal. Quel sysadmin n'a jamais trouvé au petit matin sa boîte mail remplie à craquer de mails de Zabbix indiquant aussi bien des problèmes réels que des faux positifs ou pire, des flaps. Vous me direz, et vous aurez raison, qu'avoir autant de mails est signe que la supervision est male configurée. Certes, mais là n'est pas le sujet.

Notre objectif est de permettre au sysadmin ou à l'agent du support, lorsqu'il arrive à son poste de tout de suite, immédiatement, voir les problèmes qu'il doit traîter. Pour ce faire, l'idée est la suivante : Lorsqu'un incident est détecté par Zabbix (hausse CPU, manque de RAM, etc.), on aimerait qu'un ticket Zendesk soit automatiquement ouvert. Rien de bien compliqué à priori. Si on s'arrête là, on remplace juste le mail par le ticket. Pas très intéressant. Allons donc un peu plus loin : lors que l'incident disparaît de Zabbix, faisons en sorte que le ticket Zendesk disparaisse également.

Solution technique

Comment faire ? Indice : API.

En effet, nous allons utiliser l'API publique offerte par Zendesk. Cette API permet de faire tout ce que l'on désire. Pour que Zabbix puisse parler à Zendesk, il nous faut créer un script d'alerting. Voyons ça.

Côté Zendesk

Presque rien à faire, sinon récupérer la clé d'API pour l'utilisateur dont se servira Zabbix. Pour ce faire, allez sur votre Zendesk, puis cliquez sur la roue crantée d'administration. Puis dans la partie "Canaux" cliquez sur "API".

zabbix-zendesk-09.png

Activez l'accès par jeton, puis récupérez votre jeton. Vous en aurez besoin pour la configuration de Zabbix. zabbix-zendesk-10.png

Cliquez sur Enregistrer. C'est tout pour Zendesk !

Côté Zabbix

Script d'alerting

Au début, je voulais faire ce script en Perl. Mais vu que je me fais railler à chaque fois que je parle de Perl, et bien ce sera du PHP.

1
2
3
4
5
6
7
#!/usr/bin/php
<?php

// Arguments en entrée
$to      = $argv[1] ;
$subject = $argv[2] ;
$message = $argv[3] ;

Lorsque Zabbix envoie une alerte à un script, 3 paramètres sont envoyés : le déstinataire, le sujet, et le message. Nous verrons plus bas où sont définis ces champs.

$zendesk = explode (';', $to) ;
define('ZDURL', 'https://'. $zendesk[0] .'.zendesk.com/api/v2') ;
define('ZDUSER', $zendesk[1]) ;
define('ZDAPIKEY', $zendesk[2]) ;

Ici, on définit les paramètrs de connexion à l'API Zendesk. Le champ "To" étant unique, et ayant besoin de 3 informations (URL de Zendesk, User Zendesk et clé API), on sépare ces 3 valeurs par un point-virgule.

$message = explode ("\n", $message) ;
$trigger = array (
    'name'       => explode (':', $message[0])[1],
    'id'         => explode (':', $message[1])[1],
    'status'     => explode (':', $message[2])[1],
    'hostname'   => explode (':', $message[3])[1],
    'ip'         => explode (':', $message[4])[1],
    'value'      => explode (':', $message[5])[1],
    'event_id'   => explode (':', $message[6])[1],
    'severity'   => explode (':', $message[7])[1],
    'zendesk_id' => '') ;

$trigger['zendesk_id'] = md5 ($trigger['id'] . $trigger['hostname']) ;

Puis, on met en forme les informations reçues brute par Zabbix dans un joli tableau. Oh, mais qu'est ce que cette variable étrange $trigger['zendesk_id'] ? Et bien, lorsqu'un ticket est créé, il a automatiquement un ID, choisit par Zendesk, pas par nous. Notre but étant de pouvoir supprimer un ticket déjà ouvert, et notre script étant state less (buzzzzzz word !) il nous faut un moyen imparable pour retrouver un ticket correspondant à une alerte.

Heureusement, Zendesk permet, via son API de fixer un "external_id". On peut mettre ce que l'on veut dans ce champ. Nous mettons le hash MD5 du trigger id de zabbix concaténé au hostname de la machine concernée.

$ticket_id = ticket_exists ($trigger['zendesk_id']) ;

Première étape, il faut vérifier qu'un ticket avec cet "external_id" n'existe pas déjà. Nous allons détailler cette fonction, mais en résumé, elle renvoie -1 si le ticket n'existe pas, et renvoie l'id du ticket si celui-ci existe.

function ticket_exists ($external_id)
{
    $data = zendesk_API ('/tickets.json?external_id='.$external_id, null, 'GET') ;

    if (isset ($data->tickets[0]->id))
        return $data->tickets[0]->id ;
    else
        return -1 ;
}

La fonction ticket_exists se contente de faire un appel à l'API Zendesk, via la fonction Zendesk_API. Cette dernière nous renvoie un objet énorme, avec plein d'information.

Pour savoir quoi récupérer dans cet amas d'information, la fonction print_r est bien utile. Elle affiche de façon structurée le contenu d'une variable.

Donc, si l'API renvoie un ID, c'est que le ticket existe, sinon, c'est qu'il n'existe pas.

Allons maintenant voir comment fonctionne cette fonction d'appel à l'API Zendesk. Honte à moi ! Je n'ai rien inventé. J'ai juste copié la fonctionn donnée par Zendesk dans sa documentation officielle : https://support.zendesk.com/hc/en-us/articles/203691216-Zendesk-REST-API-tutorial-PHP-edition

Passons à la suite.

On l'a vu, il y a deux cas possibles :

1) Une nouvelle alerte : on créée un ticket 2) La fin d'une alerte : on supprime le ticket

Pour discriminer ces deux cas, on demande à Zabbix de changer le champ $subject. Lors d'une nouvelle alerte, subject vaut "trigger" lors de la disparition de l'alerte, subject vaut "resolve".

Voyons cela :

if ($subject == 'trigger')
{
    if ($ticket_id < 0)
    {
        $ticket['external_id'] = $trigger['zendesk_id'] ;
        $ticket['subject']     = 'ZABBIX ['.$trigger['severity'].'] -  Problème sur '. $trigger['hostname'].' : '. $trigger['name'] ;
        $ticket['message']     = 'Une alerte est actuellement en cours :' ."\n" ;
        $ticket['message']    .= '- '. $trigger['name'] ."\n" ;
        $ticket['message']    .= '- '. $trigger['hostname'].' ('. $trigger['ip'] .")\n" ;
        $ticket['message']    .= '- '. $trigger['severity']. "\n\n" ;
        $ticket['message']    .= 'Connectez vous sur http://zabbix.osones.com pour plus de détails' ;

        switch ($trigger['severity'])
        {
            case 'Not classified':
                $ticket['priority'] = 'low' ;
                break ;
            case 'Information':
                $ticket['priority'] = 'low' ;
                break ;
            case 'Warning':
                $ticket['priority'] = 'normal' ;
                break ;
            case 'Average':
                $ticket['priority'] = 'high' ;
                break ;
            case 'High':
                $ticket['priority'] = 'high' ;
                break ;
            case 'Disaster':
                $ticket['priority'] = 'urgent' ;
                break ;
        }

        create_ticket ($ticket['external_id'], $ticket['subject'], $ticket['message'], $ticket['priority']) ;
    }
}

Donc, ici, à priori, on souhaite créer un nouveau ticket. Oui, mais seulement si le ticket n'a pas déjà été créé, d'où la condition sur $ticket_id. Après, rien de bien méchant, on formate juste le message du ticket, et on définit une priorité du ticket en fonction de la sévérité remontée par Zabbix.

Puis, on déclenche la création du ticket via la fonction create_ticket que voici.

function create_ticket ($external_id, $subject, $message, $priority)
{
    $data = array (
        'ticket' => array (
            'external_id' => $external_id,
            'subject' => $subject,
            'description' => $message,
            'priority' => $priority,
            'type'     => 'incident',
            'requester' => array (
                'name' => 'Zabbix',
                'email' => 'zabbix@osones.com')
));

    zendesk_API('/tickets.json', json_encode ($data), 'POST');
}

Là encore, rien de bien méchant. On se contente de mettre en forme un tableau qui sera par la suite converti en JSON puis passé à la fonction d'appel à l'API Zendesk. Le formatage du tableau est parfaitement documenté par Zendesk, ici : https://developer.zendesk.com/rest_api/docs/core/tickets

Voilà. Notre ticket est créé. Maintenant, si le subject n'est pas "trigger", mais "resolve", alors, nous allons supprimer ce ticket :

else
{
    if ($ticket_id > 0)
    {
        delete_ticket ($ticket_id) ;
    }
}

Même principe, si le ticket existe bien (on ne va pas supprimer un ticket qui n'existe pas), alors on appelle la fonction delete_ticket, avec comme paramètre l'ID dudit ticket. Voilà la fonction :

function delete_ticket ($id)
{
    zendesk_API ('/tickets/'.$id.'.json', null, 'DELETE') ;
}

Rien a dire tellement c'est simple !

C'est terminé ! Pour la partie script tout du moins.

Il faut maintenant copier ce script sur le serveur Zabbix. Chez moi, dans /etc/zabbix/alert.d et ne pas oublier de le rendre exécutable par l'utilisateur zabbix.

Le lien vers le script complet est en bas de page.

Configuration de Zabbix

Un peu de clic-clic maintenant :) Il faut en effet configurer Zabbix pour lui dire d'utiliser le beau script que l'on vient d'écrire.

Media Type

C'est parti, connectons-nous à l'interface d'administration de Zabbix, puis cliquons sur Administration... Media Types zabbix-zendesk-01.png

Puis, cliquons sur Create media type. zabbix-zendesk-02.png

Un nouvel écran s'ouvre : zabbix-zendesk-03.png

Name, mettez ce que vous voulez. Type, "Script" script name: le nom de votre script. Rapplez-vous, nous l'avons appelé /etc/zabbix/alert.d/zendesk. Donc pour nous c'est "zendesk". Cliquez sur Save.

Action

Il faut maintenant définir une action. Que doit-faire avec ce script ? Cliquez sur Configuration... Actions puis Create Action. zabbix-zendesk-04.png

Name, mettez ce que vous voulez. Default subject : très important, IL FAUT mettre "trigger" Default message : très important, IL FAUT mettre ceci :

name:{TRIGGER.NAME}
id:{TRIGGER.ID}
status:{TRIGGER.STATUS}
hostname:{HOSTNAME}
ip:{IPADDRESS}
value:{TRIGGER.VALUE}
event_id:{EVENT.ID}
severity:{TRIGGER.SEVERITY}

Cochez la case "Recovery message" Revoery subject, IL FAUT mettre "resolve" Revory message, IL FAUT mettre :

name:{TRIGGER.NAME}
id:{TRIGGER.ID}
status:{TRIGGER.STATUS}
hostname:{HOSTNAME}
ip:{IPADDRESS}
value:{TRIGGER.VALUE}
event_id:{EVENT.ID}
severity:{TRIGGER.SEVERITY}

Cliquez ensuite sur l'onglet Conditions. Cet onglet permet de définir les conditions que doit réunir le trigger pour qu'un message soit envoyé via le script que l'on a écrit. Voici les miennes : zabbix-zendesk-05.png

Enfin, cliquez sur l'onglet Operations. Ici, on définit ce qu'il doit se passer lorsque ce media est utilisé. Dans notre cas, on veut juste que le script soit appelé immédiatement pour l'utilisateur "support". zabbix-zendesk-06.png

Cliquez sur sur Save.

Il faut maintenant configurer notre utilisateur "support" pour lui donner ses informations de compte Zendesk. C'est parti. Cliquez sur Administration...Users puis selectionnez l'utilisateur. Support pour nous. Puis, cliquez sur l'onglet Media. zabbix-zendesk-07.png

Cliquez sur Add Une fenêtre souvre : zabbix-zendesk-08.png

Type, choisissez Zendesk (c'est le Media Type que l'on a créé juste avant) Send to, il faut formater ce champ comme ceci :

;;

Cochez en suite les sévérités qui déclencheront ou non l'ouverture d'un ticket. Pour nous, c'est tout à partir de Warning.

C'est terminé !

La discussion continue !

Nous attendons vos questions, remarques & mots doux sur notre Twitter :