ITSkillsCenter
Blog

Python pour scripting admin système : guide pratique

12 min de lecture

Lecture : 11 minutes · Niveau : intermédiaire · Mise à jour : avril 2026

Sur un serveur Linux, Bash règne pour les scripts simples. Mais dès qu’il faut manipuler du JSON, parser des logs structurés, faire des appels API, ou gérer une logique conditionnelle ramifiée, Python prend le relais avec beaucoup plus de robustesse. Ce guide rassemble les patterns vraiment utiles pour un sysadmin qui veut automatiser proprement.

Voir aussi → Python pour PME : guide pratique et Linux administration avancée.


Sommaire

  1. Bash ou Python : décider
  2. Squelette de script robuste
  3. Manipuler des fichiers et chemins
  4. Lancer des commandes shell
  5. Parser des logs
  6. Appels API et JSON
  7. Notifications email et webhook
  8. Planification et résilience
  9. FAQ

1. Bash ou Python : décider

Pour un script ad-hoc d’orchestration de commandes Unix qui tient en 20 lignes : Bash. Pour tout le reste : Python.

Signaux qui indiquent qu’il faut passer en Python :

  • Manipulation de JSON ou XML
  • Logique conditionnelle avec plus de 3 branches
  • Calculs sur des dates, durées, nombres
  • Appels HTTP avec parsing de réponses
  • Lecture de fichiers structurés (CSV, Excel, YAML)
  • Gestion d’erreurs propre avec retry, logging, alerting
  • Script qui sera maintenu et étendu sur plusieurs mois

Le coût de réécriture d’un Bash devenu illisible vers Python est élevé. Mieux vaut commencer en Python si on suspecte que le script va grandir.


2. Squelette de script robuste

Tout script d’admin sérieux mérite ce squelette de base :

#!/usr/bin/env python3
"""Description courte du script.

Usage:
    python script.py [options]
"""
import logging
import sys
from pathlib import Path

# Configuration logging vers stdout + fichier
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("/var/log/mon-script.log"),
        logging.StreamHandler(sys.stdout),
    ],
)
log = logging.getLogger(__name__)


def main() -> int:
    log.info("Démarrage")
    try:
        do_work()
        log.info("Terminé avec succès")
        return 0
    except Exception:
        log.exception("Erreur fatale")
        return 1


def do_work() -> None:
    # Logique principale
    pass


if __name__ == "__main__":
    sys.exit(main())

Quelques points clés :
– Shebang #!/usr/bin/env python3 pour exécution directe (./script.py)
– Logging plutôt que print : niveaux contrôlables, sortie multi-destination
– Code de retour explicite (0 succès, non-zéro échec) pour systemd / cron
log.exception capture la stack trace en plus du message

Arguments de ligne de commande

import argparse

parser = argparse.ArgumentParser(description="Backup quotidien")
parser.add_argument("--dry-run", action="store_true", help="Simulation sans action réelle")
parser.add_argument("--target", default="/var/backups", help="Répertoire de destination")
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose")
args = parser.parse_args()

if args.verbose:
    log.setLevel(logging.DEBUG)

argparse est dans la bibliothèque standard, pas besoin d’installer. Génère aussi le --help automatique.


3. Manipuler des fichiers et chemins

pathlib est l’outil moderne pour les chemins, beaucoup plus lisible que les manipulations de strings.

from pathlib import Path

racine = Path("/var/data")

# Lire et écrire
texte = (racine / "config.txt").read_text(encoding="utf-8")
(racine / "output.txt").write_text(texte.upper())

# Itérer
for fichier in racine.glob("*.log"):
    if fichier.stat().st_size > 100_000_000:
        log.warning(f"{fichier} > 100 Mo")

# Récursif
for fichier in racine.rglob("*.tmp"):
    fichier.unlink()  # supprimer

# Métadonnées
fichier = racine / "data.csv"
print(fichier.exists())
print(fichier.is_file())
print(fichier.stat().st_size)
print(fichier.stat().st_mtime)  # timestamp modification

# Créer arborescence
(racine / "archives" / "2026").mkdir(parents=True, exist_ok=True)

Compression et archivage

import shutil
import gzip
import tarfile

# Compresser un fichier
with open("data.csv", "rb") as src, gzip.open("data.csv.gz", "wb") as dst:
    shutil.copyfileobj(src, dst)

