---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Cron et automatisation

```{code-cell} python
:tags: [hide-input]

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
```

L'automatisation des tâches récurrentes est l'un des usages les plus précieux de l'administration système. Sauvegardes nocturnes, rapports hebdomadaires, nettoyage de fichiers temporaires, envoi de notifications, synchronisation de bases de données — ces tâches seraient fastidieuses à exécuter manuellement et cruciales à ne pas oublier. Ce chapitre présente les deux mécanismes principaux de planification sous Linux : `cron`, le planificateur historique, et les timers `systemd`, son successeur moderne.

## Architecture de `cron`

`cron` est un démon Unix qui s'exécute en arrière-plan et surveille en permanence une base de données de tâches planifiées. Son architecture repose sur plusieurs composants distincts.

```{prf:definition} Le démon `crond`
:label: definition-18-01
`crond` (ou `cron` selon les distributions) est le **processus démon** qui tourne en permanence en arrière-plan. Chaque minute, il examine les tables de planification et lance les commandes dont l'heure d'exécution est atteinte. Il est généralement démarré au boot via le système d'init (SysV init, Upstart ou systemd).
```

### Les différentes tables de planification

`cron` lit les tâches planifiées depuis plusieurs emplacements :

**Les crontabs utilisateurs** : chaque utilisateur du système peut avoir sa propre crontab, gérée via `crontab -e`. Ces fichiers sont stockés dans `/var/spool/cron/crontabs/` (Debian/Ubuntu) ou `/var/spool/cron/` (Red Hat/CentOS). Les tâches s'exécutent avec les permissions de l'utilisateur propriétaire.

**La crontab système** : le fichier `/etc/crontab` est une crontab système qui contient un champ supplémentaire — le nom d'utilisateur sous lequel la commande doit s'exécuter.

**Le répertoire `/etc/cron.d/`** : les applications et paquets installent leurs planifications dans ce répertoire sous forme de fichiers distincts. Chaque fichier suit le même format que `/etc/crontab` (avec le champ utilisateur).

**Les répertoires périodiques** : `/etc/cron.hourly/`, `/etc/cron.daily/`, `/etc/cron.weekly/` et `/etc/cron.monthly/` contiennent des scripts shell qui seront exécutés automatiquement à la fréquence indiquée par leur répertoire. Il suffit de déposer un script exécutable dans ces répertoires sans écrire de syntaxe crontab.

```bash
# Structure des répertoires cron
ls -la /etc/cron.daily/
# logrotate   apt-compat   dpkg   man-db   ...
```

## Syntaxe crontab

La syntaxe d'une entrée crontab est constituée de cinq champs de planification suivis de la commande à exécuter.

```{prf:definition} Format d'une entrée crontab
:label: definition-18-02
Une entrée crontab standard a la forme :

```
minute heure jour-du-mois mois jour-de-la-semaine commande
```

Les cinq champs de planification acceptent les valeurs suivantes :

| Champ | Valeur | Plage |
|:------|:-------|:------|
| minute | 0–59 | — |
| heure | 0–23 | — |
| jour-du-mois | 1–31 | — |
| mois | 1–12 ou jan–dec | — |
| jour-de-la-semaine | 0–7 (0 et 7 = dimanche) ou sun–sat | — |
```

### Opérateurs de planification

```{prf:example} Opérateurs dans les champs crontab
:label: example-18-01
| Opérateur | Signification | Exemple |
|:---:|:---|:---|
| `*` | N'importe quelle valeur | `* * * * *` = chaque minute |
| `,` | Liste de valeurs | `0,15,30,45` = toutes les 15 min |
| `-` | Plage de valeurs | `9-17` = de 9h à 17h |
| `/` | Pas (every) | `*/5` = toutes les 5 unités |
| `L` | Dernier (certains crons) | `L` dans jour = dernier jour du mois |
| `@reboot` | Au démarrage | `@reboot commande` |
| `@hourly` | Toutes les heures | équivalent `0 * * * *` |
| `@daily` | Tous les jours | équivalent `0 0 * * *` |
| `@weekly` | Toutes les semaines | équivalent `0 0 * * 0` |
| `@monthly` | Tous les mois | équivalent `0 0 1 * *` |
| `@yearly` | Tous les ans | équivalent `0 0 1 1 *` |
```

