Linux : utiliser des caractères spéciaux dans Bash

Supposons que j’ai un fichier qui contient :

Ligne1 aze
Ligne2 rty
Ligne3 ui
Ligne4 op

Note : il n’y a pas d’espace dans le fichier, uniquement des tabulations.

Comment faire pour afficher uniquement la deuxième colonne ? La commande cut serait parfaite mais il n’est pas possible de mettre une tabulation dans une ligne de commande Bash.

En fait c’est plutôt facile, Il est possible d’utiliser des séquences d’échappement grâce à l’ANSI-C Quoting (désolé, j’ai pas trouvé de traduction). Exemple :

$ cut -d $'\t' -f 2 fichier.txt
aze
rty
ui
op

La liste des séquences d’échappement est disponible ici : https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html

L’ANSI-C Quoting est disponible avec toutes les commandes Bash, et permets d’utiliser pas mal de commandes de manière « non standard ». Par exemple, pour affiche la troisième ligne d’un fichier :

$ cut -d $'\n' -f 3 fichier.txt
Ligne3 ui

Linux : changer le séparateur dans une boucle for

Admettons que j’ai un fichier qui contienne :

Linux, y’a moins bien mais c’est plus chère.
rm -fr en root, system dans la choucroute.

Faisons une boucle for basique dessus :

for i in `cat fichier.txt`
do
  echo $i
done

Le résultat ne correspond pas à ce à quoi on pourrait s’attendre :

Linux,
y’a
moins
bien
mais
c’est
plus
chère.
rm
-fr
en
root,
system
dans
la
choucroute.

La raison est simple, la boucle for prends comme séparateur les retours à la ligne, mais aussi les espaces, les tabulations et tous les autres caractères du style. Heureusement, la variable d’environnement $IFS (comme Internal Field Separator) permets de fournir sa propre liste de séparateurs. Il suffit de faire :

IFS=$'\n'
for i in `cat fichier.txt`
do
  echo $i
done

Le résultat correspond à ce qui est attendu :

Linux, y’a moins bien mais c’est plus chère.
rm -fr en root, system dans la choucroute.

Zabbix : surveiller l’évolution du statut de triggers

Dans le cadre de mon travail il m’est arrivé de devoir surveiller l’évolution des triggers de plusieurs hôtes de façon arbitraire. Il est possible de s’en sortir via les Host groups et la page Monitoring > Triggers, mais ça peut vite être lourd à gérer quand on doit surveiller simultanément plusieurs groupes d’équipements qui n’ont rien en commun.

Comme à l’époque je débutais en Python, j’ai décidé de développer mon propre script pour me faire la main.

#!/usr/bin/env python3
#  Copyright 2018 palc.fr
#
#  Licensed under the WTFPL, Version 2
#            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 
# 0. You just DO WHAT THE FUCK YOU WANT TO.

"""
  A trigger status viewer

  This script displays the status of triggers on some devices

  Usage :
      ./triggerstatus.py zabbix_server [list of triggers id]
"""

import os       # To manage cursor and window size
import signal   # To manage Ctrl+C and widnow resizing
import sys      # To manage script arguments and terminal buffer
import time     # To get date and for temporisation

import getpass  # For password input without displaying it
import math     # For calculating graph datas
from pyzabbix import ZabbixAPI

def signal_sigint(sig, frame):
    """
        When receiving SIGINT (Ctrl+c), the script exit proprely
    """
    os.system('setterm -cursor on')
    print()
    exit (0)

def signal_sigwinch(sig, frame):
    """
        When the window is resized, redraw the screen
    """
    draw(ok, notclassified, information, warning, average, high, disaster, disable, date)

