Python : jouons avec coding

En lisant l’article Un header d’encoding plus simple pour Python et ses commentaires, j’ai voulu voir jusqu’où on pouvait aller dans la déclaration coding en Python 3.

Je commence par créer le script suivant :

#!/usr/bin/python3
# coding: Latin-1
print('é')

Comme Python 3 est par défaut en UTF-8, je créé suis obligé d’utiliser un autre coding pour voir si c’est pris en compte. Je créé donc le script en UTF-8 mais je déclare un coding en Latin-1. De cette manière, si le coding fonctionne ça affichera é, sinon ça affichera é.

J’aurais pu créer un fichier source en Latin-1, mais si le coding ne fonctionne pas ça me retourne une erreur :

SyntaxError: (unicode error) 'utf-8' codec can't decode byte 0xe9 in position 0: unexpected end of data

Je trouve ça beaucoup moins propre.

J’ai aussi choisi de ne pas utiliser le shebang préconisé (#!/usr/bin/env python3). Ça sera utile plus tard.

Je lance donc le script, et le résultat correspond à mes attentes :

$ ./coding.py
é

Le coding est bien pris en compte.

 

Maintenant je vais tester quelques exemples dans les commentaire de l’article. Je commence par celui de Biganon :

#!/usr/bin/python3
# Bonjour, je voudrais utiliser cet encoding: Latin-1 ; et sinon, la famille ça va ?
print('é')
$ ./coding.py
é

Parfait, ça marche.

 

Ensuite je passe à haypo :

#!/usr/bin/python3
# cocoricoding: Latin-1, l’encoding bien français
print('é')
$ ./coding.py
é

Ça marche aussi. On peut donc bien mettre des caractères non-ASCII sur la ligne qui déclare l’encodage.

 

D’après mgautierfr et Sam, la regex permettant de détecter le coding est coding[:=]\s*([-\w.]+). Elle est testée uniquement sur les deux premières lignes du fichier. Voyons ce qu’il est possible de faire avec ça.

 

#!/usr/bin/python3
import os # coding: Latin-1
print('é')
./coding.py
é

Le coding n’est pas pris en compte s’il y a une instruction avant sur la ligne. Dommage, j’aurais bien aimé pouvoir changer l’encodage au milieu d’un script.

 

#!/usr/bin/python3
print('coding:Latin-1')
print('é')
$ ./coding.py
coding:Latin-1
é

Si le coding fait partie de l’instruction, ça ne marche pas non plus.

 

#!/usr/bin/python3
""" coding: Latin-1 """
print('é')
$ ./coding.py
é

Si le coding est entre triple quotes, ça ne fonctionne pas non plus. Si j’ai bien compris la doc (c’est pas garanti), les triples quotes sont considérées comme des instructions par Python. Il est donc normal que ça ne fonctionne pas.

 

#!/usr/bin/python3
# coding: Latin-1 # coding: UTF-8
print('é')
$ ./coding.py
é

S’il y a plusieurs coding, c’est le premier qui est pris en compte.

 

#!/usr/bin/python3 # coding: Latin-1
print('é')
$ ./coding.py
/usr/bin/python3: can't open file '# coding: Latin-1': [Errno 2] No such file or directory

On ne peut pas mettre le coding sur la même ligne que le shebang. Mais ça a l’air d’être une limitation de Bash. Si je garde le même script mais que je le lance directement avec python3 :

$ python3 ./coding.py
é

Là ça marche.

 

Puisque le shebang est géré par Bash et le coding par Python, il est possible de faire des choses sympa. Je commence par créer un lien symbolique /usr/bin/coding:Latin-1 qui pointe vers /usr/bin/python3 :

sudo ln -s /usr/bin/python3 /usr/bin/coding:Latin-1

Ensuite je créé le script suivant :

#!/usr/bin/coding:Latin-1
print('é')
$ ./coding.py
é

Et voilà, en une seule ligne tout le monde est content ! Bash a pu lancer Python via le lien symbolique coding:Latin-1, et Python a trouvé son coding sur la première ligne du script.

 

#!/usr/bin/coding:Latin-1
# coding: UTF-8
print('é')
$ ./coding.py
é

S’il y a deux lignes coding, c’est la première qui est prise en compte.

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.