```bash
# Exemples commentés de crontab

# Chaque minute (pour tests — à ne pas laisser en production)
* * * * * /usr/local/bin/mon_script.sh

# Tous les jours à 2h30 du matin
30 2 * * * /usr/local/bin/sauvegarde.sh

# Tous les lundis à 8h00
0 8 * * 1 /usr/local/bin/rapport_hebdo.sh

# Le 1er de chaque mois à minuit
0 0 1 * * /usr/local/bin/archivage_mensuel.sh

# Toutes les 15 minutes entre 9h et 18h, du lundi au vendredi
*/15 9-18 * * 1-5 /usr/local/bin/sync_donnees.sh

# Le 15 et le dernier jour du mois à 3h
0 3 15,L * * /usr/local/bin/facturation.sh

# Au démarrage du système
@reboot /usr/local/bin/initialiser_cache.sh

# Tous les jours à minuit (alias)
@daily /usr/local/bin/nettoyage.sh
```

## Gestion des crontabs

```{prf:definition} Commandes de gestion des crontabs
:label: definition-18-03
- `crontab -e` : ouvre la crontab de l'utilisateur courant dans l'éditeur défini par `EDITOR` ou `VISUAL`.
- `crontab -l` : liste le contenu de la crontab courante.
- `crontab -r` : supprime la crontab courante (**attention : irréversible sans sauvegarde**).
- `crontab -u utilisateur -e` : modifie la crontab d'un autre utilisateur (requiert les droits root).
- `crontab fichier.crontab` : remplace la crontab courante par le contenu du fichier.
```

```bash
# Sauvegarder la crontab avant de la modifier
crontab -l > ~/sauvegarde_crontab_$(date +%Y%m%d).txt

# Modifier la crontab
crontab -e

# Restaurer depuis une sauvegarde
crontab ~/sauvegarde_crontab_20240315.txt

# Vérifier la crontab d'un autre utilisateur (en root)
crontab -u www-data -l
```

## Variables dans crontab

La crontab peut définir des variables d'environnement qui s'appliquent à toutes les commandes de la crontab.

```{prf:remark}
:label: remark-18-01
Variables importantes dans une crontab :

- **`MAILTO`** : adresse email à laquelle envoyer la sortie des commandes. `MAILTO=""` désactive les emails (très utile pour éviter le spam si les commandes produisent une sortie normale).
- **`PATH`** : chemin de recherche des commandes. C'est l'une des sources de problèmes les plus fréquentes : le `PATH` de cron est très limité (typiquement `/usr/bin:/bin`), et les scripts qui fonctionnent en ligne de commande peuvent échouer dans cron car les commandes ne sont pas trouvées.
- **`SHELL`** : shell utilisé pour exécuter les commandes (défaut : `/bin/sh`, pas Bash !).
- **`HOME`** : répertoire personnel (défaut : le `$HOME` de l'utilisateur depuis `/etc/passwd`).
```

```bash
# En-tête recommandé pour une crontab
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
HOME=/home/utilisateur

# Tâches
30 2 * * * /usr/local/bin/sauvegarde.sh
```

## Pièges courants de cron

`cron` est réputé pour ses pièges, qui peuvent rendre le débogage difficile si on ne les connaît pas.

```{prf:example} Les pièges classiques de cron
:label: example-18-02
**Piège 1 : le PATH réduit.** La solution la plus sûre est d'utiliser des **chemins absolus** pour toutes les commandes dans les scripts cron, ou de définir explicitement `PATH` en tête de crontab.

```bash
# Fragile : curl peut ne pas être dans le PATH de cron
0 6 * * * curl https://api.exemple.fr/rapport

