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
- Bash ou Python : décider
- Squelette de script robuste
- Manipuler des fichiers et chemins
- Lancer des commandes shell
- Parser des logs
- Appels API et JSON
- Notifications email et webhook
- Planification et résilience
- 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)
- 👉 Python pour PME : guide pratique data, scripting, automatisation (pillar)
- 👉 Python pandas : traiter Excel et CSV en pratique
- 👉 Python mini-API avec FastAPI
Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.