def draw(ok, notclassified, information, warning, average, high, disaster, disable, date):
    """
        Clear the screen and redraw all the datas
    """

    os.system('clear')

    # Get screen width and height (in characters)
    rows, columns = os.popen('stty size', 'r').read().split()
    rows=int(rows)
    columns=int(columns)

    # General informations
    sum_triggers=len(disable)+len(ok)+len(notclassified)+len(information)+len(warning)+len(average)+len(high)+len(disaster)
    print(date, end='')
    print(" | " + str(sum_triggers) + " (", end='')
    print("\033[32m" + str(len(ok)) + "\033[0m/", end='')
    print("\033[31m" + str(len(notclassified)+len(information)+len(warning)+len(average)+len(high)+len(disaster)) + "\033[0m/", end='')
    print("\033[90m" + str(len(disable)) + "\033[0m)")

    # Calculate graph width
    coef_graph=1
    if(sum_triggers>(columns-6)):
        coef_graph=(columns-6)/sum_triggers

    # Draw the graph
    print('[', end='')
    for i in (range(0, math.ceil(len(disaster)*coef_graph))):
        print("\033[91m", end='')
        print('|', end='')
    for i in (range(0, math.ceil(len(high)*coef_graph))):
        print("\033[31m", end='')
        print('|', end='')
    for i in (range(0, math.ceil(len(average)*coef_graph))):
        print("\033[93m", end='')
        print('|', end='')
    for i in (range(0, math.ceil(len(warning)*coef_graph))):
        print("\033[33m", end='')
        print('|', end='')
    for i in (range(0, math.ceil(len(information)*coef_graph))):
        print("\033[34m", end='')
        print('|', end='')
    for i in (range(0, math.ceil(len(notclassified)*coef_graph))):
        print("\033[37m", end='')
        print('|', end='')
    for i in (range(0, math.ceil(len(disable)*coef_graph))):
        print("\033[90m", end='')
        print('|', end='')
    for i in (range(0, math.ceil(len(ok)*coef_graph))):
        print("\033[32m", end='')
        print('|', end='')
    print("\033[0m]", end='')

    length=3

    # Display items in PROBLEM state
    for i in disaster:
        print("\033[91m", end='')
        if(length<rows):
            print("\n" + i[:columns], end='')
            length+=1
    for i in high:
        print("\033[31m", end='')
        if(length<rows):
            print("\n" + i[:columns], end='')
            length+=1
    for i in average:
        print("\033[93m", end='')
        if(length<rows):
            print("\n" + i[:columns], end='')
            length+=1
    for i in warning:
        print("\033[33m", end='')
        if(length<rows):
            print("\n" + i[:columns], end='')
            length+=1
    for i in information:
        print("\033[34m", end='')
        if(length<rows):
            print("\n" + i[:columns], end='')
            length+=1
    for i in notclassified:
        print("\033[37m", end='')
        if(length<rows):
            print("\n" + i[:columns], end='')
            length+=1

    # Display disabled items
    print("\033[90m", end='')
    for i in disable:
        if(length<rows):
            print("\n" + i[:columns], end='')
            length+=1

    # Display items in OK state
    print("\033[32m", end='')
    for i in ok:
        if(length<rows):
            print("\n" + i[:columns], end='')
            length+=1

    print("\033[0m", end='')

    sys.stdout.flush()

# When receiving SIGINT (Ctrl+c)
signal.signal(signal.SIGINT, signal_sigint)
# When the window is resized
signal.signal(signal.SIGWINCH, signal_sigwinch)

# Connection to Zabbix server
user = input("Username: ")
password = getpass.getpass("Password for " + user + ": ")
try:
    zapi = ZabbixAPI('https://' + sys.argv[1])
    zapi.login(user, password)
except:
    print("Cannot conect to Zabbix server ☹")
    exit(1)

# Disable cursor
os.system('setterm -cursor off')

# main loop
while 1:
    # Tables for each trigger status
    ok=[]
    notclassified=[]
    information=[]
    warning=[]
    average=[]
    high=[]
    disaster=[]
    disable=[]

    for hostid in sys.argv[2:]:
        # Get triggers status
        try:
            triggers=zapi.trigger.get(hostids=hostid)
        except:
            print("Problem with Zabbix server ☹")
            exit(1)

        for trigger in triggers:
            # Item is in OK state
            if(trigger['status'] == '0' and trigger['value'] == '0'):
                ok.append(hostid + ' ' + trigger['description'])

            # Item is in PROBLEM state
            if(trigger['status'] == '0' and trigger['value'] == '1'):
                if(trigger['priority'] == '0'):
                    notclassified.append(hostid + ' ' + trigger['description'] + ' (not classified)')
                if(trigger['priority'] == '1'):
                    information.append(hostid + ' ' + trigger['description'] + ' (information)')
                if(trigger['priority'] == '2'):
                    warning.append(hostid + ' ' + trigger['description'] + ' (warning)')
                if(trigger['priority'] == '3'):
                    average.append(hostid + ' ' + trigger['description'] + ' (average)')
                if(trigger['priority'] == '4'):
                    high.append(hostid + ' ' + trigger['description'] + ' (high)')
                if(trigger['priority'] == '5'):
                    disaster.append(hostid + ' ' + trigger['description'] + ' (disaster)')

            # Item is disabled
            if(trigger['status'] == '1'):
                disable.append(hostid + ' ' + trigger['description'])

    date=time.strftime("%d/%m/%Y %H:%M:%S")

    draw(ok, notclassified, information, warning, average, high, disaster, disable, date)

    time.sleep(10)

exit(0)
triggerstatus.py

Ce script plusieurs plusieurs paramètres :

  • L’URL du serveur Zabbix (en HTTPS uniquement)
  • la liste des ID des équipements à surveiller, séparés par des espaces

Par exemple, pour superviser les équipements 13111 13112 13123 sur le serveur zabbix.palc.fr :

./triggerstatus.py zabbix.palc.fr 13111 13112 13123

Je me suis clairement inspiré de htop pour faire ce script. Il affiche, dans l’ordre :

  • La date (pour vérifier que le script n‘est pas planté), le nombre total de triggers, et le détail par statut (OK, KO et DISABLED)
  • Une barre affichant
  • Tous les triggers classés, d’abord ceux en KO (classé par ordre de sévérité), puis les DISABLED et enfin les OK

C’est mon tout premier « vrai » script fait en Pyhton (hors Hello word! ou équivalent), donc soyez indulgents.

Zabbix : planifier le lancement d’un item

Zabbix 4 offre quelques possibilités pour planifier le lancement d’un item à un moment précis. Mais avec les versions précédentes, il faut ruser.

Il faut créer l’item avec Update interval (in sec) à 0, pour que l’item ne soit jamais exécuté. Ensuite il faut configurer un Flexible intervals avec un Interval à 60 et une Period de une minute. De cette manière, l’item sera exécuté exactement une seul fois dans l’intervalle de une minute.

Par exemple, pour lancer un item à 10h du lundi au samedi :

Firefox : masquer la barre d’onglets

J’utilise le module Firefox Tree Style Tab, qui permets d’afficher la barre d’onglets sur le côté du navigateur. C’est beaucoup plus simple pour organiser ses onglets. Il est également possible de les organiser de façon arborescente, ce qui est génial si on travaille sur plusieurs trucs en même temps.

Mais sur les versions récentes de Firefox il n’est pas possible de masquer la barre d’onglets en haut de l’écran. Ça fait doublon.

Pour corriger le problème, il faut créer un fichier ~/.mozilla/firefox/XXXXXXXX.default/chrome/userChrome.css (XXXXXXXX est une suite alphanumérique aléatoire) :

#TabsToolbar {
  visibility: collapse;
}

#titlebar {
  visibility: collapse;
}

#TabsToolbar {
  visibility: collapse;
}

Zabbix : faire un export de tous les équipements supervisés et de leur statut

Cette technique est super crade. Je l’ai utilisé pour faire un export en urgence des serveurs supervisés. Elle tient en une seule requête SQL :

SELECT
  hosts.hostid AS ID,
  hosts.host AS Serveur,
  CASE
    WHEN hosts.status=0 THEN 'Activé'
    WHEN hosts.status=1 THEN 'Désactivé'
  END AS Statut,
  CASE
    WHEN hosts.maintenance_status=0 THEN 'Pas en maintenance'
    WHEN hosts.maintenance_status=1 THEN 'En maintenance'
  END AS Maintenance,
  interface.ip AS IP
  FROM hosts, interface
  WHERE hosts.status<>3
  AND hosts.hostid=interface.hostid;