# Robuste : chemin absolu
0 6 * * * /usr/bin/curl https://api.exemple.fr/rapport
```

**Piège 2 : pas de terminal (tty).** Les commandes qui nécessitent une interaction avec un terminal (demande de mot de passe, affichage graphique, etc.) échouent silencieusement dans cron.

**Piège 3 : pas de variables d'environnement du shell.** Les variables définies dans `~/.bashrc`, `~/.bash_profile` ou `~/.profile` ne sont **pas** chargées par cron. Les scripts qui en dépendent (aliases, fonctions, variables `NVM_DIR`, `JAVA_HOME`, etc.) doivent sourcer explicitement ces fichiers ou définir leurs propres variables.

```bash
# Sourcer le profil dans le script cron
0 2 * * * source /home/alice/.bashrc && /home/alice/bin/mon_script.sh

# Ou mieux : définir toutes les dépendances dans le script lui-même
```

**Piège 4 : le caractère `%` est interprété par cron.** Dans une crontab, le caractère `%` (utilisé par exemple dans `date +%Y-%m-%d`) est interprété comme un saut de ligne. Il faut l'échapper avec `\%`.

```bash
# FAUX : le % dans date est interprété
0 2 * * * /usr/bin/touch /tmp/test-$(date +%Y-%m-%d)

# CORRECT : échapper les %
0 2 * * * /usr/bin/touch /tmp/test-$(/usr/bin/date +\%Y-\%m-\%d)

# Plus propre : mettre la logique dans un script
```

**Piège 5 : les répertoires de travail implicites.** Le répertoire courant dans cron est généralement le `$HOME` de l'utilisateur. Un script qui utilise des chemins relatifs peut se comporter différemment. La bonne pratique est de **définir explicitement le répertoire de travail** dans le script avec `cd /chemin/absolu || exit 1`.
```

## `systemd` timers : l'alternative moderne

Depuis l'adoption généralisée de `systemd` comme système d'init sur les distributions Linux majeures, les **timers systemd** offrent une alternative puissante et bien intégrée à `cron`.

```{prf:definition} Timers systemd
:label: definition-18-04
Un timer systemd est une **unité** (fichier `.timer`) qui déclenche l'exécution d'une **unité de service** (fichier `.service`) associée selon un calendrier défini. Les deux fichiers ont le même nom de base (ex. `sauvegarde.timer` déclenche `sauvegarde.service`). Les timers sont gérés via `systemctl` et leurs journaux sont disponibles dans `journald`.
```

### Exemple complet : créer un timer systemd

**Étape 1 : écrire le fichier service** (`/etc/systemd/system/sauvegarde.service`)

```ini
[Unit]
Description=Sauvegarde quotidienne de la base de données
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=oneshot
User=backup
Group=backup
ExecStart=/usr/local/bin/sauvegarde_bdd.sh
StandardOutput=journal
StandardError=journal
# Timeout de 1 heure maximum
TimeoutStartSec=3600
```

**Étape 2 : écrire le fichier timer** (`/etc/systemd/system/sauvegarde.timer`)

```ini
[Unit]
Description=Planification de la sauvegarde quotidienne
Requires=sauvegarde.service

[Timer]
# Tous les jours à 2h30
OnCalendar=*-*-* 02:30:00
# Si la machine était éteinte à l'heure prévue, rattraper la tâche
Persistent=true
# Décalage aléatoire jusqu'à 30 minutes pour éviter les pics de charge
RandomizedDelaySec=30min

[Install]
WantedBy=timers.target
```

**Étape 3 : activer et démarrer le timer**

```bash
# Recharger systemd après avoir créé les fichiers
systemctl daemon-reload

# Activer le timer au démarrage
systemctl enable sauvegarde.timer

# Démarrer le timer maintenant
systemctl start sauvegarde.timer

# Vérifier le statut
systemctl status sauvegarde.timer

# Lister tous les timers actifs
systemctl list-timers

# Consulter les journaux du service
journalctl -u sauvegarde.service -f
journalctl -u sauvegarde.service --since "1 day ago"
```

### La directive `OnCalendar`

`OnCalendar` accepte une syntaxe riche pour décrire des planifications complexes :

```{prf:example} Syntaxe `OnCalendar`
:label: example-18-03
```bash
# Format : DayOfWeek YYYY-MM-DD HH:MM:SS
# Les composants peuvent être des listes, des ranges ou des jokers