# Archive tar.gz d'un dossier
with tarfile.open("backup.tar.gz", "w:gz") as tar:
    tar.add("/var/data", arcname="data")

Espace disque

import shutil

stat = shutil.disk_usage("/")
print(f"Libre: {stat.free / 1e9:.1f} Go / Total: {stat.total / 1e9:.1f} Go")

if stat.free < 5e9:  # moins de 5 Go libres
    log.warning("Espace disque critique")

4. Lancer des commandes shell

subprocess.run est l’API standard et sûre pour exécuter des commandes externes.

import subprocess

# Capturer la sortie
resultat = subprocess.run(
    ["systemctl", "is-active", "nginx"],
    capture_output=True,
    text=True,
    timeout=10,
)

if resultat.returncode == 0:
    log.info(f"nginx actif: {resultat.stdout.strip()}")
else:
    log.error(f"nginx KO: {resultat.stderr}")

# Vérifier et planter en cas d'échec
subprocess.run(["systemctl", "restart", "nginx"], check=True)

# Sans capturer (sortie directe au terminal)
subprocess.run(["du", "-sh", "/var/log"], check=True)

Pourquoi shell=False par défaut

# Mauvais : injection possible si user_input vient d'un user
user_input = "; rm -rf /"
subprocess.run(f"ls {user_input}", shell=True)  # DANGER

# Bon
subprocess.run(["ls", user_input])  # user_input traité comme argument

Sauf cas très spécifique (chaînage de pipes), éviter shell=True.

Streaming de sortie en temps réel

Pour des commandes longues, lire la sortie au fur et à mesure :

process = subprocess.Popen(
    ["rsync", "-av", "/source/", "/dest/"],
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
)
for ligne in process.stdout:
    log.info(ligne.rstrip())
process.wait()

5. Parser des logs

Cas typique : extraire des informations utiles depuis un log volumineux.

import re
from collections import Counter
from pathlib import Path

# Parser un access.log Nginx
PATTERN = re.compile(
    r'(?P<ip>\S+) \S+ \S+ \[(?P<date>[^\]]+)\] '
    r'"(?P<method>\S+) (?P<path>\S+)[^"]*" '
    r'(?P<status>\d+) (?P<size>\d+)'
)

ips = Counter()
paths_404 = Counter()

with Path("/var/log/nginx/access.log").open() as f:
    for ligne in f:
        m = PATTERN.match(ligne)
        if not m:
            continue
        ips[m["ip"]] += 1
        if m["status"] == "404":
            paths_404[m["path"]] += 1

print("Top 10 IPs:")
for ip, count in ips.most_common(10):
    print(f"  {count:6d}  {ip}")

print("\nTop 10 paths 404:")
for path, count in paths_404.most_common(10):
    print(f"  {count:6d}  {path}")

Lire des fichiers compressés

import gzip

with gzip.open("/var/log/nginx/access.log.1.gz", "rt") as f:
    for ligne in f:
        ...

Logs JSON modernes

Beaucoup d’applications loggent en JSON ligne par ligne. Plus simple à parser :

import json

with open("/var/log/app.json") as f:
    for ligne in f:
        evt = json.loads(ligne)
        if evt.get("level") == "ERROR":
            print(evt["timestamp"], evt["message"])

6. Appels API et JSON

import requests

# GET avec timeout (toujours mettre un timeout !)
r = requests.get(
    "https://api.example.com/serveurs",
    headers={"Authorization": f"Bearer {TOKEN}"},
    timeout=15,
)
r.raise_for_status()  # plante si status >= 400
serveurs = r.json()

# POST JSON
r = requests.post(
    "https://api.example.com/incident",
    json={"titre": "CPU saturé", "host": "web-1"},
    headers={"Authorization": f"Bearer {TOKEN}"},
    timeout=15,
)
r.raise_for_status()

# Retry sur erreurs réseau
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

retry_strategy = Retry(
    total=3,
    backoff_factor=2,
    status_forcelist=[429, 500, 502, 503, 504],
)
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry_strategy))
r = session.get("https://api.example.com/data", timeout=15)

Lire/écrire YAML

import yaml

with open("config.yml") as f:
    config = yaml.safe_load(f)

# Toujours yaml.safe_load (pas yaml.load) pour éviter exécution arbitraire