Cette requête fait un export des serveurs contenant :

  • L’ID de l’équipement dans Zabbix
  • Le nom de l’équipement
  • Son status (Activé ou Désactivé)
  • L’état de sa maintenance (En maintenance ou Pas en maintenance)
  • Son adresse IP

La requête est assez simple mais comporte quelques subtilités :

  • Les conditions en utilisant CASE / WHEN / THEN / END, qui permettent de convertir le numéro du status en son texte
  • Les AS qui permettent d’affiche un en-tête de colonne plus clair
  • La condition WHERE hosts.status<>3 qui permets de masquer les templates

FortiClient SSLVPN : se connecter avec un token TOTP (Google Authenticator)

Si comme moi vous êtes sous Linux et vous devez utiliser un VPN FortiGate avec une authentification forte (un token TOTP Google Authenticator), vous devez avoir remarqué un problème avec le client :

Il n’y a pas de champs pour rentrer son token.

La solution est simple, il suffit de rentrer son token dans le champs Password, directement à la suite de votre mot de passe.

Zabbix : acknowledge impossible

Sur un Zabbix 3.4, il n’était pas possible d’acknowledger les alertes. L’erreur était :

Fatal error, please report to the Zabbix team

- Controller: acknowledge.edit
- action: acknowledge.edit
- backurl: zabbix.php%3Faction%3Ddashboard.view
- eventids: Array

Le problème est tout con, et ne provient pas de Zabbix. Dans la configuration Apache j’ai retrouvé les lignes suivantes :

RewriteEngine on
RewriteRule ^/zabbix / [R,L]

Je suppose qu’a une époque l’interface web se trouvait derrière le répertoire /zabbix mais qu’à un moment quelqu’un a voulu simplifier et tout mettre à la racine. Ensuite il a rajouté la redirection pour ne pas casser tous les webservices existants (plusieurs services externes utilisent l’API de ce Zabbix). Le problème c’est que ce faisant il a redirigé tout ce qui commençait par /zabbix, y compris les appels à /zabbix.php. Et, vous l’aurez compris, l’acknowledge fait justement un appel à /zabbix.php.

En regardant les logs Apache j’ai vu que aucun webservice n’appelait /zabbix. J’ai donc tout simplement supprimé la redirection.

Si vous devez conserver la redirection, il devrait être possible de s’en sortir avec une règle du type (attention, je n’ai pas testé) :

RewriteEngine on
RewriteRule ^/zabbix(?:/.*)?$ /$1 [R,L]

Zabbix : un trigger actif seulement à certains moments de la journée

Dans Zabbix (jusqu’à la version 3.4), les maintenances ne peuvent s’appliquer qu’à des équipements entiers. Si on veut que certains trigger, mais pas tous, soient en maintenance à certains moments de la journée, il faut ruser. Il y a deux techniques principales.

Pour un seul trigger

Les fonctions date et dayofweek sont faites pour vous. dayofmonth existe aussi mais est moins utile.

{Template Windows:agent.ping.nodata(5m)}=1 and {Template Windows:agent.ping.time(0)}>090000 and {Template Windows:agent.ping.time(0)}<180000 and {Template Windows:agent.ping.dayofweek(0)}<6

Le trigger ci-dessus contient un test d’agent ping standard. J’ai rajouté deux conditions sur l’heure (.time(0)}>090000 et .time(0)}<180000, pour alerter seulement entre 9h et 18h). J’ai également ajouté une condition sur le jour de la semaine (.dayofweek(0)}<6),pour n’alerte que du lundi au vendredi.

Nous pouvons remarquer que les triggers de type date doivent obligatoirement s’appliquer sur un item, mais que sa valeur ne sert à rien dans l’évaluation de la condition.

Pour un gros groupes de triggers

La technique ci-dessus est assez contraignante si on a beaucoup de triggers. Dans ce cas il est possible de faire plus simple. On créé d’abord un trigger :

{Template Windows:agent.ping.time(0)}<090000 or {Template Windows:agent.ping.time(0)}>180000 or {Template Windows:agent.ping.dayofweek(0)}>4