OnCalendar=daily                   # Tous les jours à minuit
OnCalendar=weekly                  # Tous les lundis à minuit
OnCalendar=monthly                 # Le 1er de chaque mois à minuit
OnCalendar=hourly                  # Toutes les heures
OnCalendar=*-*-* 02:30:00          # Tous les jours à 2h30
OnCalendar=Mon *-*-* 08:00:00      # Tous les lundis à 8h
OnCalendar=Mon..Fri *-*-* 09:00    # Du lundi au vendredi à 9h
OnCalendar=*-*-01 00:00:00         # Le 1er de chaque mois
OnCalendar=*-01,07-01 00:00:00     # Le 1er janvier et le 1er juillet
OnCalendar=*:0/15                  # Toutes les 15 minutes
OnCalendar=*:00,15,30,45           # Même chose, explicite

# Vérifier la prochaine occurrence d'une expression OnCalendar
systemd-analyze calendar "*-*-* 02:30:00"
```

### Autres directives de timing

En plus de `OnCalendar` (timing absolu), les timers systemd offrent des timings relatifs :

```ini
[Timer]
# Démarrer 5 minutes après le boot
OnBootSec=5min

# Démarrer 10 minutes après l'activation du timer
OnActiveSec=10min

# Répéter toutes les heures après le dernier démarrage du service
OnUnitActiveSec=1h

# Répéter 30 minutes après la fin du service (succès ou échec)
OnUnitInactiveSec=30min
```

### Avantages des timers systemd sur cron

```{prf:remark}
:label: remark-18-02
Les timers systemd présentent plusieurs avantages décisifs par rapport à cron :

1. **Journalisation intégrée.** Toute la sortie du service (stdout et stderr) est capturée par `journald` et consultable avec `journalctl`. Avec cron, la sortie doit être redirigée manuellement ou envoyée par email.

2. **Gestion des dépendances.** `After=`, `Wants=`, `Requires=` permettent d'exprimer que la tâche doit s'exécuter après que certains services soient démarrés (base de données, réseau, etc.).

3. **Rattrapage des tâches manquées.** Avec `Persistent=true`, si la machine était éteinte à l'heure prévue, la tâche sera exécutée au prochain démarrage.

4. **Délai aléatoire.** `RandomizedDelaySec` disperse les exécutions dans le temps, ce qui évite les pics de charge sur un parc de machines.

5. **Isolation et sécurité.** Les unités systemd peuvent définir des namespaces, limites de ressources, utilisateurs dédiés, systèmes de fichiers en lecture seule, etc.

6. **Monitoring unifié.** `systemctl status`, `systemctl list-timers` et `journalctl` donnent une vue cohérente de l'état de toutes les tâches planifiées.
```

## `at` : tâches ponctuelles différées

`cron` et les timers systemd sont conçus pour les tâches **récurrentes**. Pour une tâche à exécuter **une seule fois** dans le futur, `at` est l'outil approprié.

```{prf:definition} La commande `at`
:label: definition-18-05
`at` permet de planifier une **tâche ponctuelle** pour une exécution future. Il accepte une grande variété de spécifications temporelles en anglais : `now + 1 hour`, `3:30pm tomorrow`, `noon`, `midnight`, `Jan 15`, etc. Les tâches sont gérées par le démon `atd`.
```

```bash
# Planifier une commande dans une heure
echo "/usr/local/bin/envoyer_rapport.sh" | at now + 1 hour

# Syntaxe interactive (terminer avec Ctrl+D)
at 23:30
> /usr/local/bin/sauvegarde.sh
> mail -s "Rapport" admin@exemple.fr < /tmp/rapport.txt
> ^D

# Planifier pour demain à 8h
at 8:00 tomorrow

# Planifier pour une date précise
at 14:30 March 21

# Lister les tâches en attente
atq
# ou
at -l

# Afficher le détail d'une tâche (numéro donné par atq)
at -c 42

# Supprimer une tâche
atrm 42
# ou
at -d 42
```

```{prf:remark}
:label: remark-18-03
`at` hérite du même environnement restreint que cron concernant le `PATH`. La sortie standard et stderr sont envoyées par email à l'utilisateur (si un MTA est configuré). Pour éviter cela, rediriger explicitement dans la tâche : `ma_commande > /var/log/ma_tache.log 2>&1`.

L'accès à `at` peut être contrôlé via les fichiers `/etc/at.allow` et `/etc/at.deny` : si `at.allow` existe, seuls les utilisateurs y figurant peuvent l'utiliser ; sinon, `at.deny` liste les utilisateurs qui n'y ont pas accès.
```

## Journalisation des tâches planifiées

La journalisation des tâches cron est un aspect souvent négligé qui peut rendre le débogage difficile.

### Rediriger stdout et stderr dans cron

```bash
# Tout capturer dans un fichier de log avec horodatage
30 2 * * * /usr/local/bin/sauvegarde.sh >> /var/log/sauvegarde.log 2>&1

# Créer un log daté pour chaque exécution
0 6 * * * /usr/local/bin/rapport.sh > "/var/log/rapport_$(date +\%Y\%m\%d).log" 2>&1

# Séparer stdout et stderr
*/5 * * * * /usr/local/bin/surveillance.sh \
    >> /var/log/surveillance_out.log \
    2>> /var/log/surveillance_err.log

# Supprimer toute sortie (si la tâche est silencieuse par convention)
@daily /usr/local/bin/nettoyage.sh > /dev/null 2>&1
```

### `logger` : écrire dans le journal système

La commande `logger` permet d'envoyer des messages vers le journal système (`syslog` / `journald`), ce qui centralise les logs de toutes les tâches dans un seul endroit.

```{prf:definition} La commande `logger`
:label: definition-18-06
`logger` envoie un message au démon syslog local. Les messages sont consultables via `journalctl` (systemd) ou dans `/var/log/syslog`. La commande accepte une priorité (`-p`), un tag (`-t`) et le message lui-même.
```

```bash
# Envoyer un message d'information
logger -t sauvegarde "Début de la sauvegarde quotidienne"

# Envoyer un message d'erreur
logger -t sauvegarde -p user.error "Échec de la sauvegarde : espace disque insuffisant"

# Utilisation dans un script cron
#!/usr/bin/env bash
set -euo pipefail

readonly TAG="sauvegarde-bdd"

logger -t "$TAG" "Démarrage de la sauvegarde"

if pg_dump -Fc ma_base > /var/backups/ma_base_$(date +%Y%m%d).dump; then
    logger -t "$TAG" "Sauvegarde terminée avec succès"
else
    logger -t "$TAG" -p user.error "Échec de la sauvegarde"
    exit 1
fi
```

```bash
# Consulter les logs d'un tag spécifique
journalctl -t sauvegarde-bdd
journalctl -t sauvegarde-bdd --since "7 days ago"

# Dans le syslog traditionnel
grep "sauvegarde-bdd" /var/log/syslog
```

### Rotation des logs avec `logrotate`

Pour éviter que les fichiers de log ne grossissent indéfiniment, on utilise `logrotate` :

```ini
# /etc/logrotate.d/mes-scripts-cron
/var/log/sauvegarde.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 root adm
}
```

```{code-cell} python
:tags: [hide-input]

fig, axes = plt.subplots(1, 2, figsize=(14, 8))

# --- Graphique 1 : Comparaison des fonctionnalités cron vs systemd ---
ax = axes[0]
ax.set_title('Comparaison : Cron vs Timers systemd', fontsize=12,
             fontweight='bold', pad=15)

categories = [
    'Journalisation\nautomatique',
    'Dépendances\nentre services',
    'Rattrapage\ntâches manquées',
    'Isolation\nsécurité',
    'Syntaxe\nSimplicité',
    'Universalité\nPortabilité',
    'Délai\naléatoire',
]

scores_cron = [2, 1, 1, 2, 5, 5, 1]
scores_systemd = [5, 5, 5, 5, 3, 3, 5]

x = np.arange(len(categories))
width = 0.35

bars1 = ax.bar(x - width/2, scores_cron, width, label='cron',
               color='#e67e22', alpha=0.85, edgecolor='white', linewidth=1.5)
bars2 = ax.bar(x + width/2, scores_systemd, width, label='systemd timers',
               color='#2980b9', alpha=0.85, edgecolor='white', linewidth=1.5)

