Chapitre 1725 minDébutant
Scripts Python pour l'administration système (SISR)
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- choisir entre Python, PowerShell et Bash selon la tâche ;
- produire un rapport d'inventaire avec contrôles d'adressage ;
- préparer des comptes et analyser un export de l'annuaire ;
- contrôler des sauvegardes avec un code de retour pour la supervision.
2 exercices corrigés
Un administrateur système se sert de Python pour les traitements que les outils natifs font mal : croiser des fichiers, contrôler un inventaire, préparer des données pour un autre outil, produire un rapport, vérifier l'état d'un service avec un code de retour pour la supervision. Ce chapitre commence par le choix entre Python, PowerShell et Bash, puis assemble ce que tu as appris en quatre scripts complets pour le parc de Sorbier Logistique : rapport d'inventaire, préparation des comptes des arrivants, comptes inactifs de l'annuaire et contrôle des sauvegardes.
Python, PowerShell ou Bash ?
Les trois se complètent : on choisit selon la cible et la nature du travail.
| Critère | PowerShell | Bash | Python |
|---|---|---|---|
| Présent d'office | Sur tout Windows et Windows Server | Sur tout Linux | Souvent sur Linux (Debian 13 : 3.13) ; à installer sous Windows |
| Point fort | Active Directory, Windows, Microsoft 365 : des modules officiels | Enchaîner les commandes Linux, manipuler fichiers et services | Traiter des données : CSV, JSON, journaux, calculs, API, tests |
| Ce qui circule | Des objets | Du texte | Des structures (listes, dictionnaires, objets) |
| Portabilité | Windows, et Linux avec PowerShell 7 | Linux, macOS, WSL | Partout, le même script |
| Tests automatisés | Pester | Peu outillé | pytest |
| Taille raisonnable | Quelques centaines de lignes | Quelques dizaines de lignes | Sans limite : modules, paquets |
La règle d'un service informatique comme celui de Sorbier : PowerShell pour agir sur l'annuaire et les serveurs Windows, Bash pour les petites tâches d'un serveur Linux, Python dès que le script calcule, croise ou contrôle des données, ou qu'il doit tourner sur les deux systèmes. Les scripts ci-dessous illustrent la répartition : Python prépare et contrôle, PowerShell exécute dans l'annuaire (créer des comptes en masse avec PowerShell).
Pour pratiquer PowerShell et Bash sur des cas concrets avant de passer à Python, fais le TP Automatiser de petites tâches.
Inventaire du parc depuis un CSV
Le service informatique exporte chaque mois la liste des postes de son outil de gestion de parc (GLPI), au format CSV. Le script en tire un rapport : effectifs par site, garanties expirées ou qui vont expirer, et anomalies d'adressage (adresse absente, hors du réseau du site, ou en double). Les postes du siège sont dans le VLAN 10 POSTES (192.168.10.64/26), ceux de l'entrepôt de Mâcon dans 192.168.20.0/24.
nom;type;service;site;os;achat;fin_garantie;ip
PC-0012;Fixe;Comptabilité;Siège;Windows 11 24H2;2021-03-15;2025-03-15;192.168.10.71
PC-0015;Portable;Logistique;Mâcon;Windows 11 25H2;2024-09-02;2027-09-02;192.168.20.53
PC-0021;Fixe;Logistique;Siège;Windows 11 24H2;2023-01-10;2026-01-10;192.168.10.80
PC-0042;Portable;Direction;Siège;Windows 11 23H2;2020-06-30;2024-06-30;192.168.10.92
PC-0050;Fixe;Logistique;Mâcon;Windows 11 25H2;2025-02-17;2028-02-17;192.168.20.61
PC-0063;Portable;Commercial;Siège;Windows 11 25H2;2025-10-06;2028-10-06;192.168.10.97
PC-0071;Fixe;Comptabilité;Siège;Windows 11 24H2;2023-11-20;2026-11-20;192.168.10.104
PC-0077;Fixe;Logistique;Mâcon;Windows 11 25H2;2025-06-03;2028-06-03;192.168.10.99
PC-0080;Portable;Commercial;Siège;Windows 11 25H2;2026-01-12;2029-01-12;192.168.10.97
PC-0084;Fixe;Logistique;Mâcon;Windows 11 24H2;2023-12-04;2026-12-04;192.168.20.70
PC-0091;Portable;Direction;Siège;Windows 11 25H2;2026-09-21;2029-09-21;
#!/usr/bin/env python3
"""Rapport sur l'inventaire des postes : effectifs, garanties et adresses IP."""
import argparse
import csv
import ipaddress
import sys
from collections import Counter
from datetime import date, timedelta
from pathlib import Path
RESEAUX_POSTES = {
"Siège": ipaddress.ip_network("192.168.10.64/26"), # VLAN 10 POSTES
"Mâcon": ipaddress.ip_network("192.168.20.0/24"), # réseau de l'entrepôt
}
def lire(chemin):
with open(chemin, encoding="utf-8-sig", newline="") as f:
return list(csv.DictReader(f, delimiter=";"))
def garanties(postes, jour, delai_jours=90):
"""Renvoie (expirées, bientôt expirées) au jour donné."""
limite = jour + timedelta(days=delai_jours)
expirees, bientot = [], []
for p in postes:
fin = date.fromisoformat(p["fin_garantie"])
if fin < jour:
expirees.append(p)
elif fin <= limite:
bientot.append(p)
return expirees, bientot
def anomalies_ip(postes):
"""Adresse absente, hors du réseau du site, ou attribuée deux fois."""
problemes = []
vues = {}
for p in postes:
if not p["ip"]:
problemes.append(f"{p['nom']} : pas d'adresse IP")
continue
ip = ipaddress.ip_address(p["ip"])
reseau = RESEAUX_POSTES.get(p["site"])
if reseau and ip not in reseau:
problemes.append(f"{p['nom']} : {ip} hors du réseau du site {p['site']} ({reseau})")
if ip in vues:
problemes.append(f"{p['nom']} : {ip} déjà attribuée à {vues[ip]}")
vues[ip] = p["nom"]
return problemes
def main(arguments=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("csv", type=Path, help="export de l'inventaire (séparateur ;)")
parser.add_argument("--au", type=date.fromisoformat, default=date.today(),
metavar="AAAA-MM-JJ", help="date du rapport (aujourd'hui par défaut)")
args = parser.parse_args(arguments)
postes = lire(args.csv)
print(f"Inventaire au {args.au:%d/%m/%Y} : {len(postes)} postes")
for site, nombre in sorted(Counter(p["site"] for p in postes).items()):
print(f" {site} : {nombre}")
expirees, bientot = garanties(postes, args.au)
print(f"Garantie expirée ({len(expirees)}) : {', '.join(p['nom'] for p in expirees)}")
print(f"Garantie expirant sous 90 jours ({len(bientot)}) : "
+ ", ".join(f"{p['nom']} le {p['fin_garantie']}" for p in bientot))
problemes = anomalies_ip(postes)
print(f"Anomalies d'adressage ({len(problemes)}) :")
for probleme in problemes:
print(" " + probleme)
return 1 if problemes else 0
if __name__ == "__main__":
sys.exit(main())
python inventaire.py inventaire.csv --au 2026-11-08
Inventaire au 08/11/2026 : 11 postes
Mâcon : 4
Siège : 7
Garantie expirée (3) : PC-0012, PC-0021, PC-0042
Garantie expirant sous 90 jours (2) : PC-0071 le 2026-11-20, PC-0084 le 2026-12-04
Anomalies d'adressage (3) :
PC-0077 : 192.168.10.99 hors du réseau du site Mâcon (192.168.20.0/24)
PC-0080 : 192.168.10.97 déjà attribuée à PC-0063
PC-0091 : pas d'adresse IP
Code de retour : 1
type=date.fromisoformat fait lire et valider la date par argparse lui-même : --au 08/11/2026 serait refusé avec un message clair. La date du rapport est un paramètre, aujourd'hui par défaut : le script se teste avec n'importe quelle date. Le code de retour 1 signale des anomalies à une tâche planifiée qui enverrait le rapport par courriel seulement dans ce cas. Les trois anomalies sont typiques d'un inventaire saisi à la main : un poste de Mâcon avec une adresse du siège (déplacé sans mise à jour), une adresse en double (la fiche d'un poste remplacé, recopiée) et un poste neuf pas encore enregistré dans le DHCP.
Préparer les comptes des arrivants
En novembre, les ressources humaines envoient la liste des saisonniers de l'entrepôt, arrivees.csv. Avant que le script PowerShell crée les comptes, le script Python calcule des logins uniques (en tenant compte des comptes existants, exportés de l'annuaire dans logins-existants.txt), vérifie les services et produit un CSV propre, prêt pour l'import :
Prenom;Nom;Service;Fonction;DateFin
Théo;Marchal;Logistique;Préparateur;2027-01-08
Éloïse;Gaillard;Logistique;Préparatrice;2027-01-08
Inès;Roux;Comptabilite;Assistante comptable;
Zoë;N'Diaye;Logistique;Cariste;2027-01-08
Jean-Luc;Dupré;Comptabilite;Comptable;
Hugo;Petit;Marketing;Chargé de communication;
#!/usr/bin/env python3
"""Prépare le CSV d'import des comptes des arrivants : logins uniques, services vérifiés."""
import csv
import re
import sys
import unicodedata
from pathlib import Path
BASE_OU = "OU=Utilisateurs,OU=Sorbier,DC=ad,DC=sorbier,DC=example"
SERVICES = {"Direction", "Comptabilite", "Commercial", "Logistique", "Informatique"}
def login(prenom, nom):
brut = (prenom[:1] + nom).lower().replace("æ", "ae").replace("œ", "oe")
sans_accents = unicodedata.normalize("NFD", brut).encode("ascii", "ignore").decode("ascii")
return re.sub(r"[^a-z0-9]", "", sans_accents)
def login_unique(base, pris):
"""base, puis base2, base3... jusqu'à trouver un login libre."""
candidat, n = base, 1
while candidat in pris:
n += 1
candidat = f"{base}{n}"
return candidat
def main():
pris = set(Path("logins-existants.txt").read_text(encoding="utf-8").split())
with open("arrivees.csv", encoding="utf-8-sig", newline="") as f:
arrivees = list(csv.DictReader(f, delimiter=";"))
a_creer, refus = [], []
for a in arrivees:
if a["Service"] not in SERVICES:
refus.append(f"{a['Prenom']} {a['Nom']} : service inconnu « {a['Service']} »")
continue
identifiant = login_unique(login(a["Prenom"], a["Nom"]), pris)
pris.add(identifiant) # deux arrivants homonymes : logins différents
a_creer.append({"Prenom": a["Prenom"], "Nom": a["Nom"], "Login": identifiant,
"OU": f"OU={a['Service']},{BASE_OU}", "DateFin": a["DateFin"]})
with open("comptes-a-creer.csv", "w", encoding="utf-8", newline="") as f:
ecrivain = csv.DictWriter(f, fieldnames=["Prenom", "Nom", "Login", "OU", "DateFin"], delimiter=";")
ecrivain.writeheader()
ecrivain.writerows(a_creer)
for c in a_creer:
print(f"{c['Login']:<12}{c['Prenom']} {c['Nom']}")
for r in refus:
print("REFUSÉ :", r, file=sys.stderr)
return 1 if refus else 0
if __name__ == "__main__":
sys.exit(main())
tmarchal Théo Marchal
egaillard Éloïse Gaillard
iroux2 Inès Roux
zndiaye Zoë N'Diaye
jdupre Jean-Luc Dupré
REFUSÉ : Hugo Petit : service inconnu « Marketing »
Code de retour : 1
Les trois premières lignes du fichier comptes-a-creer.csv produit :
Prenom;Nom;Login;OU;DateFin
Théo;Marchal;tmarchal;OU=Logistique,OU=Utilisateurs,OU=Sorbier,DC=ad,DC=sorbier,DC=example;2027-01-08
Éloïse;Gaillard;egaillard;OU=Logistique,OU=Utilisateurs,OU=Sorbier,DC=ad,DC=sorbier,DC=example;2027-01-08
La fonction login() est celle testée au chapitre 15, qui applique la même règle que le script PowerShell du cours de scripting : les identifiants sont identiques quel que soit l'outil. Inès Roux reçoit iroux2, car une homonyme du service commercial a déjà iroux. Le service « Marketing » n'existe pas dans l'annuaire : la ligne est refusée sur la sortie d'erreur, et le code de retour 1 oblige à la traiter, plutôt que de créer un compte dans une unité d'organisation inexistante.
Comptes inactifs dans un export de l'annuaire
Un compte actif dont personne ne se sert est une porte ouverte : ancien stagiaire, saisonnier parti, compte créé « au cas où ». Le module PowerShell ActiveDirectory exporte les comptes avec leur dernière connexion ; Python fait l'analyse. L'export, avec la date au format ISO pour qu'elle se lise sans ambiguïté :
Get-ADUser -Filter * -Properties LastLogonDate, Department |
Select-Object SamAccountName, Enabled, Department,
@{ Name = 'LastLogon'; Expression = { if ($_.LastLogonDate) { $_.LastLogonDate.ToString('s') } } } |
Export-Csv -Path comptes-ad.csv -Delimiter ';' -Encoding utf8 -UseQuotes AsNeeded
SamAccountName;Enabled;Department;LastLogon
amercier;True;Informatique;2026-11-08T07:41:12
clambert;True;Informatique;2026-11-08T07:58:31
mdiallo;True;Informatique;2026-11-07T17:02:45
iroux;True;Commercial;2026-11-06T08:12:09
aguerin;True;Comptabilite;2026-07-17T16:45:00
nvidal;True;Logistique;2026-05-28T06:30:12
rblanc;False;Comptabilite;2026-02-13T11:20:41
kmorel;True;Comptabilite;2026-10-30T08:55:02
saisonnier01;True;Logistique;
stage.web;True;Commercial;2026-08-01T09:00:00
svc-sauvegarde;True;Informatique;2026-11-08T02:30:04
#!/usr/bin/env python3
"""Comptes actifs de l'annuaire sans connexion depuis N jours, d'après un export CSV."""
import csv
from datetime import datetime, timedelta
JOUR_DU_RAPPORT = datetime(2026, 11, 8, 9, 0) # datetime.now() en production
SEUIL = timedelta(days=90)
with open("comptes-ad.csv", encoding="utf-8-sig", newline="") as f:
comptes = list(csv.DictReader(f, delimiter=";"))
jamais, inactifs = [], []
for c in comptes:
if c["Enabled"] != "True": # déjà désactivé : rien à faire
continue
if not c["LastLogon"]:
jamais.append(c["SamAccountName"])
continue
derniere = datetime.fromisoformat(c["LastLogon"])
absence = JOUR_DU_RAPPORT - derniere
if absence > SEUIL:
inactifs.append((absence.days, c["SamAccountName"], c["Department"]))
print(f"{len(comptes)} comptes, rapport du {JOUR_DU_RAPPORT:%d/%m/%Y}")
print("Jamais connectés :", ", ".join(jamais) or "aucun")
print(f"Actifs sans connexion depuis plus de {SEUIL.days} jours :")
for jours, login, service in sorted(inactifs, reverse=True):
print(f" {login:<12}{service:<14}{jours} jours")
11 comptes, rapport du 08/11/2026
Jamais connectés : saisonnier01
Actifs sans connexion depuis plus de 90 jours :
nvidal Logistique 164 jours
aguerin Comptabilite 113 jours
stage.web Commercial 99 jours
datetime.fromisoformat() lit directement le format 2026-11-08T07:58:31 produit par .ToString('s'). Sans cette conversion, Export-Csv écrirait la date selon la langue du serveur (08/11/2026 07:58:31 en français), ambiguë et plus difficile à lire. L'option -UseQuotes AsNeeded n'existe que dans PowerShell 7 ; Windows PowerShell 5.1 entoure chaque valeur de guillemets, ce que le module csv de Python gère sans rien changer.
Contrôler l'âge des sauvegardes
Une sauvegarde qui ne tourne plus depuis trois jours ne prévient personne. Ce script vérifie, pour chaque application, que la sauvegarde la plus récente a moins de 26 heures, et le dit par son code de retour à la manière des sondes de supervision : 0 OK, 1 alerte, 2 critique.
#!/usr/bin/env python3
"""Contrôle de la fraîcheur des sauvegardes : code de retour pour la supervision."""
import sys
from datetime import datetime, timedelta
from pathlib import Path
MAINTENANT = datetime(2026, 11, 8, 9, 0) # datetime.now() en production
AGE_MAXIMUM = timedelta(hours=26) # une sauvegarde par nuit, et 2 h de marge
def derniere_sauvegarde(dossier, prefixe):
"""Date de la sauvegarde la plus récente, lue dans les noms prefixe-AAAA-MM-JJ_HHMMSS.zip."""
dates = []
for archive in Path(dossier).glob(f"{prefixe}-*.zip"):
horodatage = archive.stem.removeprefix(f"{prefixe}-")
dates.append(datetime.strptime(horodatage, "%Y-%m-%d_%H%M%S"))
return max(dates, default=None)
etat = 0
for prefixe in ["partages", "glpi", "intranet"]:
derniere = derniere_sauvegarde("sauvegardes", prefixe)
if derniere is None:
print(f"CRITIQUE {prefixe} : aucune sauvegarde trouvée")
etat = 2
elif MAINTENANT - derniere > AGE_MAXIMUM:
heures = (MAINTENANT - derniere) // timedelta(hours=1)
print(f"ALERTE {prefixe} : dernière sauvegarde il y a {heures} h ({derniere:%d/%m %H:%M})")
etat = max(etat, 1)
else:
print(f"OK {prefixe} : {derniere:%d/%m %H:%M}")
sys.exit(etat)
OK partages : 08/11 02:30
ALERTE glpi : dernière sauvegarde il y a 55 h (06/11 01:30)
CRITIQUE intranet : aucune sauvegarde trouvée
Code de retour : 2
L'âge vient de la date inscrite dans le nom des archives (le format du script de sauvegarde), pas de la date de modification du fichier, qu'une copie ou une restauration peut changer. max(dates, default=None) renvoie None si aucune archive n'existe, au lieu de lever une erreur. Diviser un timedelta par timedelta(hours=1) avec // donne un nombre entier d'heures. Les codes 0, 1 et 2 suivent la convention des sondes de Nagios et de Centreon ; avec Zabbix, l'agent de srv-journaux récupère plutôt la sortie du script par un paramètre utilisateur, et un déclencheur alerte selon son contenu (cours Supervision).
Exercices
Exercice 1 sur 2 Le bon outil Réussi À revoir
Pour chaque tâche, choisis PowerShell, Bash ou Python et justifie en une phrase.
- Ajouter les 40 saisonniers au groupe
GG_Logisticiensde l'annuaire. - Comparer l'inventaire du parc et l'export des comptes de l'annuaire pour trouver les postes sans utilisateur.
- Redémarrer le service Nginx de
srv-websi la page d'accueil ne répond plus. - Interroger chaque nuit l'API de l'outil de gestion de parc et produire un rapport des garanties.
- Archiver chaque semaine les journaux d'un serveur Debian.
Voir la réponseMasquer la réponse de l'exercice 1
- PowerShell : le module
ActiveDirectoryle fait en une commande (Add-ADGroupMember). - Python : croiser deux fichiers est un travail de données, fait en quelques lignes avec deux ensembles et leur différence.
- Bash : sur le serveur Linux,
curletsystemctl restart nginxsuffisent dans un petit script lancé par un timer systemd. - Python :
requests, la pagination, le JSON et le rapport, avec des tests ; PowerShell (Invoke-RestMethod) se défend aussi si le rapport reste simple. - Bash :
taretfindsur le serveur lui-même, sans rien installer ; Python si l'archivage doit vérifier l'archive et prévenir par code de retour, comme au chapitre 9.
Tu avais trouvé ?
Exercice 2 sur 2 La liste des comptes à désactiver Réussi À revoir
À partir de comptes-ad.csv, écris dans a-desactiver.csv (colonnes SamAccountName et Motif) les comptes actifs jamais connectés ou inactifs depuis plus de 90 jours, en excluant le compte de service svc-sauvegarde. Ce fichier sera lu par un script PowerShell qui fera Disable-ADAccount après validation par le responsable.
Voir la réponseMasquer la réponse de l'exercice 2
# ex_desactivation.py : produire la liste des comptes à désactiver, pour un script PowerShell
import csv
from datetime import datetime, timedelta
from pathlib import Path
JOUR_DU_RAPPORT = datetime(2026, 11, 8, 9, 0)
SEUIL = timedelta(days=90)
EXCLUS = {"svc-sauvegarde"} # comptes de service : jamais désactivés par ce script
with open("comptes-ad.csv", encoding="utf-8-sig", newline="") as f:
comptes = list(csv.DictReader(f, delimiter=";"))
a_desactiver = []
for c in comptes:
if c["Enabled"] != "True" or c["SamAccountName"] in EXCLUS:
continue
jamais = not c["LastLogon"]
if jamais or JOUR_DU_RAPPORT - datetime.fromisoformat(c["LastLogon"]) > SEUIL:
a_desactiver.append({"SamAccountName": c["SamAccountName"],
"Motif": "jamais connecté" if jamais else "inactif depuis plus de 90 jours"})
with open("a-desactiver.csv", "w", encoding="utf-8", newline="") as f:
ecrivain = csv.DictWriter(f, fieldnames=["SamAccountName", "Motif"], delimiter=";")
ecrivain.writeheader()
ecrivain.writerows(a_desactiver)
print(Path("a-desactiver.csv").read_text(encoding="utf-8"), end="")
SamAccountName;Motif
aguerin;inactif depuis plus de 90 jours
nvidal;inactif depuis plus de 90 jours
saisonnier01;jamais connecté
stage.web;inactif depuis plus de 90 jours
Les comptes de service se connectent par des mécanismes que lastLogonTimestamp ne reflète pas toujours ; on les exclut explicitement, plutôt que de les désactiver par erreur et d'arrêter les sauvegardes. Le motif rend la liste vérifiable par un humain avant l'action : un script d'administration qui désactive sans validation ni journal fait plus de dégâts qu'il n'en évite. Côté PowerShell, Import-Csv a-desactiver.csv -Delimiter ';' | ForEach-Object { Disable-ADAccount -Identity $_.SamAccountName -WhatIf } montre d'abord ce qui serait fait.
Tu avais trouvé ?