Sur le même principe que la première technique, ce trigger sera systématiquement en alerte entre 18h et 9h, le samedi et le dimanche. Il faut le mettre en sévérité Information pour qu’il ne remonte pas en MCO et ne dérange pas l’astreinte. Ensuite il n’y a plus qu’à faire une dépendance entre ce trigger et ceux qui devront être en maintenance :

De cette manière, le vrai trigger ne sonnera pas quand le trigger « maintenance » sera en alerte, c’est à dire la nuit et le week-end.

Cette technique est plus efficace pour gérer un grand nombre de triggers sur la même plage de maintenance.

Zabbix : plusieurs seuils sur un seul trigger

Ce type de trigger est particulièrement utile pour superviser l’utilisation de ressources qui peuvent beaucoup varier d’un serveur à l’autre, comme le disque ou la RAM. Le concept est d’avoir un seuil qui s’adapte à la taille de la ressource.

Un exemple concret avec l’utilisation disque. Le but est d’avoir une alerte :

  • à 5% d’utilisation pour une partition de moins de 700 Go
  • à 35 Go pour une partition entre 700 Go et 1300 Go
  • à 50 Go pour une partition de plus de 50 Go

Le trigger sera le suivant :

({Template OS:vfs.fs.size[{#FSNAME},total].last(0)}<700000000000 and {Template OS:vfs.fs.size[{#FSNAME},pfree].max(#3)}<5) or ({Template OS:vfs.fs.size[{#FSNAME},total].last(0)}>=700000000000 and {Template OS:vfs.fs.size[{#FSNAME},total].last(0)}<=1300000000000 and {Template OS:vfs.fs.size[{#FSNAME},free].max(#3)}<35000000000) or ({Template OS:vfs.fs.size[{#FSNAME},total].last(0)}>1300000000000 and {Template OS:vfs.fs.size[{#FSNAME},free].max(#3)}<50000000000)

Ce trigger est un peu complexe et il est difficilement interprétable pour quelqu’un qui n’est pas familier avec Zabbix. Il faudra donc bien le documenter.

Ce trigger se découpe en trois parties principales (les trois seuils différents), elles mêmes découpées en plusieurs sous-parties.

  1. ({Template OS:vfs.fs.size[{#FSNAME},total].last(0)}<700000000000 and {Template OS:vfs.fs.size[{#FSNAME},pfree].last(0)}<5) : Si la taille de la partition est inférieure à 700 Go (la valeur dans le trigger est en octet), alors on sonne si il reste moins de 5% d’espace libre
  2. ({Template OS:vfs.fs.size[{#FSNAME},total].last(0)}>=700000000000 and {Template OS:vfs.fs.size[{#FSNAME},total].last(0)}<=1300000000000 and {Template OS:vfs.fs.size[{#FSNAME},free].last(0)}<35000000000) : Si la taille de la partition est comprise entre 700 Go et 1300 Go, on sonne en dessous de 35 Go d’espace libre
  3. ({Template OS:vfs.fs.size[{#FSNAME},total].last(0)}>1300000000000 and {Template OS:vfs.fs.size[{#FSNAME},free].max(#3)}<50000000000) : Si la taille de la partition est supérieure à 1300 Go, alors on sonne si il reste moins de 50 Go de libre

Il y a deux points sur lesquels il faut faire attention. D’abord il faut s’assurer que toutes les valeurs de taille de partition sont couvertes. Si par exemple je fais disque < X or disque > X, et bien un disque qui fait exactement X ne sera pas pris en compte. Et comme on a toujours tendance à arrondir les tailles de disque et les seuils, ça peut arriver plus vite qu’on ne le pense.

Ensuite, il est possible que ces seuils possèdent des effets de bord qui peuvent être contre intuitifs. Si je prends l’exemple ci-dessus et que je trace le seuil en % en fonction de la taille du disque :

On peut voir un saut à 1300 Go, qui correspond à la limite entre le deuxième et le troisième seuil. A utilisation disque égal, il est donc possible de faire sonner le trigger en augmentant la taille de la partition.