ax.set_xticks(x)
ax.set_xticklabels(categories, fontsize=8.5, rotation=15, ha='right')
ax.set_ylabel('Score (1 = faible, 5 = excellent)', fontsize=9)
ax.set_ylim(0, 6.5)
ax.legend(fontsize=10)
ax.set_yticks([1, 2, 3, 4, 5])

for bar in bars1:
    h = bar.get_height()
    ax.text(bar.get_x() + bar.get_width()/2, h + 0.1, str(int(h)),
            ha='center', va='bottom', fontsize=8, fontweight='bold',
            color='#e67e22')
for bar in bars2:
    h = bar.get_height()
    ax.text(bar.get_x() + bar.get_width()/2, h + 0.1, str(int(h)),
            ha='center', va='bottom', fontsize=8, fontweight='bold',
            color='#2980b9')

# --- Graphique 2 : Répartition temporelle des tâches cron (visualisation) ---
ax2 = axes[1]
ax2.set_title('Exemple de planning cron sur 24 heures', fontsize=12,
              fontweight='bold', pad=15)
ax2.set_xlim(-0.5, 24.5)
ax2.set_ylim(-0.5, 6)
ax2.set_xlabel('Heure', fontsize=10)
ax2.set_yticks([])
ax2.set_xticks(range(0, 25, 2))

# Grille verticale légère
for h in range(25):
    ax2.axvline(h, color='#ddd', lw=0.5, alpha=0.5)

# Tâches planifiées
taches = [
    (0.5, 1, '#c0392b', 'Archivage mensuel\n(1er du mois, 0h30)'),
    (2.5, 2, '#2980b9', 'Sauvegarde BDD\n(chaque nuit, 2h30)'),
    (4.0, 3, '#27ae60', 'Nettoyage tmp\n(quotidien, 4h)'),
    (6.0, 4, '#8e44ad', 'Rapport quotidien\n(6h)'),
    (8.0, 5, '#e67e22', 'Sync données\n(lun-ven, toutes les 15 min)'),
]

for heure, y, couleur, label in taches:
    if 'toutes les 15' in label:
        # Multiples points toutes les 15 min de 8h à 18h
        heures = np.arange(8, 18.5, 0.25)
        ax2.scatter(heures, [y] * len(heures), c=couleur, s=40, zorder=5,
                    alpha=0.8, marker='|')
        ax2.text(13, y + 0.35, label, ha='center', va='bottom',
                 fontsize=7.5, color=couleur, fontweight='bold')
    else:
        ax2.scatter([heure], [y], c=couleur, s=120, zorder=5, alpha=0.9)
        ax2.text(heure + 0.3, y + 0.15, label, ha='left', va='bottom',
                 fontsize=7.5, color=couleur, fontweight='bold')

ax2.axhline(0, color='#333', lw=1)

plt.tight_layout(pad=3.0)
plt.show()
```

## Résumé

Ce chapitre a couvert les mécanismes de planification de tâches sous Linux :

- `cron` est le planificateur historique, présent sur tous les systèmes Unix. Sa syntaxe à cinq champs (minute, heure, jour, mois, jour-semaine) est concise mais parfois contre-intuitive. Ses principaux pièges — PATH réduit, absence de variables d'environnement, interprétation du `%` — nécessitent une attention particulière.
- `crontab -e`, `-l` et `-r` gèrent la crontab de l'utilisateur courant ; `/etc/cron.d/` et les répertoires périodiques permettent une installation système.
- Les **timers systemd** offrent une alternative moderne avec des avantages substantiels : journalisation automatique via `journald`, gestion des dépendances, rattrapage des tâches manquées avec `Persistent=true`, isolation de sécurité et délais aléatoires avec `RandomizedDelaySec`.
- `at` couvre le cas d'usage des tâches ponctuelles différées, à la différence de cron qui gère les récurrences.
- La journalisation — redirection vers des fichiers de log datés, utilisation de `logger` vers syslog — est indispensable pour surveiller et déboguer les tâches planifiées.

Dans le chapitre suivant, nous explorons la frontière entre Bash et les autres langages de programmation, et en particulier la collaboration entre Bash et Python.