7. Notifications email et webhook

Email simple

import smtplib
from email.message import EmailMessage

def alerter(sujet: str, corps: str, destinataire: str = "admin@exemple.com"):
    msg = EmailMessage()
    msg["From"] = "alerts@exemple.com"
    msg["To"] = destinataire
    msg["Subject"] = sujet
    msg.set_content(corps)
    with smtplib.SMTP_SSL("smtp.exemple.com", 465) as smtp:
        smtp.login("alerts@exemple.com", os.environ["SMTP_PASS"])
        smtp.send_message(msg)

Webhook Slack ou Discord

import requests

def notifier_slack(message: str, channel: str = "#alertes"):
    requests.post(
        os.environ["SLACK_WEBHOOK"],
        json={"text": message, "channel": channel},
        timeout=10,
    )

Telegram via bot

def notifier_telegram(message: str):
    requests.post(
        f"https://api.telegram.org/bot{os.environ['TELEGRAM_TOKEN']}/sendMessage",
        json={"chat_id": os.environ["TELEGRAM_CHAT_ID"], "text": message},
        timeout=10,
    )

8. Planification et résilience

Cron classique

# /etc/cron.d/mon-script
0 2 * * * sysadmin /opt/scripts/backup.py >> /var/log/backup.log 2>&1

systemd timer (préférable)

Voir systemd services tutoriel pour la config détaillée.

Verrou pour éviter exécutions parallèles

Si un script peut être déclenché plusieurs fois et qu’on ne veut pas qu’il tourne en double :

import fcntl
import sys

LOCK_FILE = "/tmp/mon-script.lock"
lock = open(LOCK_FILE, "w")
try:
    fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
    log.warning("Une autre instance tourne déjà, arrêt")
    sys.exit(0)

Idempotence

Un script d’admin doit pouvoir être relancé sans casser quoi que ce soit. Avant de créer une ressource, vérifier qu’elle n’existe pas. Avant de modifier un fichier, comparer le contenu attendu et actuel. Cette discipline évite les surprises lors d’une relance après échec partiel.


9. FAQ

Pourquoi pas Fabric ou Ansible plutôt qu’un script Python ?

Pour de l’orchestration multi-serveurs, Ansible est mieux adapté (voir Terraform Ansible PME). Pour des opérations locales sur un seul serveur ou des scripts ponctuels, Python pur est plus simple et plus flexible. Les deux coexistent dans les équipes matures.

Puis-je remplacer tous mes scripts Bash par Python ?

Pas tout, mais beaucoup. Pour un Bash de moins de 20 lignes qui orchestre quelques commandes Unix : ne pas réécrire. Au-delà, le bénéfice de la réécriture en Python (lisibilité, maintenabilité, testabilité) justifie le coût. Faire le tri en fonction de la fréquence de modification du script.

Comment gérer les secrets dans un script Python ?

Variables d’environnement chargées depuis un fichier protégé en chmod 600, ou un gestionnaire de secrets externe (Vault, sops). Jamais dans le code source. Pour des cas simples, le module python-dotenv charge un fichier .env automatiquement.

Ce script doit envoyer une alerte uniquement la première fois, comment ?

Stocker un état (fichier sur disque, base SQLite, Redis) et comparer avant alerter. Pattern classique :

state_file = Path("/var/lib/mon-script/last_alert.txt")
last_alert = state_file.read_text() if state_file.exists() else ""
if condition_alerte and last_alert != "alerté":
    alerter(...)
    state_file.write_text("alerté")
elif not condition_alerte:
    state_file.write_text("normal")

Comment tester un script qui appelle des commandes système ?

Trois approches : tests unitaires avec unittest.mock pour mocker subprocess.run, tests d’intégration dans un conteneur Docker isolé, ou un mode --dry-run qui logge les actions au lieu de les exécuter. Le mode dry-run est souvent le plus pragmatique pour des scripts d’admin.

Mon script doit gérer la rotation de logs, comment ?

logging.handlers.RotatingFileHandler ou TimedRotatingFileHandler rotent automatiquement. Ou laisser logrotate (outil Linux standard) gérer la rotation au niveau système, configuré via /etc/logrotate.d/. La seconde approche est plus simple et plus standard.


Articles liés (cluster Python pour PME)


Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité