Initial
This commit is contained in:
Executable
+322
@@ -0,0 +1,322 @@
|
|||||||
|
# Guide : Création d'un container LXC sur Proxmox VE
|
||||||
|
|
||||||
|
## Sommaire
|
||||||
|
- [Prérequis](#prérequis)
|
||||||
|
- [Création du container LXC](#création-du-container-lxc)
|
||||||
|
- [Configuration réseau](#configuration-réseau)
|
||||||
|
- [Configuration SSH](#configuration-ssh)
|
||||||
|
- [Vérifications](#vérifications)
|
||||||
|
- [Commandes utiles](#commandes-utiles)
|
||||||
|
- [Exemple concret : CT201 (status-monitor)](#exemple-concret--ct201-status-monitor)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
### Environnement
|
||||||
|
- Cluster Proxmox VE 9.2.4 (Debian 13.5 Trixie)
|
||||||
|
- Stockage ZFS configuré : `vm-zfs` (rpool/vm)
|
||||||
|
- Réseau : `vmbr0` (bridge par défaut)
|
||||||
|
- Accès SSH aux nœuds Proxmox avec authentification par certificat
|
||||||
|
|
||||||
|
### Templates disponibles
|
||||||
|
Vérifier les templates disponibles sur le nœud :
|
||||||
|
```bash
|
||||||
|
ls /var/lib/vz/template/cache/
|
||||||
|
```
|
||||||
|
Exemple de sortie :
|
||||||
|
```
|
||||||
|
debian-12-standard_12.12-1_amd64.tar.zst
|
||||||
|
debian-13-standard_13.6-1_amd64.tar.zst
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Création du container LXC
|
||||||
|
|
||||||
|
### Commande de base
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pct create <CTID> <TEMPLATE> \
|
||||||
|
--rootfs <STORAGE>:<SIZE> \
|
||||||
|
--ostype <OS_TYPE> \
|
||||||
|
--hostname <HOSTNAME> \
|
||||||
|
--password <PASSWORD> \
|
||||||
|
--unprivileged 0 \
|
||||||
|
--onboot 1 \
|
||||||
|
--nameserver <DNS_IP> \
|
||||||
|
--net0 name=eth0,bridge=vmbr0,ip=<IP>/24,gw=<GATEWAY> \
|
||||||
|
--start 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paramètres
|
||||||
|
|
||||||
|
| Paramètre | Description | Valeur recommandée |
|
||||||
|
|-----------|-------------|-------------------|
|
||||||
|
| `CTID` | Identifiant unique du container | 100-999999999 |
|
||||||
|
| `TEMPLATE` | Template système | `local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst` |
|
||||||
|
| `--rootfs` | Stockage et taille du disque racine | `vm-zfs:2` (2 GiB sur ZFS) |
|
||||||
|
| `--ostype` | Type du système d'exploitation | `debian`, `ubuntu`, `alpine` |
|
||||||
|
| `--hostname` | Nom d'hôte du container | `status-monitor` |
|
||||||
|
| `--password` | Mot de passe root | `secrettemp123` (temporaire) |
|
||||||
|
| `--unprivileged` | Container non privilégié | `0` (privilégié) ou `1` (non-privilégié) |
|
||||||
|
| `--onboot` | Démarrage automatique | `1` (oui) ou `0` (non) |
|
||||||
|
| `--nameserver` | Serveur DNS | `192.168.0.5` (AdGuard) ou `8.8.8.8` |
|
||||||
|
| `--net0` | Configuration réseau | `name=eth0,bridge=vmbr0,ip=192.168.0.X/24,gw=192.168.0.1` |
|
||||||
|
| `--start` | Démarrer après création | `1` (oui) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration réseau
|
||||||
|
|
||||||
|
### Options réseau (`--net0`)
|
||||||
|
|
||||||
|
| Option | Description | Exemple |
|
||||||
|
|--------|-------------|---------|
|
||||||
|
| `name` | Nom de l'interface | `eth0` |
|
||||||
|
| `bridge` | Bridge Proxmox | `vmbr0` |
|
||||||
|
| `ip` | Adresse IP | `192.168.0.201/24` |
|
||||||
|
| `gw` | Passerelle | `192.168.0.1` |
|
||||||
|
| `hwaddr` | Adresse MAC | `BC:24:11:XX:XX:XX` (auto-générée) |
|
||||||
|
| `firewall` | Pare-feu activé | `1` (oui) ou `0` (non) |
|
||||||
|
|
||||||
|
### Exemple de configuration réseau complète
|
||||||
|
```bash
|
||||||
|
--net0 name=eth0,bridge=vmbr0,ip=192.168.0.201/24,gw=192.168.0.1,firewall=0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration SSH
|
||||||
|
|
||||||
|
### Ajout de la clé SSH pour accès sans mot de passe
|
||||||
|
|
||||||
|
1. **Créer le répertoire .ssh dans le container** :
|
||||||
|
```bash
|
||||||
|
pct exec <CTID> -- mkdir -p /root/.ssh
|
||||||
|
pct exec <CTID> -- chmod 700 /root/.ssh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Ajouter la clé publique** :
|
||||||
|
```bash
|
||||||
|
pct exec <CTID> -- bash -c 'echo ssh-ed25519\ AAAAC3NzaC1lZDI1NTE5AAAAIG8eq44iROSYLJj0G5jNh2Py57/f2Z0LnTE7rPG2T6HH\ theo@peis.fr > /root/.ssh/authorized_keys'
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configurer les permissions** :
|
||||||
|
```bash
|
||||||
|
pct exec <CTID> -- chmod 600 /root/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Vérifier la configuration SSH** (optionnel) :
|
||||||
|
```bash
|
||||||
|
pct exec <CTID> -- cat /etc/ssh/sshd_config | grep -E 'PasswordAuthentication|PermitRootLogin|PubkeyAuthentication'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test de connexion
|
||||||
|
```bash
|
||||||
|
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no root@192.168.0.201
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vérifications
|
||||||
|
|
||||||
|
### Vérifier le statut du container
|
||||||
|
```bash
|
||||||
|
pct status <CTID>
|
||||||
|
```
|
||||||
|
Exemple de sortie :
|
||||||
|
```
|
||||||
|
status: running
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérifier l'adresse IP
|
||||||
|
```bash
|
||||||
|
pct exec <CTID> -- ip addr show eth0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérifier le service SSH
|
||||||
|
```bash
|
||||||
|
pct exec <CTID> -- ss -tlnp | grep 22
|
||||||
|
```
|
||||||
|
Exemple de sortie :
|
||||||
|
```
|
||||||
|
LISTEN 0 4096 *:22 *:* users:(("sshd",pid=121,fd=3))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tester la connectivité réseau
|
||||||
|
```bash
|
||||||
|
ping 192.168.0.<CTID_IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commandes utiles
|
||||||
|
|
||||||
|
### Gestion du container
|
||||||
|
|
||||||
|
| Action | Commande |
|
||||||
|
|--------|----------|
|
||||||
|
| Démarrer | `pct start <CTID>` |
|
||||||
|
| Arrêter | `pct stop <CTID>` |
|
||||||
|
| Redémarrer | `pct restart <CTID>` |
|
||||||
|
| Supprimer | `pct destroy <CTID>` |
|
||||||
|
| Voir les logs | `pct console <CTID>` |
|
||||||
|
| Exécuter une commande | `pct exec <CTID> -- <COMMANDE>` |
|
||||||
|
|
||||||
|
### Gestion du stockage
|
||||||
|
|
||||||
|
| Action | Commande |
|
||||||
|
|--------|----------|
|
||||||
|
| Redimensionner | `pct resize <CTID> rootfs +2G` |
|
||||||
|
| Voir l'utilisation | `pct df <CTID>` |
|
||||||
|
|
||||||
|
### Gestion du réseau
|
||||||
|
|
||||||
|
| Action | Commande |
|
||||||
|
|--------|----------|
|
||||||
|
| Ajouter une IP | `pct set <CTID> --net0 name=eth0,bridge=vmbr0,ip=192.168.0.X/24,gw=192.168.0.1` |
|
||||||
|
| Voir la config | `pct config <CTID>` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exemple concret : CT201 (status-monitor)
|
||||||
|
|
||||||
|
### Contexte
|
||||||
|
Création d'un container dédié au monitoring du statut des services du homelab.
|
||||||
|
|
||||||
|
### Commande de création
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pct create 201 \
|
||||||
|
local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
|
||||||
|
--rootfs vm-zfs:2 \
|
||||||
|
--ostype debian \
|
||||||
|
--hostname status-monitor \
|
||||||
|
--password secrettemp123 \
|
||||||
|
--unprivileged 0 \
|
||||||
|
--onboot 1 \
|
||||||
|
--nameserver 192.168.0.5 \
|
||||||
|
--net0 name=eth0,bridge=vmbr0,ip=192.168.0.201/24,gw=192.168.0.1 \
|
||||||
|
--start 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Post-création
|
||||||
|
|
||||||
|
1. **Ajout de la clé SSH** :
|
||||||
|
```bash
|
||||||
|
# Sur le nœud Proxmox (prox2)
|
||||||
|
pct exec 201 -- mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
||||||
|
pct exec 201 -- bash -c 'echo ssh-ed25519\ AAAAC3NzaC1lZDI1NTE5AAAAIG8eq44iROSYLJj0G5jNh2Py57/f2Z0LnTE7rPG2T6HH\ theo@peis.fr > /root/.ssh/authorized_keys'
|
||||||
|
pct exec 201 -- chmod 600 /root/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Vérification** :
|
||||||
|
```bash
|
||||||
|
# Vérifier le statut
|
||||||
|
pct status 201
|
||||||
|
|
||||||
|
# Tester le ping
|
||||||
|
ping 192.168.0.201
|
||||||
|
|
||||||
|
# Tester SSH
|
||||||
|
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no root@192.168.0.201 echo "OK"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Installation des paquets** (si DNS fonctionne) :
|
||||||
|
```bash
|
||||||
|
ssh root@192.168.0.201 "apt-get update && apt-get install -y python3 curl git"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note** : Si le DNS ne fonctionne pas avec `192.168.0.5` (AdGuard), utiliser `8.8.8.8` :
|
||||||
|
> ```bash
|
||||||
|
> ssh root@192.168.0.201 "echo 'nameserver 8.8.8.8' > /etc/resolv.conf"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### Configuration finale
|
||||||
|
|
||||||
|
| Propriété | Valeur |
|
||||||
|
|-----------|--------|
|
||||||
|
| **CTID** | 201 |
|
||||||
|
| **Nom** | status-monitor |
|
||||||
|
| **IP** | 192.168.0.201 |
|
||||||
|
| **Stockage** | vm-zfs:2 (2 GiB) |
|
||||||
|
| **OS** | Debian 13 (Trixie) |
|
||||||
|
| **Statut** | running |
|
||||||
|
| **Démarrage auto** | ✅ Oui |
|
||||||
|
| **Accès SSH** | `ssh root@192.168.0.201` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Résolution des problèmes
|
||||||
|
|
||||||
|
### Problème : DNS ne fonctionne pas
|
||||||
|
**Symptômes** : `apt-get update` échoue avec "Temporary failure resolving"
|
||||||
|
|
||||||
|
**Solution** :
|
||||||
|
```bash
|
||||||
|
# Changer temporairement le DNS
|
||||||
|
ssh root@192.168.0.201 "echo 'nameserver 8.8.8.8' > /etc/resolv.conf"
|
||||||
|
|
||||||
|
# Ou vérifier AdGuard (CT103)
|
||||||
|
# Si AdGuard bloque les requêtes, vérifier sa configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problème : SSH ne répond pas
|
||||||
|
**Symptômes** : `ssh root@192.168.0.201` timeout
|
||||||
|
|
||||||
|
**Vérifications** :
|
||||||
|
1. Le container est bien démarré : `pct status 201`
|
||||||
|
2. L'IP est configurée : `pct exec 201 -- ip a`
|
||||||
|
3. SSH est en écoute : `pct exec 201 -- ss -tlnp | grep 22`
|
||||||
|
4. La clé SSH est correcte : `cat /root/.ssh/authorized_keys`
|
||||||
|
|
||||||
|
### Problème : Impossible de créer le container (template introuvable)
|
||||||
|
**Symptômes** : `volume 'local:vztmpl/...' does not exist`
|
||||||
|
|
||||||
|
**Solution** :
|
||||||
|
```bash
|
||||||
|
# Lister les templates disponibles
|
||||||
|
ls /var/lib/vz/template/cache/
|
||||||
|
|
||||||
|
# Si nécessaire, télécharger un template
|
||||||
|
pveam download local debian-13-standard_13.6-1_amd64.tar.zst
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bonnes pratiques
|
||||||
|
|
||||||
|
1. **Nommage** : Utiliser des noms explicites pour les containers (ex: `ct201-status-monitor`)
|
||||||
|
2. **Stockage** : Utiliser ZFS pour les containers avec beaucoup d'E/S
|
||||||
|
3. **Réseau** : Toujours spécifier une IP statique pour les services critiques
|
||||||
|
4. **Sécurité** :
|
||||||
|
- Désactiver l'accès root SSH si possible
|
||||||
|
- Utiliser des clés SSH au lieu de mots de passe
|
||||||
|
- Mettre à jour les paquets après création
|
||||||
|
5. **Sauvegarde** : Configurer des sauvegardes régulières des containers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commandes résumées pour CT201
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Création
|
||||||
|
pct create 201 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst --rootfs vm-zfs:2 --ostype debian --hostname status-monitor --password secrettemp123 --unprivileged 0 --onboot 1 --nameserver 192.168.0.5 --net0 name=eth0,bridge=vmbr0,ip=192.168.0.201/24,gw=192.168.0.1 --start 1
|
||||||
|
|
||||||
|
# Ajout clé SSH
|
||||||
|
pct exec 201 -- mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
||||||
|
pct exec 201 -- bash -c 'echo ssh-ed25519\ AAAAC3NzaC1lZDI1NTE5AAAAIG8eq44iROSYLJj0G5jNh2Py57/f2Z0LnTE7rPG2T6HH\ theo@peis.fr > /root/.ssh/authorized_keys'
|
||||||
|
pct exec 201 -- chmod 600 /root/.ssh/authorized_keys
|
||||||
|
|
||||||
|
# Connexion
|
||||||
|
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no root@192.168.0.201
|
||||||
|
|
||||||
|
# Vérifications
|
||||||
|
pct status 201
|
||||||
|
ping 192.168.0.201
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document généré le 18/07/2026*
|
||||||
Executable
+140
@@ -0,0 +1,140 @@
|
|||||||
|
# Inventaire Homelab
|
||||||
|
|
||||||
|
## Sommaire
|
||||||
|
- [Serveurs Proxmox](#serveurs-proxmox)
|
||||||
|
- [Proxmox Principal (prox2)](#proxmox-principal-prox2)
|
||||||
|
- [Proxmox Secondaire (prox)](#proxmox-secondaire-prox)
|
||||||
|
- [NAS](#nas)
|
||||||
|
- [Raspberry Pi](#raspberry-pi)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Serveurs Proxmox
|
||||||
|
|
||||||
|
### Proxmox Principal (prox2)
|
||||||
|
- **IP**: 192.168.0.95
|
||||||
|
- **Utilisateur**: root
|
||||||
|
- **Accès SSH**: `ssh root@192.168.0.95` (authentification par certificat)
|
||||||
|
- **Version**: Proxmox VE 9.2.4 (Debian 13.5 Trixie)
|
||||||
|
- **Noyau**: 7.0.14-3-pve
|
||||||
|
- **Statut**: À jour
|
||||||
|
|
||||||
|
#### Machines virtuelles (VMs)
|
||||||
|
*Aucune (VM 100 sur prox)*
|
||||||
|
|
||||||
|
#### Containers LXC
|
||||||
|
*Aucun (tous sur prox)*
|
||||||
|
|
||||||
|
### Proxmox Secondaire (prox)
|
||||||
|
- **IP**: 192.168.0.94
|
||||||
|
- **Utilisateur**: root
|
||||||
|
- **Accès SSH**: `ssh root@192.168.0.94` (authentification par certificat)
|
||||||
|
- **Version**: Proxmox VE 9.2.4 (Debian 13.5 Trixie)
|
||||||
|
- **Noyau**: 7.0.14-3-pve
|
||||||
|
- **Statut**: À jour
|
||||||
|
- **Stockage supplémentaire**: `local-lvm` (119G) sur `/dev/sdb` - Créé pour la migration des containers
|
||||||
|
- **Stockage ZFS LXC**: `pve-zfs` (rpool/data, 447G) - Utilisé pour les containers LXC
|
||||||
|
- **Stockage ZFS VM**: `vm-zfs` (rpool/vm) - Créé pour les VMs
|
||||||
|
- **Cluster HA**: Activé (quorum OK, master: prox2)
|
||||||
|
|
||||||
|
#### Machines virtuelles (VMs)
|
||||||
|
| VMID | Nom | IP | Stockage | HA | Statut |
|
||||||
|
|------|-----|-----|---------|-----|--------|
|
||||||
|
| 100 | Home Assistant OS (haos12.3) | 192.168.0.43 | vm-zfs (vm-100-disk-0:32G, vm-100-disk-1:1M) | ✅ Oui | running |
|
||||||
|
|
||||||
|
#### Containers LXC
|
||||||
|
| CTID | Nom | IP | Stockage | HA | Statut | Ports |
|
||||||
|
|------|-----|-----|---------|-----|--------|-------|
|
||||||
|
| 101 | MQTT | 192.168.0.5 | vm-zfs (subvol-101-disk-0, 2G) | ✅ Oui | running | 1883, 8883 |
|
||||||
|
| 102 | Frigate | 192.168.0.91 | vm-zfs (subvol-102-disk-0, 20G) | ✅ Oui | running | 5000 |
|
||||||
|
| 103 | AdGuard | 192.168.0.92 | vm-zfs (subvol-103-disk-0, 2G) | ✅ Oui | running | 80, 443, 3000 |
|
||||||
|
| 105 | Alpine-Docker | 192.168.0.155 | vm-zfs (subvol-105-disk-0, 2G) | ✅ Oui | running | 22, 2375-2376 |
|
||||||
|
| 106 | Zigbee2MQTT | 192.168.0.177 | vm-zfs (subvol-106-disk-0, 5G) | ✅ Oui | running | 8081 |
|
||||||
|
| 200 | traefik-proxy | 192.168.0.100 | vm-zfs (subvol-200-disk-0, 10G) | ✅ Oui | running | 80, 443 |
|
||||||
|
| 201 | status-monitor | 192.168.0.201 | vm-zfs (subvol-201-disk-0, 2G) | ✅ Oui | running | 80 |
|
||||||
|
| 202 | git-server | 192.168.0.202 | vm-zfs (subvol-202-disk-0, 10G) | ✅ Oui | running | 3000, 2222 |
|
||||||
|
|
||||||
|
##### Status Monitor (CT201)
|
||||||
|
**IP**: 192.168.0.201 | **URL**: https://status.peis.fr | **Version**: v1.1.0
|
||||||
|
- **Description**: Service de monitoring en temps réel de l'état des VMs, containers et services du homelab
|
||||||
|
- **Fonctionnalités**:
|
||||||
|
- Vérification SSH, HTTP, HTTPS pour chaque service
|
||||||
|
- Bouton de refresh individuel par service
|
||||||
|
- Bouton de refresh global
|
||||||
|
- Animation de chargement pendant le refresh
|
||||||
|
- Historique du dernier changement de statut
|
||||||
|
- Affichage du numéro de version
|
||||||
|
- Auto-refresh toutes les 30 secondes
|
||||||
|
- **Services monitorés**: 25 services (SSH, HTTP, HTTPS pour MQTT, Frigate, AdGuard, Alpine-Docker, Zigbee2MQTT, Traefik, Home Assistant, Proxmox hosts, Raspberry Pi, et tous les domaines publics)
|
||||||
|
- **Ports vérifiés**:
|
||||||
|
- SSH: 22 pour tous les containers/VMs
|
||||||
|
- HTTP: ports spécifiques (1883, 5000, 80, 2375, 8081, etc.)
|
||||||
|
- HTTPS: 443 pour les domaines publics
|
||||||
|
|
||||||
|
##### Git Server (CT202)
|
||||||
|
**IP**: 192.168.0.202 | **URL**: https://git.peis.fr | **Logiciel**: Gitea
|
||||||
|
- **Description**: Serveur Git auto-hébergé pour la gestion des dépôts et sauvegardes
|
||||||
|
- **Logiciel**: Gitea (via Docker)
|
||||||
|
- **Ports**:
|
||||||
|
- Web: 3000 (HTTP)
|
||||||
|
- SSH: 2222 (pour git clone/push)
|
||||||
|
- **Stockage**: 10Go sur vm-zfs
|
||||||
|
- **Fonctionnalités**:
|
||||||
|
- Création et gestion de dépôts Git
|
||||||
|
- Interface web complète
|
||||||
|
- Gestion des utilisateurs et organisations
|
||||||
|
- Issues, Pull Requests, Wiki
|
||||||
|
- Webhooks pour CI/CD
|
||||||
|
- **Accès initial**:
|
||||||
|
- URL: http://192.168.0.202:3000 (installation)
|
||||||
|
- SSH: git@192.168.0.202:2222
|
||||||
|
- Après configuration: https://git.peis.fr
|
||||||
|
**IP**: 192.168.0.201 | **URL**: https://status.peis.fr | **Version**: v1.1.0
|
||||||
|
- **Description**: Service de monitoring en temps réel de l'état des VMs, containers et services du homelab
|
||||||
|
- **Fonctionnalités**:
|
||||||
|
- Vérification SSH, HTTP, HTTPS pour chaque service
|
||||||
|
- Bouton de refresh individuel par service
|
||||||
|
- Bouton de refresh global
|
||||||
|
- Animation de chargement pendant le refresh
|
||||||
|
- Historique du dernier changement de statut
|
||||||
|
- Affichage du numéro de version
|
||||||
|
- Auto-refresh toutes les 30 secondes
|
||||||
|
- **Services monitorés**: 25 services (SSH, HTTP, HTTPS pour MQTT, Frigate, AdGuard, Alpine-Docker, Zigbee2MQTT, Traefik, Home Assistant, Proxmox hosts, Raspberry Pi, et tous les domaines publics)
|
||||||
|
- **Ports vérifiés**:
|
||||||
|
- SSH: 22 pour tous les containers/VMs
|
||||||
|
- HTTP: ports spécifiques (1883, 5000, 80, 2375, 8081, etc.)
|
||||||
|
- HTTPS: 443 pour les domaines publics
|
||||||
|
|
||||||
|
##### Reverse Proxy (Traefik - CT200)
|
||||||
|
**IP**: 192.168.0.100 | **Dashboard**: https://traefik.peis.fr (admin/admin123)
|
||||||
|
|
||||||
|
| Domaine | Service cible | Type | SSL | Certificat | Statut |
|
||||||
|
| `ha.peis.fr` | Home Assistant (VM100) | HTTP | ✅ TLS | Let's Encrypt PROD | ✅ Running |
|
||||||
|
| `traefik.peis.fr` | Dashboard Traefik | Internal API | ✅ TLS | Let's Encrypt PROD | ✅ Running |
|
||||||
|
| `test.peis.fr` | Whoami (test) | HTTP | ✅ TLS | Let's Encrypt PROD | ✅ Running |
|
||||||
|
| `status.peis.fr` | Status Monitor (CT201) | HTTP | ✅ TLS | Let's Encrypt PROD | ✅ Running |
|
||||||
|
| `git.peis.fr` | Gitea (CT202) | HTTP | ✅ TLS | Let's Encrypt PROD | ⚠️ Certificat en attente |
|
||||||
|
|
||||||
|
**Configuration Let's Encrypt**
|
||||||
|
- Email: theo@peis.fr
|
||||||
|
- caServer: https://acme-v02.api.letsencrypt.org/directory (PRODUCTION)
|
||||||
|
- Storage: /etc/traefik/certs/acme.json
|
||||||
|
- Challenge: HTTP (entryPoint: web)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NAS
|
||||||
|
|
||||||
|
*À compléter*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Raspberry Pi
|
||||||
|
|
||||||
|
### Pi3
|
||||||
|
- **IP**: 192.168.0.156
|
||||||
|
- **Utilisateur**: adminpi3
|
||||||
|
- **Mot de passe**: qse45gfML
|
||||||
|
- **Accès SSH**: `ssh adminpi3@192.168.0.156`
|
||||||
|
- **Accès SSH root**: `ssh -i ~/.ssh/id_ed25519 root@192.168.0.156` (authentification par clé)
|
||||||
|
- **Statut**: Configuré
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#Prox1
|
||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDX7m4/3fdhNf3l9sxQ9LrQErLOuFLvDa4p6+4SdxFZ2y38VrGjrqOivkcaJUUBaub3woZShyEn+Muh7D+bIPsfQTdGzN+Xa19UPl2FreqsS2HZUccfAFCzZgg8D7iST72eCsMV22nJebGAKHDV7jd6jwpfkxyjAoJ3BGixdkYyLvWAYz4FVoyRRQkM834wjg3aOcZBV2L6RSeb2qvsbQ2SgZ6k8n2F/rQtIyDc7lfW/VaIAk/AJDXoICsDSxXbYSzQ/KwzkfTrea0r2MG/r1q57RbYCNUNJS+N8q/acgzU/5CYOoGCjEkm1kne9mfMAPbd4CAkxLmxtkyGOs7zkY7OcB4ZjoadkA6RoF70LZXKZsKJWJO37q+6+x9/CltXIAFzarcQA27kqP0Lah21y/cOR88jpqr10xr6kzi5uHNggs1N+qUbyDd76vEWSEhTeKsBql/FJ6jGRGSc2TaA8U9qpI5b0y3lGOA7iXnjxIy8uWqt669RTnT9KUUe471OyYVx3+vEBnC3ibNtNz8UL2mO80V/mXOF9ycqsez+4UG27km/ngdfRyUbdMiE8JnZH64bdWpWHb8Eq0XkwlbtKw5hTxbC+w+SNcS+6oh6zEEnv97eu1HT3pe0lUXj1LESqp1oaeLs8iOsjm+NDqxu714YfbT0C6J6CGdmQb4/Nqnz8Q== root@prox
|
||||||
|
#portable
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG8eq44iROSYLJj0G5jNh2Py57/f2Z0LnTE7rPG2T6HH theo@peis.fr
|
||||||
|
#Prox2
|
||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCRj0aGzGZ79V4KLTkt4i34fjgkjitASxRjU11efINIgCJ9x0hJp/59FrAIoLVWlitKZPh+WlWSB/lrk/x1M1Nr//1Krj663uVwOA6nfeIwF8CyjLJuRRiEi/hDMcp7Oqp0YLpZKVhZKuCK7AyNjCaFOUBct/fLSBu8Pkw4t5ten3bRB07Bw49JhKquRyVOioPpc3PJT5lFiXrXGT1ByETfXo4jF6d8gzA7vM2P2Zq9Hr5JkjGUeQAgaiPz1LcwJ7w1G5dTBxEFpwQg5GwdalILwyxKU4JF/vydpqSYdPRZDxEr1De4x1LMnqMwyfWgoHPqjjUg5e6zq/MNvGaKG3XGFsF5t6RkH/2hML60bZZTDcbcJXJX9AOCbZ9WFvd8coKzxgaQsflBMeU1ppuIQEzscYOYs9I44EN5yYZqE1qFL2SzSTKg/kh5lGAlG5Gx9V3iK9uW/zHu8GrcKr3jVNYtxcHgHIIE+k7bBzu63R+w20EtVDcYSX4kvV2K0AEJSqEL5ge0SRtrTpWr9DxreybRRf6G4u5qywmHE7aMCW4bX+MVgN7dN4lJfUs502VJnz4Y/8AgumYVkmd65dz1JGTJFYBmVhezRHOCZ1jKz9XoBMRYppSRPmztHDCBBG6RPUeiW5g4tf4Xx8n3flySlXF/nC0Pab8+xhJWmIEzi2ejBQ== root@prox2
|
||||||
|
|
||||||
|
#pi3
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIArHR91hhr8crpOi9it0PoeYcgkTkd1HkZi2lCqf2YGS pi3@homelab
|
||||||
|
|
||||||
|
et process:
|
||||||
|
View your public key:
|
||||||
|
|
||||||
|
cat ~/.ssh/agent/id_ed25519_agent.pub
|
||||||
|
Copy the output, then SSH into your Proxmox server and paste it into the authorized_keys file:
|
||||||
|
|
||||||
|
ssh root@YOUR_PROXMOX_IP
|
||||||
|
cd ~/.ssh
|
||||||
|
nano authorized_keys
|
||||||
|
Paste your public key at the bottom, press Ctrl+X, then Y, then Enter to save.
|
||||||
|
|
||||||
|
Now you can SSH without a password:
|
||||||
|
|
||||||
|
ssh root@YOUR_PROXMOX_IP
|
||||||
|
|
||||||
|
!! COPIE DE CLE:
|
||||||
|
ssh -i "C:\Users\theo\.ssh\id_ed25519" root@192.168.0.95 "pct exec 200 -- sh -c 'echo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG8eq44iROSYLJj0G5jNh2Py57/f2Z0LnTE7rPG2T6HH theo@peis.fr > /root/.ssh/authorized_keys'" (timeout 30s)
|
||||||
Executable
+1062
File diff suppressed because it is too large
Load Diff
Executable
+326
@@ -0,0 +1,326 @@
|
|||||||
|
# Procédure de Migration des Containers LXC : prox2 → prox (LVM → ZFS)
|
||||||
|
|
||||||
|
**Objectif** : Migrer les containers LXC de **prox2 (192.168.0.95)** vers **prox (192.168.0.94)** sur stockage LVM, puis déplacer leur stockage vers ZFS (`rpool/data`).
|
||||||
|
|
||||||
|
**Basé sur** : Migration réussie du container **103 (AdGuard)**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ **NOTE IMPORTANTE : Stockages déjà configurés sur prox**
|
||||||
|
|
||||||
|
✅ **Les stockages suivants existent déjà sur prox (192.168.0.94)** :
|
||||||
|
- **`local-lvm`** (119G, lvmthin) sur `/dev/sdb` - VG `pve`, thin pool `data`
|
||||||
|
- **`pve-zfs`** (447G, dir) pointant vers `/rpool/data` (dataset ZFS)
|
||||||
|
|
||||||
|
**→ Pour les prochaines migrations, SAUTEZ les étapes de création de ces stockages (Étape 1 et Étape 3).**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Vérification rapide des stockages existants
|
||||||
|
|
||||||
|
Avant de commencer, vérifiez sur **prox (192.168.0.94)** :
|
||||||
|
```bash
|
||||||
|
pvesm status | grep -E 'local-lvm|pve-zfs'
|
||||||
|
zfs list | grep rpool/data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Si les stockages existent** → Passez directement à l'**Étape 2** (Migration).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Prérequis
|
||||||
|
|
||||||
|
### 1. Cluster Proxmox fonctionnel
|
||||||
|
- Les deux nœuds (**prox2** et **prox**) doivent être **en cluster** et **en quorum**.
|
||||||
|
- Vérifier avec :
|
||||||
|
```bash
|
||||||
|
pvecm status
|
||||||
|
```
|
||||||
|
→ Doit afficher : `Quorate` avec les 2 nœuds + Qdevice (Pi3).
|
||||||
|
|
||||||
|
### 2. Stockage `local-lvm` disponible sur **prox**
|
||||||
|
- Un disque supplémentaire (`/dev/sdb` dans notre cas) doit être **disponible** sur **prox**.
|
||||||
|
- Vérifier avec :
|
||||||
|
```bash
|
||||||
|
lsblk
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Stockage ZFS existant sur **prox**
|
||||||
|
- Le pool ZFS `rpool` doit exister avec un dataset `rpool/data`.
|
||||||
|
- Vérifier avec :
|
||||||
|
```bash
|
||||||
|
zfs list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Étape 1 : Préparer le stockage LVM sur **prox** *(À SAUTER si déjà configuré)*
|
||||||
|
|
||||||
|
**Objectif** : Créer un stockage `local-lvm` sur **prox** pour permettre la migration des containers depuis **prox2**.
|
||||||
|
|
||||||
|
⚠️ **À SAUTER** : Ce stockage existe déjà sur prox (119G sur `/dev/sdb`).
|
||||||
|
|
||||||
|
### Commandes à exécuter sur **prox (192.168.0.94)** *(uniquement si `local-lvm` n'existe pas) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Nettoyer le disque (si partitionné)
|
||||||
|
wipefs -a /dev/sdb
|
||||||
|
|
||||||
|
# 2. Créer un volume physique LVM
|
||||||
|
pvcreate /dev/sdb
|
||||||
|
|
||||||
|
# 3. Créer un volume group (nommé 'pve' pour cohérence avec prox2)
|
||||||
|
vgcreate pve /dev/sdb
|
||||||
|
|
||||||
|
# 4. Créer un thin pool pour les containers
|
||||||
|
lvcreate -L <TAILLE>G -n data pve --thinpool data
|
||||||
|
# Exemple pour 119G :
|
||||||
|
lvcreate -L 119G -n data pve --thinpool data
|
||||||
|
|
||||||
|
# 5. Ajouter le stockage dans Proxmox
|
||||||
|
pvesm add lvmthin local-lvm --vgname pve --thinpool data
|
||||||
|
|
||||||
|
# 6. Vérifier le stockage
|
||||||
|
pvesm status
|
||||||
|
```
|
||||||
|
|
||||||
|
**Résultat attendu** :
|
||||||
|
```
|
||||||
|
Name Type Status Total Used Available
|
||||||
|
local-lvm lvmthin active 119G 0B 119G
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Étape 2 : Migrer un container de **prox2** vers **prox** (sur LVM)
|
||||||
|
|
||||||
|
**Objectif** : Déplacer un container (ex: 103) de **prox2** vers **prox** en conservant le stockage `local-lvm`.
|
||||||
|
|
||||||
|
### Commandes à exécuter sur **prox2 (192.168.0.95)** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Arrêter le container (obligatoire pour la migration)
|
||||||
|
pct stop <CTID>
|
||||||
|
# Exemple :
|
||||||
|
pct stop 103
|
||||||
|
|
||||||
|
# 2. Lancer la migration vers prox
|
||||||
|
pct migrate <CTID> prox
|
||||||
|
# Exemple :
|
||||||
|
pct migrate 103 prox
|
||||||
|
|
||||||
|
# 3. Vérifier la migration sur prox
|
||||||
|
ssh root@192.168.0.94 "pct list"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Résultat attendu** :
|
||||||
|
- Le container apparaît sur **prox** avec le stockage `local-lvm`.
|
||||||
|
- Statut : `stopped` (à redémarrer manuellement).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 4. Redémarrer le container sur prox
|
||||||
|
pct start <CTID>
|
||||||
|
# Exemple :
|
||||||
|
pct start 103
|
||||||
|
|
||||||
|
# 5. Vérifier le statut
|
||||||
|
pct status <CTID>
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Problèmes courants** :
|
||||||
|
- **Erreur** : `storage 'local-lvm' does not exist` → Vérifier que `local-lvm` existe bien sur **prox** (Étape 1).
|
||||||
|
- **Erreur** : `migration aborted` → Vérifier la connectivité réseau entre les nœuds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 Étape 3 : Préparer le stockage ZFS pour les containers sur **prox** *(À SAUTER si déjà configuré)*
|
||||||
|
|
||||||
|
**Objectif** : Créer un stockage Proxmox pointant vers le dataset ZFS `rpool/data`.
|
||||||
|
|
||||||
|
⚠️ **À SAUTER** : Le stockage `pve-zfs` (pointant vers `/rpool/data`) existe déjà sur prox.
|
||||||
|
|
||||||
|
### Commandes à exécuter sur **prox (192.168.0.94)** *(uniquement si `pve-zfs` n'existe pas) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Vérifier que rpool/data existe et est monté
|
||||||
|
zfs list
|
||||||
|
# Doit afficher : rpool/data 96K 443G 96K /rpool/data
|
||||||
|
|
||||||
|
# 2. Créer un stockage Proxmox de type 'dir' pointant vers /rpool/data
|
||||||
|
# (ZFS est monté automatiquement, pas besoin de 'zfs mount')
|
||||||
|
pvesm add dir pve-zfs --path /rpool/data --content rootdir
|
||||||
|
|
||||||
|
# 3. Vérifier le stockage
|
||||||
|
pvesm status
|
||||||
|
```
|
||||||
|
|
||||||
|
**Résultat attendu** :
|
||||||
|
```
|
||||||
|
Name Type Status Total Used Available
|
||||||
|
pve-zfs dir active 447G 0B 447G
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔀 Étape 4 : Déplacer le container de LVM vers ZFS sur **prox**
|
||||||
|
|
||||||
|
**Objectif** : Déplacer le stockage du container de `local-lvm` vers `pve-zfs` (ZFS).
|
||||||
|
|
||||||
|
### Commandes à exécuter sur **prox (192.168.0.94)** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Arrêter le container
|
||||||
|
pct stop <CTID>
|
||||||
|
# Exemple :
|
||||||
|
pct stop 103
|
||||||
|
|
||||||
|
# 2. Déplacer le volume racine (rootfs) vers pve-zfs
|
||||||
|
pct move-volume <CTID> rootfs pve-zfs
|
||||||
|
# Exemple :
|
||||||
|
pct move-volume 103 rootfs pve-zfs
|
||||||
|
|
||||||
|
# 3. Redémarrer le container
|
||||||
|
pct start <CTID>
|
||||||
|
# Exemple :
|
||||||
|
pct start 103
|
||||||
|
|
||||||
|
# 4. Vérifier la configuration
|
||||||
|
pct config <CTID>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Résultat attendu** :
|
||||||
|
```
|
||||||
|
rootfs: pve-zfs:103/vm-103-disk-0.raw,size=2G
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Problèmes courants** :
|
||||||
|
- **Erreur** : `cannot move volumes of a running container` → Arrêter le container avant.
|
||||||
|
- **Erreur** : `storage 'rpool/data' contains illegal characters` → Utiliser le stockage `pve-zfs` (créé à l'Étape 3).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Étape 5 : Vérifications finales
|
||||||
|
|
||||||
|
### Sur **prox** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Vérifier que le container est en running
|
||||||
|
pct status <CTID>
|
||||||
|
|
||||||
|
# 2. Vérifier le stockage utilisé
|
||||||
|
pct config <CTID> | grep rootfs
|
||||||
|
|
||||||
|
# 3. Vérifier l'IP du container
|
||||||
|
pct exec <CTID> -- hostname -I
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sur **prox2** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Vérifier que le container a disparu
|
||||||
|
pct list
|
||||||
|
|
||||||
|
# 2. Nettoyer les anciens volumes (si nécessaire)
|
||||||
|
# Les anciens volumes sur local-lvm sont marqués comme 'unused' et peuvent être supprimés
|
||||||
|
pct config <CTID> # Vérifier les entrées 'unusedX'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Exemple complet : Migration du container 103 (AdGuard)
|
||||||
|
|
||||||
|
⚠️ **Note** : Pour le container 103, les stockages **n'existaient pas** sur prox. Pour les prochains containers (101, 102, 104, 105, 106), **les stockages existent déjà** → Sautez la partie "Préparation".
|
||||||
|
|
||||||
|
### Sur **prox (192.168.0.94)** - Préparation *(uniquement pour le premier container)* :
|
||||||
|
```bash
|
||||||
|
# Créer le stockage LVM
|
||||||
|
wipefs -a /dev/sdb
|
||||||
|
pvcreate /dev/sdb
|
||||||
|
vgcreate pve /dev/sdb
|
||||||
|
lvcreate -L 119G -n data pve --thinpool data
|
||||||
|
pvesm add lvmthin local-lvm --vgname pve --thinpool data
|
||||||
|
|
||||||
|
# Créer le stockage ZFS
|
||||||
|
pvesm add dir pve-zfs --path /rpool/data --content rootdir
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sur **prox2 (192.168.0.95)** - Migration :
|
||||||
|
```bash
|
||||||
|
pct stop 103
|
||||||
|
pct migrate 103 prox
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sur **prox (192.168.0.94)** - Déplacement vers ZFS :
|
||||||
|
```bash
|
||||||
|
pct stop 103
|
||||||
|
pct move-volume 103 rootfs pve-zfs
|
||||||
|
pct start 103
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérification :
|
||||||
|
```bash
|
||||||
|
pct config 103 | grep rootfs
|
||||||
|
# → rootfs: pve-zfs:103/vm-103-disk-0.raw,size=2G
|
||||||
|
pct status 103
|
||||||
|
# → status: running
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Procédure simplifiée pour les PROCHAINS containers (101, 102, 104, 105, 106)
|
||||||
|
|
||||||
|
**Les stockages `local-lvm` et `pve-zfs` existent déjà sur prox → Sautez les Étapes 1 et 3.**
|
||||||
|
|
||||||
|
### Commandes à exécuter :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sur prox2 (192.168.0.95) :
|
||||||
|
pct stop <CTID>
|
||||||
|
pct migrate <CTID> prox
|
||||||
|
|
||||||
|
# Sur prox (192.168.0.94) :
|
||||||
|
pct stop <CTID>
|
||||||
|
pct move-volume <CTID> rootfs pve-zfs
|
||||||
|
pct start <CTID>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Résumé des commandes clés
|
||||||
|
|
||||||
|
| Action | Commande | Nœud |
|
||||||
|
|--------|----------|------|
|
||||||
|
| Créer LVM | `pvcreate /dev/sdb; vgcreate pve /dev/sdb; lvcreate -L 119G -n data pve --thinpool data` | prox |
|
||||||
|
| Ajouter stockage LVM | `pvesm add lvmthin local-lvm --vgname pve --thinpool data` | prox |
|
||||||
|
| Ajouter stockage ZFS | `pvesm add dir pve-zfs --path /rpool/data --content rootdir` | prox |
|
||||||
|
| Arrêter container | `pct stop <CTID>` | prox2 ou prox |
|
||||||
|
| Migrer container | `pct migrate <CTID> prox` | prox2 |
|
||||||
|
| Déplacer vers ZFS | `pct move-volume <CTID> rootfs pve-zfs` | prox |
|
||||||
|
| Redémarrer container | `pct start <CTID>` | prox |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Notes importantes
|
||||||
|
|
||||||
|
1. **Compatibilité** : Cette procédure fonctionne pour les **containers LXC** uniquement. Pour les **VMs KVM**, utiliser `qm migrate`.
|
||||||
|
|
||||||
|
2. **Espace disque** : Vérifier que **prox** a assez d'espace sur `local-lvm` et `rpool/data` avant migration.
|
||||||
|
|
||||||
|
3. **Cluster** : La migration entre nœuds nécessite un **cluster Proxmox fonctionnel**.
|
||||||
|
|
||||||
|
4. **Stockage ZFS** : Le dataset `rpool/data` doit exister. Si ce n'est pas le cas, le créer avec :
|
||||||
|
```bash
|
||||||
|
zfs create rpool/data
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Nettoyage** : Après migration, les anciens volumes sur **prox2** (marqués `unusedX`) peuvent être supprimés avec :
|
||||||
|
```bash
|
||||||
|
pct set <CTID> --delete unused0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Références
|
||||||
|
- [Documentation Proxmox VE - LXC](https://pve.proxmox.com/wiki/Linear_Virtual_Environment_(LXC))
|
||||||
|
- [Documentation Proxmox VE - Storage](https://pve.proxmox.com/wiki/Storage)
|
||||||
|
- [Documentation Proxmox VE - Migration](https://pve.proxmox.com/wiki/Migration_of_virtual_guests)
|
||||||
Executable
+541
@@ -0,0 +1,541 @@
|
|||||||
|
# Mode opératoire - Réinstallation de prox2 (192.168.0.95) en ZFS
|
||||||
|
|
||||||
|
## Sommaire
|
||||||
|
- [⚠️ CORRECTION IMMÉDIATE DU REPOSITORY](#-correction-immédiate-du-repository-à-faire-en-premier-si-erreur-401)
|
||||||
|
- [Prérequis](#prérequis)
|
||||||
|
- [Étape 0 : Après installation - Correction définitive des dépôts](#étape-0--après-installation---correction-définitive-des-dépôts)
|
||||||
|
- [Étape 1 : Installation Proxmox VE](#étape-1--installation-proxmox-ve)
|
||||||
|
- [Étape 2 : Configuration réseau](#étape-2--configuration-réseau)
|
||||||
|
- [Étape 3 : Configuration SSH](#étape-3--configuration-ssh)
|
||||||
|
- [Étape 4 : Création des pools ZFS](#étape-4--création-des-pools-zfs)
|
||||||
|
- [Étape 5 : Rejoindre le cluster](#étape-5--rejoindre-le-cluster)
|
||||||
|
- [Étape 6 : Configuration des stockages](#étape-6--configuration-des-stockages)
|
||||||
|
- [Étape 7 : Configuration USB Zigbee](#étape-7--configuration-usb-zigbee)
|
||||||
|
- [Étape 8 : Vérifications finales](#étape-8--vérifications-finales)
|
||||||
|
- [Commandes complètes à copier-coller](#commandes-complètes-à-copier-coller)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ CORRECTION IMMÉDIATE DU REPOSITORY (à faire en premier si erreur 401)
|
||||||
|
|
||||||
|
Si vous voyez des erreurs `401 Unauthorized` avec les dépôts Proxmox, exécutez **immédiatement** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Supprimer le repository Enterprise (si présent)
|
||||||
|
rm -f /etc/apt/sources.list.d/pve-enterprise.list
|
||||||
|
|
||||||
|
# Ajouter le repository Community (Proxmox VE 9.x / Debian Bookworm)
|
||||||
|
echo "deb http://download.proxmox.com/debian/bookworm pve-no-subscription main" > /etc/apt/sources.list.d/pve-no-subscription.list
|
||||||
|
|
||||||
|
# Mettre à jour la liste des paquets
|
||||||
|
apt update
|
||||||
|
```
|
||||||
|
|
||||||
|
> **✅ Résultat** : Plus d'erreurs de souscription, accès complet aux mises à jour.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
- Serveur avec disque(s) compatible ZFS
|
||||||
|
- ISO Proxmox VE 9.2.x
|
||||||
|
- Accès réseau 192.168.0.0/24
|
||||||
|
- **prox (192.168.0.94)** et **Pi3 (192.168.0.156)** opérationnels
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étape 0 : Après installation - Correction définitive des dépôts
|
||||||
|
|
||||||
|
### ⚠️ À exécuter APRÈS l'installation (avant toute mise à jour) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Supprimer le fichier enterprise qui cause les erreurs 401
|
||||||
|
rm -f /etc/apt/sources.list.d/pve-enterprise.list
|
||||||
|
|
||||||
|
# 2. Créer le fichier no-subscription pour Proxmox VE 9.x (Bookworm)
|
||||||
|
cat > /etc/apt/sources.list.d/pve-no-subscription.list << 'EOF'
|
||||||
|
deb http://download.proxmox.com/debian/bookworm pve-no-subscription main
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 3. Vérifier qu'il n'y a plus de référence à enterprise
|
||||||
|
sed -i '/enterprise.proxmox.com/d' /etc/apt/sources.list
|
||||||
|
|
||||||
|
# 4. Mettre à jour
|
||||||
|
apt update
|
||||||
|
|
||||||
|
# 5. Vérifier qu'il n'y a plus d'erreurs
|
||||||
|
apt update 2>&1 | grep -i error || echo "✅ Aucun erreur - dépôts configurés correctement"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Pourquoi ?** : Proxmox installe par défaut le dépôt `enterprise` qui nécessite une licence payante. Sans abonnement, vous obtenez des erreurs 401. Le dépôt `pve-no-subscription` donne accès aux mêmes paquets, sans restriction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étape 1 : Installation Proxmox VE
|
||||||
|
|
||||||
|
### Dans l'installateur ISO :
|
||||||
|
```
|
||||||
|
Sélectionner : Install Proxmox VE
|
||||||
|
Target Disk: sda (disque système)
|
||||||
|
Filesystem: ZFS (RAID0)
|
||||||
|
ZFS Pool Name: rpool
|
||||||
|
Partition Scheme: Utiliser tout le disque
|
||||||
|
Country: France
|
||||||
|
Time zone: Europe/Paris
|
||||||
|
Keyboard Layout: fr
|
||||||
|
Hostname: prox2
|
||||||
|
IP Address: 192.168.0.95/24
|
||||||
|
Gateway: 192.168.0.254
|
||||||
|
DNS Server: 8.8.8.8
|
||||||
|
Mot de passe root: [votre mot de passe]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étape 2 : Configuration réseau
|
||||||
|
|
||||||
|
### Appliquer la configuration :
|
||||||
|
```bash
|
||||||
|
# Écrire /etc/hostname
|
||||||
|
cat > /etc/hostname << 'EOF'
|
||||||
|
prox2
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Écrire /etc/hosts
|
||||||
|
cat > /etc/hosts << 'EOF'
|
||||||
|
127.0.0.1 localhost.localdomain localhost
|
||||||
|
192.168.0.95 prox2.lab.peis.fr prox2
|
||||||
|
|
||||||
|
# Nœuds du cluster
|
||||||
|
192.168.0.94 prox
|
||||||
|
192.168.0.156 pi3
|
||||||
|
|
||||||
|
# IPv6
|
||||||
|
::1 ip6-localhost ip6-loopback
|
||||||
|
fe00::0 ip6-localnet
|
||||||
|
ff00::0 ip6-mcastprefix
|
||||||
|
ff02::1 ip6-allnodes
|
||||||
|
ff02::2 ip6-allrouters
|
||||||
|
ff02::3 ip6-allhosts
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Écrire /etc/network/interfaces (adapter l'interface si nécessaire)
|
||||||
|
cat > /etc/network/interfaces << 'EOF'
|
||||||
|
auto lo
|
||||||
|
iface lo inet loopback
|
||||||
|
|
||||||
|
iface enp1s0 inet manual
|
||||||
|
|
||||||
|
auto vmbr0
|
||||||
|
iface vmbr0 inet static
|
||||||
|
address 192.168.0.95/24
|
||||||
|
gateway 192.168.0.254
|
||||||
|
bridge-ports enp1s0
|
||||||
|
bridge-stp off
|
||||||
|
bridge-fd 0
|
||||||
|
|
||||||
|
iface wlp2s0 inet manual
|
||||||
|
|
||||||
|
source /etc/network/interfaces.d/*
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Redémarrer le réseau
|
||||||
|
systemctl restart networking
|
||||||
|
```
|
||||||
|
|
||||||
|
> **⚠️ Vérifier l'interface réseau avec `ip a` et adapter `enp1s0` si nécessaire.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étape 3 : Configuration SSH
|
||||||
|
|
||||||
|
### Appliquer la configuration :
|
||||||
|
```bash
|
||||||
|
# Créer le répertoire .ssh
|
||||||
|
mkdir -p /root/.ssh
|
||||||
|
chmod 700 /root/.ssh
|
||||||
|
|
||||||
|
# Écrire les clés autorisées
|
||||||
|
cat > /root/.ssh/authorized_keys << 'EOF'
|
||||||
|
# Clé depuis theo@peis.fr (ed25519 - PRINCIPALE À CONSERVER)
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG8eq44iROSYLJj0G5jNh2Py57/f2Z0LnTE7rPG2T6HH theo@peis.fr
|
||||||
|
|
||||||
|
# Clé depuis prox
|
||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCclm2iExnWguIEoZt5tYC8jaIZ3rUwVGmdsyMiYAMkWuQ32T5B1OLaqe7pBCwkahxAj6jva0JDHyiBQSl39ouWfIXzFk/HrCMbVuQzEnx5EKt5LYmnSajW+qi6YeApksp0GCcD7kOZQPN2/8EyGkk15L4sJMzP9zWu53ucEjxbuoRl7tmwmqmPLlAVtdGc5irE9om6yxLkUtFH/MfCjqJGtUiJXBvUNR99XTOAvzDEsWze5RFKjoy4gsKlI9AK80p9kzlBCdR62yRVzVyzUH/4kliRBV76C874/mWN3zrdRozm6qc0zNtXlG8/X+ndUkS7ULlJE5GRIf/DP1Wfbr+Y9lHrsEbQpweDjtKYrXlaqaXswUeLwzyFdb6ZDxfWFEhF9WlKhkZ89yv7370Nz3qAq9mX3+5WwYVuBwcK8/jZXZ+J1BB5yW/rHyI9l3NUQzFstfjOfes2T4Dk04sQ0zRR+7/3U6mUUBYJgPPk159WLABum5KqtPJ/B7c7d3drtlruwDOF7NCtbUc1dQsiJzhGR4f4KNvn251T//KX6Gqu1zdLozo5gTf49fJSW8RteafRDyXOhwpAnZDju9TKZSE4CpEitHqTFpfxvtLG/eFEm80/BU1nO2j+46/cyyAq/nuotu6IxIsWkIQSx4nD3HXrOQiv7lVx97iQ175Sbhu/lQ== root@prox
|
||||||
|
|
||||||
|
# Clé depuis prox2 (ancienne)
|
||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCyBNKBtZyeYC4KlNhfYgT2/fCnW8TEz7/Dl/s8C2ZkLqq5tCSf83Y3GKTOqsplhT31V7O+A8b7oGoAo/ltHmlvbFQh4jQQGfB897vLb7BfikBCEvCb+9Z+KyGJpFRT8f6ahjZJ/j6DaWoNq1bvJ7ZYy9o/tkMtxBEIXre/R/GPaHMlLRFDOyv1EItugLcRY+ri3aEZ/MK5ynkAYt/I3ZurJyVSi/kww8uRXcTzGtMnU/MaUmwYuPW7k2Oq61j/+QiMsWJnXa9sZfqD7KcsI7p7CzpjLeC9alcN+IlIcIksA5v5c4ia+4em7Phn4EO+WDiNSBFExXvM2HwyyBnlvRI+uvmqDGdN6GYRjptwBQ7/gB1nLQYwK/NWFSAWUzIAtV2Eo7dtC463uQfZCnGTPw2zIueHY7b1cGD6L/844XVGkbLaNXkrS0FqKFsIMXY99eug0iqrLfPBefZn4rV9M6CGUCypLadvXoKNHq60n8EjytwYU1tY6vqE1J+3fNldeL3jsRde4R2D3LwMy/3So+Xcke7mcs4sPqi4X1tsgqUH2JneuiaMx4Tb54L9wecZ4MYqrBmJDhw98HM5mSlJM8x5f0oT24/kxfRJByixi1Nz0/xsTf0wTrUBYmDxGQCkg/hfJgQ9M7ER38S+GMquGM/pXFK//blN5YGIg+2K4+Mu2w== root@pve
|
||||||
|
|
||||||
|
# Clé depuis adminpi3@Pi3
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAc8o6yCJlVNFKqCHdFKZ30o+TXyXrBsDyau1Ydn9PkF theo@peis.fr
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Définir les permissions
|
||||||
|
chmod 600 /root/.ssh/authorized_keys
|
||||||
|
|
||||||
|
# Configurer sshd pour autoriser root
|
||||||
|
sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
|
||||||
|
sed -i 's/^PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
|
||||||
|
|
||||||
|
# Redémarrer SSH
|
||||||
|
systemctl restart ssh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étape 4 : Création des pools ZFS
|
||||||
|
|
||||||
|
### Appliquer la configuration :
|
||||||
|
```bash
|
||||||
|
# Créer les datasets ZFS
|
||||||
|
zfs create rpool/data
|
||||||
|
zfs create rpool/vm
|
||||||
|
zfs create rpool/lxc
|
||||||
|
|
||||||
|
# Créer les points de montage
|
||||||
|
mkdir -p /mnt/pve/rpool-data
|
||||||
|
mount -t zfs rpool/data /mnt/pve/rpool-data
|
||||||
|
|
||||||
|
# Ajouter au fstab pour montage automatique
|
||||||
|
if ! grep -q "rpool/data" /etc/fstab; then
|
||||||
|
echo "rpool/data /mnt/pve/rpool-data zfs defaults 0 0" >> /etc/fstab
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étape 5 : Rejoindre le cluster
|
||||||
|
|
||||||
|
### Appliquer la configuration :
|
||||||
|
```bash
|
||||||
|
# Rejoindre le cluster (exécuté depuis prox2)
|
||||||
|
pvecm add 192.168.0.94
|
||||||
|
|
||||||
|
# Vérifier et configurer le Qdevice si nécessaire
|
||||||
|
pvecm qdevice status
|
||||||
|
# Si non configuré :
|
||||||
|
pvecm qdevice setup 192.168.0.156
|
||||||
|
```
|
||||||
|
|
||||||
|
> **⚠️ Vérifier que le cluster est en quorum avant de continuer :** `pvecm status`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étape 6 : Configuration des stockages
|
||||||
|
|
||||||
|
### Appliquer la configuration :
|
||||||
|
```bash
|
||||||
|
# Écrire /etc/pve/storage.cfg
|
||||||
|
cat > /etc/pve/storage.cfg << 'EOF'
|
||||||
|
dir: local
|
||||||
|
path /var/lib/vz
|
||||||
|
content vztmpl,iso,backup
|
||||||
|
|
||||||
|
nfs: nas-C1-1F-5A
|
||||||
|
export /data/Frigate
|
||||||
|
path /mnt/pve/nas-C1-1F-5A
|
||||||
|
server 192.168.0.184
|
||||||
|
content images,backup
|
||||||
|
prune-backups keep-all=1
|
||||||
|
|
||||||
|
nfs: NAS_backup
|
||||||
|
export /data/Proxmox_backup
|
||||||
|
path /mnt/pve/NAS_backup
|
||||||
|
server 192.168.0.184
|
||||||
|
content images,backup,iso
|
||||||
|
prune-backups keep-all=1
|
||||||
|
|
||||||
|
# Stockage ZFS pour les containers LXC
|
||||||
|
dir: pve-zfs
|
||||||
|
path /mnt/pve/rpool-data
|
||||||
|
content rootdir
|
||||||
|
|
||||||
|
# Stockage ZFS pour les VMs
|
||||||
|
zfspool: vm-zfs
|
||||||
|
pool rpool/vm
|
||||||
|
content images,rootdir
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Créer les points de montage NFS
|
||||||
|
mkdir -p /mnt/pve/nas-C1-1F-5A
|
||||||
|
mkdir -p /mnt/pve/NAS_backup
|
||||||
|
|
||||||
|
# Monter les partages NFS
|
||||||
|
mount -t nfs 192.168.0.184:/data/Frigate /mnt/pve/nas-C1-1F-5A
|
||||||
|
mount -t nfs 192.168.0.184:/data/Proxmox_backup /mnt/pve/NAS_backup
|
||||||
|
|
||||||
|
# Ajouter au fstab
|
||||||
|
if ! grep -q "192.168.0.184:/data/Frigate" /etc/fstab; then
|
||||||
|
echo "192.168.0.184:/data/Frigate /mnt/pve/nas-C1-1F-5A nfs defaults 0 0" >> /etc/fstab
|
||||||
|
fi
|
||||||
|
if ! grep -q "192.168.0.184:/data/Proxmox_backup" /etc/fstab; then
|
||||||
|
echo "192.168.0.184:/data/Proxmox_backup /mnt/pve/NAS_backup nfs defaults 0 0" >> /etc/fstab
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étape 7 : Configuration USB Zigbee
|
||||||
|
|
||||||
|
### Pour le container Zigbee2MQTT (CTID 106) :
|
||||||
|
```bash
|
||||||
|
# Configurer les permissions USB et montages
|
||||||
|
pct set 106 --cgroup2-devices.allow c 188:* rwm
|
||||||
|
pct set 106 --cgroup2-devices.allow c 189:* rwm
|
||||||
|
pct set 106 --cgroup2-devices.allow c 10:200 rwm
|
||||||
|
pct set 106 --mount 0: /dev/serial/by-id,dev/serial/by-id,none,bind,optional,create=dir
|
||||||
|
pct set 106 --mount 1: /dev/ttyUSB0,dev/ttyUSB0,none,bind,optional,create=file
|
||||||
|
pct set 106 --mount 2: /dev/ttyUSB1,dev/ttyUSB1,none,bind,optional,create=file
|
||||||
|
pct set 106 --mount 3: /dev/ttyACM0,dev/ttyACM0,none,bind,optional,create=file
|
||||||
|
pct set 106 --mount 4: /dev/ttyACM1,dev/ttyACM1,none,bind,optional,create=file
|
||||||
|
pct set 106 --mount 5: /dev/net/tun,dev/net/tun,none,bind,create=file
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérification :
|
||||||
|
```bash
|
||||||
|
# Vérifier la détection USB
|
||||||
|
lsusb
|
||||||
|
ls -la /dev/serial/by-id
|
||||||
|
|
||||||
|
# Redémarrer le container
|
||||||
|
pct restart 106
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étape 8 : Vérifications finales
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Vérifier le cluster
|
||||||
|
echo "=== État du cluster ==="
|
||||||
|
pvecm status
|
||||||
|
|
||||||
|
# 2. Vérifier les stockages
|
||||||
|
echo -e "\n=== Stockages ==="
|
||||||
|
pvesm status
|
||||||
|
|
||||||
|
# 3. Vérifier la version
|
||||||
|
echo -e "\n=== Version Proxmox ==="
|
||||||
|
pveversion
|
||||||
|
|
||||||
|
# 4. Vérifier la connectivité SSH (depuis un autre terminal)
|
||||||
|
# ssh root@192.168.0.95
|
||||||
|
|
||||||
|
# 5. Vérifier l'USB dans le container 106
|
||||||
|
echo -e "\n=== USB dans container 106 ==="
|
||||||
|
pct enter 106 ls -la /dev/ttyUSB* /dev/serial/by-id 2>/dev/null || echo "Container non accessible ou pas de périphérique"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-installation recommandée
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Mettre à jour le système (APRÈS avoir corrigé le repository)
|
||||||
|
apt update
|
||||||
|
apt dist-upgrade -y
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt upgrade -y
|
||||||
|
|
||||||
|
# Redémarrer si nécessaire (pour appliquer le nouveau noyau)
|
||||||
|
# reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
> **⚠️ IMPORTANT** : La correction du repository Enterprise doit être faite **AVANT** cette étape. Voir [Étape 0](#étape-0--après-installation---correction-définitive-des-dépôts) ou la section [CORRECTION IMMÉDIATE](#-correction-immédiate-du-repository-à-faire-en-premier-si-erreur-401).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commandes complètes à copier-coller
|
||||||
|
|
||||||
|
### Tout en une seule fois (exécuter étape par étape) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== [1/8] Configuration réseau ==="
|
||||||
|
cat > /etc/hostname << 'EOF'
|
||||||
|
prox2
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > /etc/hosts << 'EOF'
|
||||||
|
127.0.0.1 localhost.localdomain localhost
|
||||||
|
192.168.0.95 prox2.lab.peis.fr prox2
|
||||||
|
192.168.0.94 prox
|
||||||
|
192.168.0.156 pi3
|
||||||
|
::1 ip6-localhost ip6-loopback
|
||||||
|
fe00::0 ip6-localnet
|
||||||
|
ff00::0 ip6-mcastprefix
|
||||||
|
ff02::1 ip6-allnodes
|
||||||
|
ff02::2 ip6-allrouters
|
||||||
|
ff02::3 ip6-allhosts
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Détecter l'interface réseau principale
|
||||||
|
INTERFACE=$(ip -o addr show | awk '/192.168.0/ {print $2}' | head -1)
|
||||||
|
if [ -z "$INTERFACE" ]; then
|
||||||
|
INTERFACE="enp1s0"
|
||||||
|
echo "Interface non détectée, utilisation par défaut: $INTERFACE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > /etc/network/interfaces << EOF
|
||||||
|
auto lo
|
||||||
|
iface lo inet loopback
|
||||||
|
|
||||||
|
iface $INTERFACE inet manual
|
||||||
|
|
||||||
|
auto vmbr0
|
||||||
|
iface vmbr0 inet static
|
||||||
|
address 192.168.0.95/24
|
||||||
|
gateway 192.168.0.254
|
||||||
|
bridge-ports $INTERFACE
|
||||||
|
bridge-stp off
|
||||||
|
bridge-fd 0
|
||||||
|
|
||||||
|
source /etc/network/interfaces.d/*
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl restart networking
|
||||||
|
|
||||||
|
echo "=== [2/8] Configuration SSH ==="
|
||||||
|
mkdir -p /root/.ssh
|
||||||
|
chmod 700 /root/.ssh
|
||||||
|
|
||||||
|
cat > /root/.ssh/authorized_keys << 'EOF'
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG8eq44iROSYLJj0G5jNh2Py57/f2Z0LnTE7rPG2T6HH theo@peis.fr
|
||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCclm2iExnWguIEoZt5tYC8jaIZ3rUwVGmdsyMiYAMkWuQ32T5B1OLaqe7pBCwkahxAj6jva0JDHyiBQSl39ouWfIXzFk/HrCMbVuQzEnx5EKt5LYmnSajW+qi6YeApksp0GCcD7kOZQPN2/8EyGkk15L4sJMzP9zWu53ucEjxbuoRl7tmwmqmPLlAVtdGc5irE9om6yxLkUtFH/MfCjqJGtUiJXBvUNR99XTOAvzDEsWze5RFKjoy4gsKlI9AK80p9kzlBCdR62yRVzVyzUH/4kliRBV76C874/mWN3zrdRozm6qc0zNtXlG8/X+ndUkS7ULlJE5GRIf/DP1Wfbr+Y9lHrsEbQpweDjtKYrXlaqaXswUeLwzyFdb6ZDxfWFEhF9WlKhkZ89yv7370Nz3qAq9mX3+5WwYVuBwcK8/jZXZ+J1BB5yW/rHyI9l3NUQzFstfjOfes2T4Dk04sQ0zRR+7/3U6mUUBYJgPPk159WLABum5KqtPJ/B7c7d3drtlruwDOF7NCtbUc1dQsiJzhGR4f4KNvn251T//KX6Gqu1zdLozo5gTf49fJSW8RteafRDyXOhwpAnZDju9TKZSE4CpEitHqTFpfxvtLG/eFEm80/BU1nO2j+46/cyyAq/nuotu6IxIsWkIQSx4nD3HXrOQiv7lVx97iQ175Sbhu/lQ== root@prox
|
||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCyBNKBtZyeYC4KlNhfYgT2/fCnW8TEz7/Dl/s8C2ZkLqq5tCSf83Y3GKTOqsplhT31V7O+A8b7oGoAo/ltHmlvbFQh4jQQGfB897vLb7BfikBCEvCb+9Z+KyGJpFRT8f6ahjZJ/j6DaWoNq1bvJ7ZYy9o/tkMtxBEIXre/R/GPaHMlLRFDOyv1EItugLcRY+ri3aEZ/MK5ynkAYt/I3ZurJyVSi/kww8uRXcTzGtMnU/MaUmwYuPW7k2Oq61j/+QiMsWJnXa9sZfqD7KcsI7p7CzpjLeC9alcN+IlIcIksA5v5c4ia+4em7Phn4EO+WDiNSBFExXvM2HwyyBnlvRI+uvmqDGdN6GYRjptwBQ7/gB1nLQYwK/NWFSAWUzIAtV2Eo7dtC463uQfZCnGTPw2zIueHY7b1cGD6L/844XVGkbLaNXkrS0FqKFsIMXY99eug0iqrLfPBefZn4rV9M6CGUCypLadvXoKNHq60n8EjytwYU1tY6vqE1J+3fNldeL3jsRde4R2D3LwMy/3So+Xcke7mcs4sPqi4X1tsgqUH2JneuiaMx4Tb54L9wecZ4MYqrBmJDhw98HM5mSlJM8x5f0oT24/kxfRJByixi1Nz0/xsTf0wTrUBYmDxGQCkg/hfJgQ9M7ER38S+GMquGM/pXFK//blN5YGIg+2K4+Mu2w== root@pve
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAc8o6yCJlVNFKqCHdFKZ30o+TXyXrBsDyau1Ydn9PkF theo@peis.fr
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 600 /root/.ssh/authorized_keys
|
||||||
|
sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
|
||||||
|
sed -i 's/^PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
|
||||||
|
systemctl restart ssh
|
||||||
|
|
||||||
|
echo "=== [3/8] Création des pools ZFS ==="
|
||||||
|
zfs create rpool/data
|
||||||
|
zfs create rpool/vm
|
||||||
|
zfs create rpool/lxc
|
||||||
|
mkdir -p /mnt/pve/rpool-data
|
||||||
|
mount -t zfs rpool/data /mnt/pve/rpool-data
|
||||||
|
if ! grep -q "rpool/data" /etc/fstab; then
|
||||||
|
echo "rpool/data /mnt/pve/rpool-data zfs defaults 0 0" >> /etc/fstab
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== [4/8] Rejoindre le cluster (attendre que prox soit accessible) ==="
|
||||||
|
echo "Exécuter manuellement après vérification :"
|
||||||
|
echo " pvecm add 192.168.0.94"
|
||||||
|
echo " pvecm qdevice status"
|
||||||
|
echo " pvecm qdevice setup 192.168.0.156 # si nécessaire"
|
||||||
|
|
||||||
|
echo "=== [5/8] Configuration des stockages ==="
|
||||||
|
cat > /etc/pve/storage.cfg << 'EOF'
|
||||||
|
dir: local
|
||||||
|
path /var/lib/vz
|
||||||
|
content vztmpl,iso,backup
|
||||||
|
|
||||||
|
nfs: nas-C1-1F-5A
|
||||||
|
export /data/Frigate
|
||||||
|
path /mnt/pve/nas-C1-1F-5A
|
||||||
|
server 192.168.0.184
|
||||||
|
content images,backup
|
||||||
|
prune-backups keep-all=1
|
||||||
|
|
||||||
|
nfs: NAS_backup
|
||||||
|
export /data/Proxmox_backup
|
||||||
|
path /mnt/pve/NAS_backup
|
||||||
|
server 192.168.0.184
|
||||||
|
content images,backup,iso
|
||||||
|
prune-backups keep-all=1
|
||||||
|
|
||||||
|
dir: pve-zfs
|
||||||
|
path /mnt/pve/rpool-data
|
||||||
|
content rootdir
|
||||||
|
|
||||||
|
zfspool: vm-zfs
|
||||||
|
pool rpool/vm
|
||||||
|
content images,rootdir
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mkdir -p /mnt/pve/nas-C1-1F-5A /mnt/pve/NAS_backup
|
||||||
|
mount -t nfs 192.168.0.184:/data/Frigate /mnt/pve/nas-C1-1F-5A
|
||||||
|
mount -t nfs 192.168.0.184:/data/Proxmox_backup /mnt/pve/NAS_backup
|
||||||
|
if ! grep -q "192.168.0.184:/data/Frigate" /etc/fstab; then
|
||||||
|
echo "192.168.0.184:/data/Frigate /mnt/pve/nas-C1-1F-5A nfs defaults 0 0" >> /etc/fstab
|
||||||
|
fi
|
||||||
|
if ! grep -q "192.168.0.184:/data/Proxmox_backup" /etc/fstab; then
|
||||||
|
echo "192.168.0.184:/data/Proxmox_backup /mnt/pve/NAS_backup nfs defaults 0 0" >> /etc/fstab
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== [6/8] Configuration USB Zigbee ==="
|
||||||
|
echo "Exécuter après création du container 106 (Zigbee2MQTT) :"
|
||||||
|
echo "pct set 106 --cgroup2-devices.allow c 188:* rwm"
|
||||||
|
echo "pct set 106 --cgroup2-devices.allow c 189:* rwm"
|
||||||
|
echo "pct set 106 --cgroup2-devices.allow c 10:200 rwm"
|
||||||
|
echo "pct set 106 --mount 0: /dev/serial/by-id,dev/serial/by-id,none,bind,optional,create=dir"
|
||||||
|
echo "pct set 106 --mount 1: /dev/ttyUSB0,dev/ttyUSB0,none,bind,optional,create=file"
|
||||||
|
echo "pct set 106 --mount 2: /dev/ttyUSB1,dev/ttyUSB1,none,bind,optional,create=file"
|
||||||
|
echo "pct set 106 --mount 3: /dev/ttyACM0,dev/ttyACM0,none,bind,optional,create=file"
|
||||||
|
echo "pct set 106 --mount 4: /dev/ttyACM1,dev/ttyACM1,none,bind,optional,create=file"
|
||||||
|
echo "pct set 106 --mount 5: /dev/net/tun,dev/net/tun,none,bind,create=file"
|
||||||
|
|
||||||
|
echo "=== [0/8] Correction du repository Enterprise ==="
|
||||||
|
rm -f /etc/apt/sources.list.d/pve-enterprise.list
|
||||||
|
cat > /etc/apt/sources.list.d/pve-no-subscription.list << 'EOF'
|
||||||
|
deb http://download.proxmox.com/debian/bookworm pve-no-subscription main
|
||||||
|
EOF
|
||||||
|
sed -i '/enterprise.proxmox.com/d' /etc/apt/sources.list
|
||||||
|
apt update
|
||||||
|
|
||||||
|
echo "=== [7/8] Mise à jour du système ==="
|
||||||
|
apt update
|
||||||
|
apt dist-upgrade -y
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt upgrade -y
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== [8/8] VÉRIFICATIONS FINALES ==="
|
||||||
|
echo "Exécuter ces commandes pour vérifier :"
|
||||||
|
echo " pvecm status"
|
||||||
|
echo " pvesm status"
|
||||||
|
echo " pveversion"
|
||||||
|
echo " lsusb"
|
||||||
|
echo " ssh root@192.168.0.95 # depuis un autre terminal"
|
||||||
|
echo ""
|
||||||
|
echo "✅ Installation de base terminée !"
|
||||||
|
echo "⚠️ Penser à :"
|
||||||
|
echo " 0. CORRIGER LE REPOSITORY avec 'rm -f /etc/apt/sources.list.d/pve-enterprise.list && echo deb... > pve-no-subscription.list && apt update'"
|
||||||
|
echo " 1. Rejoindre le cluster avec 'pvecm add 192.168.0.94'"
|
||||||
|
echo " 2. Configurer le Qdevice avec 'pvecm qdevice setup 192.168.0.156'"
|
||||||
|
echo " 3. Configurer le container 106 pour l'USB Zigbee"
|
||||||
|
echo " 4. Redémarrer avec 'reboot' si nécessaire"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Résumé des fichiers de configuration
|
||||||
|
|
||||||
|
| Fichier | Contenu | Commande d'application |
|
||||||
|
|--------|---------|------------------------|
|
||||||
|
| `/etc/hostname` | `prox2` | `cat > /etc/hostname` |
|
||||||
|
| `/etc/hosts` | Voir section | `cat > /etc/hosts` |
|
||||||
|
| `/etc/network/interfaces` | Voir section | `cat > /etc/network/interfaces` puis `systemctl restart networking` |
|
||||||
|
| `/root/.ssh/authorized_keys` | 4 clés SSH | `cat > /root/.ssh/authorized_keys` puis `chmod 600` |
|
||||||
|
| `/etc/ssh/sshd_config` | `PermitRootLogin yes` | `sed -i` puis `systemctl restart ssh` |
|
||||||
|
| `/etc/apt/sources.list.d/pve-enterprise.list` | **À SUPPRIMER** | `rm -f /etc/apt/sources.list.d/pve-enterprise.list` |
|
||||||
|
| `/etc/apt/sources.list.d/pve-no-subscription.list` | Repository community | `echo "deb http://..." > pve-no-subscription.list` puis `apt update` |
|
||||||
|
| `/etc/pve/corosync.conf` | Configuration cluster | Créé automatiquement par `pvecm add` |
|
||||||
|
| `/etc/pve/storage.cfg` | 5 stockages | `cat > /etc/pve/storage.cfg` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes importantes
|
||||||
|
|
||||||
|
1. **Ordre d'exécution** : Suivre les étapes dans l'ordre indiqué
|
||||||
|
2. **Cluster** : Ne PAS joindre le cluster avant d'avoir configuré le réseau et SSH
|
||||||
|
3. **ZFS** : Les pools `rpool/data`, `rpool/vm`, `rpool/lxc` doivent exister avant de créer les stockages
|
||||||
|
4. **NFS** : Le serveur 192.168.0.184 doit être accessible
|
||||||
|
5. **Qdevice** : Pi3 (192.168.0.156) doit être opérationnel avec le certificat TLS
|
||||||
|
6. **Container 106** : Doit exister avant de configurer l'USB Zigbee
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document généré le 03/07/2026 - Mode opératoire pour réinstallation de prox2*
|
||||||
Executable
+318
@@ -0,0 +1,318 @@
|
|||||||
|
# Service Whoami - Test et Validation Traefik
|
||||||
|
|
||||||
|
## 📋 Description
|
||||||
|
|
||||||
|
**Whoami** est un service simple de Traefik qui renvoie des informations sur la requête HTTP reçue. Il est utilisé comme service de test pour valider que Traefik et Let's Encrypt sont correctement configurés.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Fonctionnalités
|
||||||
|
|
||||||
|
- Affiche l'adresse IP source
|
||||||
|
- Affiche les headers HTTP
|
||||||
|
- Affiche l'hôte et le chemin de la requête
|
||||||
|
- Utilisé pour tester la configuration reverse proxy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📡 Accès
|
||||||
|
|
||||||
|
- **URL**: https://test.peis.fr
|
||||||
|
- **Type**: HTTPS (certificat Let's Encrypt **production**)
|
||||||
|
- **Service**: whoami (traefik/whoami)
|
||||||
|
- **Réseau**: traefik_network
|
||||||
|
- **Conteneur**: whoami
|
||||||
|
- **IP Traefik**: 192.168.0.100 (CT 200 - traefik-proxy)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Déploiement
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
|
||||||
|
- Traefik déployé et fonctionnel (voir [procédure HTTPS homelab](procedure-https-homelab-infomaniak.md))
|
||||||
|
- Réseau Docker `traefik_network` créé
|
||||||
|
- Port forwarding 80/443 configuré sur le routeur
|
||||||
|
|
||||||
|
### Commandes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Déployer le service whoami
|
||||||
|
cd /opt/traefik
|
||||||
|
docker-compose -f docker-compose-whoami.yml up -d
|
||||||
|
|
||||||
|
# Vérifier que le conteneur est en cours d'exécution
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
# Voir les logs
|
||||||
|
docker logs whoami
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Fichier docker-compose-whoami.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
whoami:
|
||||||
|
image: traefik/whoami
|
||||||
|
container_name: whoami
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
# Router HTTP → HTTPS (redirection automatique)
|
||||||
|
- "traefik.http.routers.whoami-http.rule=Host(`test.peis.fr`)"
|
||||||
|
- "traefik.http.routers.whoami-http.entrypoints=web"
|
||||||
|
- "traefik.http.routers.whoami-http.middlewares=redirect-to-https@file"
|
||||||
|
# Router HTTPS (principal)
|
||||||
|
- "traefik.http.routers.whoami.rule=Host(`test.peis.fr`)"
|
||||||
|
- "traefik.http.routers.whoami.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.whoami.tls=true"
|
||||||
|
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
|
||||||
|
# Configuration du service
|
||||||
|
- "traefik.http.services.whoami.loadbalancer.server.port=80"
|
||||||
|
networks:
|
||||||
|
- traefik_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik_network:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **Note** : Le sous-domaine `test.peis.fr` doit exister dans votre DNS Infomaniak avec un enregistrement CNAME pointant vers `lab.peis.fr` (qui pointe vers 82.67.139.7).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Validation
|
||||||
|
|
||||||
|
### 1. Tester la connectivité HTTP (redirection)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis un terminal interne ou externe
|
||||||
|
curl -v http://test.peis.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
**Résultat attendu**: Redirection 301 vers `https://test.peis.fr`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Tester HTTPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis un navigateur
|
||||||
|
Ouvrir : https://test.peis.fr
|
||||||
|
|
||||||
|
# Depuis un terminal
|
||||||
|
curl -vk https://test.peis.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
**Résultat attendu** : Réponse JSON avec les informations de la requête
|
||||||
|
|
||||||
|
Exemple de réponse :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"host": "freebox.peis.fr",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"headers": {
|
||||||
|
"Accept": ["*/*"],
|
||||||
|
"User-Agent": ["curl/7.88.1"],
|
||||||
|
"X-Forwarded-For": ["192.168.0.1"],
|
||||||
|
"X-Forwarded-Host": ["freebox.peis.fr"],
|
||||||
|
"X-Forwarded-Port": ["443"],
|
||||||
|
"X-Forwarded-Proto": ["https"],
|
||||||
|
"X-Forwarded-Server": ["traefik"],
|
||||||
|
"X-Real-Ip": ["192.168.0.1"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Vérifier le certificat SSL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis un navigateur
|
||||||
|
1. Ouvrir https://test.peis.fr
|
||||||
|
2. Cliquer sur l'icône 🔒 dans la barre d'adresse
|
||||||
|
3. Vérifier que le certificat est émis par "Let's Encrypt"
|
||||||
|
|
||||||
|
# Depuis un terminal (openssl)
|
||||||
|
openssl s_client -connect test.peis.fr:443 -servername test.peis.fr | openssl x509 -noout -text | grep -A 5 "Issuer:"
|
||||||
|
```
|
||||||
|
|
||||||
|
**En mode production** : Le certificat sera émis par "Let's Encrypt"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Vérifier dans les logs Traefik
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Voir les logs du conteneur Traefik
|
||||||
|
docker logs traefik-proxy | grep -i certificate
|
||||||
|
|
||||||
|
# Voir les certificats obtenus
|
||||||
|
docker exec traefik-proxy cat /etc/traefik/certs/acme.json | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
**Résultat attendu** :
|
||||||
|
```
|
||||||
|
legolog: [INFO] [test.peis.fr] acme: Obtaining bundled SAN certificate
|
||||||
|
legolog: [INFO] [test.peis.fr] acme: Validations succeeded
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Dépannage
|
||||||
|
|
||||||
|
### Problème : Certificat non généré
|
||||||
|
|
||||||
|
**Symptômes** :
|
||||||
|
- Le site n'est pas accessible en HTTPS
|
||||||
|
- Erreur de certificat dans le navigateur
|
||||||
|
- Logs Traefik : "Unable to obtain ACME certificate"
|
||||||
|
|
||||||
|
**Solutions** :
|
||||||
|
|
||||||
|
1. **Vérifier le port forwarding** :
|
||||||
|
```bash
|
||||||
|
# Depuis une machine externe (pas votre réseau local)
|
||||||
|
curl -v http://test.peis.fr/.well-known/acme-challenge/test
|
||||||
|
```
|
||||||
|
→ Doit retourner une réponse HTTP (même une 404)
|
||||||
|
|
||||||
|
2. **Vérifier que Traefik écoute sur le port 80** :
|
||||||
|
```bash
|
||||||
|
docker exec traefik-proxy netstat -tuln | grep 80
|
||||||
|
```
|
||||||
|
→ Doit afficher `:::80` ou `0.0.0.0:80`
|
||||||
|
|
||||||
|
3. **Vérifier que le DNS pointe vers la bonne IP** :
|
||||||
|
```bash
|
||||||
|
dig +short test.peis.fr
|
||||||
|
```
|
||||||
|
→ Doit retourner `82.67.139.7` (via CNAME lab.peis.fr)
|
||||||
|
|
||||||
|
4. **Vérifier que Let's Encrypt n'est pas rate limité** :
|
||||||
|
- Attendez 1 heure si vous avez fait beaucoup de tests
|
||||||
|
|
||||||
|
5. **Vérifier la configuration Let's Encrypt** :
|
||||||
|
```bash
|
||||||
|
docker exec traefik-proxy cat /etc/traefik/traefik.yml | grep -A 5 certificatesResolvers
|
||||||
|
```
|
||||||
|
→ Doit afficher `email: theo@peis.fr` et **pas** de `caServer: acme-staging`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Problème : 404 Not Found
|
||||||
|
|
||||||
|
**Symptômes** :
|
||||||
|
- La page renvoie 404
|
||||||
|
- Pas de réponse whoami
|
||||||
|
|
||||||
|
**Solutions** :
|
||||||
|
|
||||||
|
1. **Vérifier que le conteneur whoami est en cours d'exécution** :
|
||||||
|
```bash
|
||||||
|
docker ps | grep whoami
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Vérifier que whoami est sur le réseau traefik_network** :
|
||||||
|
```bash
|
||||||
|
docker inspect whoami | grep Network
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Vérifier que Traefik détecte le service** :
|
||||||
|
```bash
|
||||||
|
docker logs traefik-proxy | grep whoami
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Vérifier les routes Traefik** :
|
||||||
|
```bash
|
||||||
|
docker exec traefik-proxy traefik get routers | grep whoami
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Problème : Erreur de connection
|
||||||
|
|
||||||
|
**Symptômes** :
|
||||||
|
- Impossible de se connecter à freebox.peis.fr
|
||||||
|
- Timeout ou connection refused
|
||||||
|
|
||||||
|
**Solutions** :
|
||||||
|
|
||||||
|
1. **Vérifier que le CT 200 est démarré** :
|
||||||
|
```bash
|
||||||
|
pct list | grep 200
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Vérifier que Docker est en cours d'exécution dans le CT** :
|
||||||
|
```bash
|
||||||
|
ssh root@192.168.0.100
|
||||||
|
docker ps
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Vérifier que Traefik est en cours d'exécution** :
|
||||||
|
```bash
|
||||||
|
docker ps | grep traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Vérifier le port forwarding sur le routeur** :
|
||||||
|
- TCP 80 → 192.168.0.100
|
||||||
|
- TCP 443 → 192.168.0.100
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Exemples d'utilisation
|
||||||
|
|
||||||
|
### Exemple 1 : Tester depuis un navigateur
|
||||||
|
|
||||||
|
1. Ouvrir https://test.peis.fr
|
||||||
|
2. Vous devriez voir une page JSON avec les détails de votre requête
|
||||||
|
3. Vérifier que l'URL est bien en HTTPS et que le cadenas est présent
|
||||||
|
|
||||||
|
### Exemple 2 : Tester avec curl depuis une machine externe
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis un smartphone en 4G ou une machine en dehors de votre réseau
|
||||||
|
curl -v https://test.peis.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemple 3 : Tester avec différents headers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "X-Custom-Header: test-value" https://test.peis.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Vous devriez voir `"X-Custom-Header": ["test-value"]` dans la réponse JSON
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗑️ Suppression
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Arrêter et supprimer le conteneur whoami
|
||||||
|
cd /opt/traefik
|
||||||
|
docker-compose -f docker-compose-whoami.yml down
|
||||||
|
|
||||||
|
# Supprimer le fichier de configuration (optionnel)
|
||||||
|
rm docker-compose-whoami.yml
|
||||||
|
|
||||||
|
# Redémarrer whoami (après modification)
|
||||||
|
docker-compose -f docker-compose-whoami.yml up -d --force-recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Liens utiles
|
||||||
|
|
||||||
|
- [Image Docker traefik/whoami](https://hub.docker.com/r/traefik/whoami)
|
||||||
|
- [Documentation Traefik](https://doc.traefik.io/traefik/)
|
||||||
|
- [Let's Encrypt Status](https://letsencrypt.status.io/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Documentation générée le 17/07/2026 pour ton homelab*
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
https://www.domo-blog.fr/domotique-indestructible-creez-un-cluster-proxmox-a-haute-disponibilite-pour-home-assistant/
|
||||||
Executable
+340
@@ -0,0 +1,340 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import threading
|
||||||
|
|
||||||
|
VERSION = "1.0.0"
|
||||||
|
|
||||||
|
SERVICES = {
|
||||||
|
"proxmox-prox2": {"name": "Proxmox Principal", "type": "ssh", "host": "192.168.0.95", "port": 22},
|
||||||
|
"proxmox-prox": {"name": "Proxmox Secondaire", "type": "ssh", "host": "192.168.0.94", "port": 22},
|
||||||
|
"vm100-haos": {"name": "Home Assistant OS", "type": "http", "host": "192.168.0.43", "port": 8123, "url": "/"},
|
||||||
|
"ct101-mqtt": {"name": "MQTT", "type": "ssh", "host": "192.168.0.5", "port": 22},
|
||||||
|
"ct102-frigate": {"name": "Frigate", "type": "http", "host": "192.168.0.91", "port": 5000, "url": "/"},
|
||||||
|
"ct103-adguard": {"name": "AdGuard", "type": "http", "host": "192.168.0.92", "port": 80, "url": "/"},
|
||||||
|
"ct105-alpine": {"name": "Alpine-Docker", "type": "ssh", "host": "192.168.0.155", "port": 22},
|
||||||
|
"ct106-zigbee": {"name": "Zigbee2MQTT", "type": "http", "host": "192.168.0.177", "port": 8081, "url": "/"},
|
||||||
|
"ct200-traefik": {"name": "Traefik Proxy", "type": "https", "host": "192.168.0.100", "port": 443, "url": "/", "insecure": True},
|
||||||
|
"ha-peis-fr": {"name": "ha.peis.fr", "type": "https", "host": "ha.peis.fr", "port": 443, "url": "/", "insecure": True},
|
||||||
|
"traefik-dash": {"name": "traefik.peis.fr", "type": "https", "host": "traefik.peis.fr", "port": 443, "url": "/dashboard/", "insecure": True},
|
||||||
|
"pi3": {"name": "Raspberry Pi 3", "type": "ssh", "host": "192.168.0.156", "port": 22},
|
||||||
|
}
|
||||||
|
|
||||||
|
status_history = {}
|
||||||
|
status_cache = {}
|
||||||
|
lock = threading.Lock()
|
||||||
|
|
||||||
|
def check_ssh(host, port, timeout=3):
|
||||||
|
try:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
result = sock.connect_ex((host, port))
|
||||||
|
sock.close()
|
||||||
|
return {"status": "online", "error": None}
|
||||||
|
except socket.timeout:
|
||||||
|
return {"status": "timeout", "error": "Connection timed out"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "offline", "error": str(e)[:100]}
|
||||||
|
|
||||||
|
def check_http(host, port, url="/", timeout=5):
|
||||||
|
try:
|
||||||
|
full_url = f"http://{host}:{port}{url}"
|
||||||
|
req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"})
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||||
|
return {"status": "online", "status_code": r.status, "error": None}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "offline", "error": str(e)[:100]}
|
||||||
|
|
||||||
|
def check_https(host, port, url="/", timeout=5, insecure=False):
|
||||||
|
try:
|
||||||
|
full_url = f"https://{host}:{port}{url}"
|
||||||
|
req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"})
|
||||||
|
if insecure:
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
else:
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
with urllib.request.urlopen(req, context=ctx, timeout=timeout) as r:
|
||||||
|
return {"status": "online", "status_code": r.status, "error": None}
|
||||||
|
except ssl.SSLError as e:
|
||||||
|
return {"status": "ssl_error", "error": f"SSL: {str(e)[:80]}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "offline", "error": str(e)[:100]}
|
||||||
|
|
||||||
|
def check_service(service_id, config):
|
||||||
|
st = config["type"]
|
||||||
|
if st == "ssh": return check_ssh(config["host"], config["port"])
|
||||||
|
elif st == "http": return check_http(config["host"], config["port"], config.get("url", "/"))
|
||||||
|
elif st == "https": return check_https(config["host"], config["port"], config.get("url", "/"), config.get("insecure", False))
|
||||||
|
return {"status": "error", "error": f"Unknown type: {st}"}
|
||||||
|
|
||||||
|
def update_history(sid, old, new):
|
||||||
|
global status_history
|
||||||
|
if old != new:
|
||||||
|
status_history[sid] = {"old_status": old, "new_status": new, "changed_at": datetime.now().isoformat()}
|
||||||
|
|
||||||
|
def check_single(sid):
|
||||||
|
if sid not in SERVICES: return None
|
||||||
|
result = check_service(sid, SERVICES[sid])
|
||||||
|
return {"config": SERVICES[sid], "result": result, "time": datetime.now().isoformat()}
|
||||||
|
|
||||||
|
def check_all():
|
||||||
|
global status_cache, status_history
|
||||||
|
new_cache = {}
|
||||||
|
for sid, cfg in SERVICES.items():
|
||||||
|
result = check_service(sid, cfg)
|
||||||
|
old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown")
|
||||||
|
update_history(sid, old, result["status"])
|
||||||
|
new_cache[sid] = {"config": cfg, "result": result, "time": datetime.now().isoformat()}
|
||||||
|
with lock: status_cache = new_cache
|
||||||
|
return new_cache
|
||||||
|
|
||||||
|
print(f"Starting Status Monitor v{VERSION}")
|
||||||
|
check_all()
|
||||||
|
|
||||||
|
# Build HTML page
|
||||||
|
html_page = f"""<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Status Monitor v{VERSION}</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: system-ui; background: #0f172a; color: #e2e8f0; margin: 0; padding: 20px; }}
|
||||||
|
.container {{ max-width: 1200px; margin: 0 auto; }}
|
||||||
|
h1 {{ color: #60a5fa; text-align: center; margin-bottom: 5px; }}
|
||||||
|
.version {{ text-align: center; color: #64748b; font-size: 0.85em; margin-bottom: 15px; }}
|
||||||
|
.controls {{ text-align: center; margin: 20px 0; position: relative; }}
|
||||||
|
button {{ padding: 8px 16px; background: #3b82f6; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9em; }}
|
||||||
|
button:hover {{ background: #2563eb; }}
|
||||||
|
button:disabled {{ opacity: 0.7; cursor: not-allowed; }}
|
||||||
|
.grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; margin-top: 20px; }}
|
||||||
|
.card {{ background: #1e293b; padding: 15px; border-radius: 8px; border-left: 4px solid; position: relative; }}
|
||||||
|
.card.online {{ border-left-color: #22c55e; }}
|
||||||
|
.card.offline {{ border-left-color: #ef4444; }}
|
||||||
|
.card.timeout {{ border-left-color: #f97316; }}
|
||||||
|
.card.ssl_error {{ border-left-color: #f97316; }}
|
||||||
|
.card.error {{ border-left-color: #ef4444; }}
|
||||||
|
.header {{ display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }}
|
||||||
|
.name {{ font-weight: bold; font-size: 1.1em; flex: 1; }}
|
||||||
|
.type {{ font-size: 0.8em; background: #334155; padding: 2px 8px; border-radius: 12px; color: #94a3b8; }}
|
||||||
|
.badge {{ padding: 4px 12px; border-radius: 12px; font-size: 0.85em; font-weight: bold; }}
|
||||||
|
.badge.online {{ background: #166534; color: #dcfce7; }}
|
||||||
|
.badge.offline {{ background: #991b1b; color: #fca5a5; }}
|
||||||
|
.badge.timeout {{ background: #92400e; color: #fde68a; }}
|
||||||
|
.badge.ssl_error {{ background: #92400e; color: #fde68a; }}
|
||||||
|
.badge.error {{ background: #991b1b; color: #fca5a5; }}
|
||||||
|
.details {{ margin-top: 10px; font-size: 0.85em; color: #94a3b8; }}
|
||||||
|
.error {{ color: #fca5a5; margin-top: 5px; font-size: 0.85em; word-break: break-word; }}
|
||||||
|
.summary {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin: 20px 0; }}
|
||||||
|
.card-sum {{ background: #1e293b; padding: 15px; border-radius: 8px; text-align: center; }}
|
||||||
|
.card-sum .count {{ font-size: 2em; font-weight: bold; color: #60a5fa; }}
|
||||||
|
.card-sum .label {{ color: #94a3b8; }}
|
||||||
|
.history-note {{ font-size: 0.75em; color: #64748b; margin-top: 5px; }}
|
||||||
|
.refresh-btn {{ position: absolute; top: 10px; right: 10px; background: transparent; border: none; cursor: pointer; font-size: 1.2em; padding: 0; line-height: 1; }}
|
||||||
|
.refresh-btn:hover {{ opacity: 0.8; }}
|
||||||
|
.refresh-btn:disabled {{ cursor: not-allowed; opacity: 0.5; }}
|
||||||
|
@@keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }}
|
||||||
|
.loading-spinner {{ display: inline-block; width: 16px; height: 16px; border: 2px solid #334155; border-top-color: #60a5fa; border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 8px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Homelab Status Monitor</h1>
|
||||||
|
<div class="version">v{VERSION}</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="refresh-all" onclick="refreshAll()">Refresh All</button>
|
||||||
|
<button onclick="location.reload()">Reload Page</button>
|
||||||
|
<span id="ts"></span><span id="global-loading"></span>
|
||||||
|
</div>
|
||||||
|
<div class="summary">
|
||||||
|
<div class="card-sum"><div class="count" id="total">0</div><div class="label">Total</div></div>
|
||||||
|
<div class="card-sum"><div class="count" id="ok">0</div><div class="label">Online</div></div>
|
||||||
|
<div class="card-sum"><div class="count" id="ko">0</div><div class="label">Offline</div></div>
|
||||||
|
<div class="card-sum"><div class="count" id="err">0</div><div class="label">Errors</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid" id="grid"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let data = {{}};
|
||||||
|
let history = {{}};
|
||||||
|
let refreshInProgress = false;
|
||||||
|
|
||||||
|
function getClass(s) {{
|
||||||
|
var c = {{'online': 'online', 'offline': 'offline', 'timeout': 'timeout', 'error': 'error', 'ssl_error': 'ssl_error'}};
|
||||||
|
return c[s] || 'offline';
|
||||||
|
}}
|
||||||
|
|
||||||
|
function fmtHist(sid) {{
|
||||||
|
if (history[sid]) {{
|
||||||
|
var h = history[sid];
|
||||||
|
return 'Changed: ' + h.old_status + ' -> ' + h.new_status + ' at ' + new Date(h.changed_at).toLocaleTimeString();
|
||||||
|
}}
|
||||||
|
return '';
|
||||||
|
}}
|
||||||
|
|
||||||
|
function setGlobalLoad(b) {{
|
||||||
|
refreshInProgress = b;
|
||||||
|
document.getElementById('global-loading').innerHTML = b ? '<span class="loading-spinner"></span>' : '';
|
||||||
|
}}
|
||||||
|
|
||||||
|
function render() {{
|
||||||
|
let h = '';
|
||||||
|
let t = 0, ok = 0, ko = 0, err = 0;
|
||||||
|
for (const [id, v] of Object.entries(data)) {{
|
||||||
|
const c = v.config, r = v.result, s = r.status;
|
||||||
|
t++;
|
||||||
|
if (s === 'online') ok++;
|
||||||
|
else if (s === 'offline' || s === 'error' || s === 'timeout' || s === 'ssl_error') ko++;
|
||||||
|
else err++;
|
||||||
|
|
||||||
|
const bc = getClass(s);
|
||||||
|
const pi = c.url ? c.host + ':' + c.port + c.url : c.host + ':' + c.port;
|
||||||
|
|
||||||
|
h += '<div class="card ' + bc + '">';
|
||||||
|
h += '<button class="refresh-btn" onclick="refreshService(\'' + id + '\')" title="Refresh">🔄</button>';
|
||||||
|
h += '<div class="header"><div class="name">' + c.name + '</div><span class="type">' + c.type + '</span></div>';
|
||||||
|
h += '<span class="badge ' + bc + '">' + s.replace('_', ' ') + '</span>';
|
||||||
|
h += '<div class="details">' + pi + '</div>';
|
||||||
|
if (r.error) h += '<div class="error">' + r.error + '</div>';
|
||||||
|
if (r.status_code) h += '<div class="details">Status: ' + r.status_code + '</div>';
|
||||||
|
h += '<div class="details">Checked: ' + new Date(v.time).toLocaleTimeString() + '</div>';
|
||||||
|
const hist = fmtHist(id);
|
||||||
|
if (hist) h += '<div class="history-note">' + hist + '</div>';
|
||||||
|
h += '</div>';
|
||||||
|
}}
|
||||||
|
|
||||||
|
document.getElementById('grid').innerHTML = h || '<p style="color:#64748b;text-align:center;">No services to display</p>';
|
||||||
|
document.getElementById('total').textContent = t;
|
||||||
|
document.getElementById('ok').textContent = ok;
|
||||||
|
document.getElementById('ko').textContent = ko;
|
||||||
|
document.getElementById('err').textContent = err;
|
||||||
|
document.getElementById('ts').textContent = 'Updated: ' + new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
if (refreshInProgress) document.getElementById('global-loading').innerHTML = '<span class="loading-spinner"></span>';
|
||||||
|
else document.getElementById('global-loading').innerHTML = '';
|
||||||
|
}}
|
||||||
|
|
||||||
|
function refreshService(sid) {{
|
||||||
|
const btn = event.target;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="loading-spinner"></span>';
|
||||||
|
fetch('/api/check/' + sid)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {{
|
||||||
|
data[sid] = d;
|
||||||
|
if (d.history) history[sid] = d.history;
|
||||||
|
render();
|
||||||
|
}})
|
||||||
|
.catch(err => console.error('Error:', err))
|
||||||
|
.finally(() => {{
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '🔄';
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
|
||||||
|
function refreshAll() {{
|
||||||
|
const btn = document.querySelector('.refresh-all');
|
||||||
|
setGlobalLoad(true);
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="loading-spinner"></span> Refreshing...';
|
||||||
|
fetch('/api/refresh')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {{
|
||||||
|
data = d.data;
|
||||||
|
history = d.history || {{}};
|
||||||
|
render();
|
||||||
|
}})
|
||||||
|
.catch(err => console.error('Error:', err))
|
||||||
|
.finally(() => {{
|
||||||
|
setGlobalLoad(false);
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = 'Refresh All';
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
fetch('/api/status')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {{
|
||||||
|
data = d.data;
|
||||||
|
history = d.history || {{}};
|
||||||
|
render();
|
||||||
|
}})
|
||||||
|
.catch(err => console.error('Error:', err));
|
||||||
|
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
setInterval(function() {{
|
||||||
|
setGlobalLoad(true);
|
||||||
|
fetch('/api/status')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(function(d) {{
|
||||||
|
data = d.data;
|
||||||
|
history = d.history || {{}};
|
||||||
|
render();
|
||||||
|
}})
|
||||||
|
.catch(function(err) {{
|
||||||
|
console.error('Error:', err);
|
||||||
|
}})
|
||||||
|
.finally(function() {{
|
||||||
|
setGlobalLoad(false);
|
||||||
|
}});
|
||||||
|
}}, 30000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def log_message(self, *args): pass
|
||||||
|
def do_GET(self):
|
||||||
|
try:
|
||||||
|
if self.path in ['/', '/index.html']:
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'text/html; charset=utf-8')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(html_page.encode('utf-8'))
|
||||||
|
elif self.path == '/api/status':
|
||||||
|
if not status_cache: check_all()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.send_header('Cache-Control', 'no-cache')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({"data": status_cache, "history": status_history}, default=str).encode('utf-8'))
|
||||||
|
elif self.path == '/api/refresh':
|
||||||
|
result = check_all()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({"data": result, "history": status_history}, default=str).encode('utf-8'))
|
||||||
|
elif self.path.startswith('/api/check/'):
|
||||||
|
sid = self.path.split('/')[-1]
|
||||||
|
if sid in SERVICES:
|
||||||
|
old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown")
|
||||||
|
rd = check_single(sid)
|
||||||
|
new_st = rd["result"]["status"]
|
||||||
|
if old != new_st: update_history(sid, old, new_st)
|
||||||
|
with lock: status_cache[sid] = rd
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({**rd, "history": status_history.get(sid)}, default=str).encode('utf-8'))
|
||||||
|
else: self.send_response(404); self.end_headers()
|
||||||
|
else: self.send_response(404); self.end_headers()
|
||||||
|
except Exception as e:
|
||||||
|
self.send_response(500)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from http.server import ThreadingHTTPServer
|
||||||
|
check_all()
|
||||||
|
server = ThreadingHTTPServer(('', 80), Handler)
|
||||||
|
print(f'Status Monitor v{VERSION} running on port 80')
|
||||||
|
server.serve_forever()
|
||||||
Executable
+426
@@ -0,0 +1,426 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Homelab Status Monitor v1.1.0"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
|
VERSION = "1.1.0"
|
||||||
|
|
||||||
|
# Service configuration: name, type (ssh/http/https), host, port, url (for http/https), insecure (for https)
|
||||||
|
SERVICES = {
|
||||||
|
# Containers LXC
|
||||||
|
"ct101-mqtt": {"name": "MQTT", "type": "ssh", "host": "192.168.0.5", "port": 22},
|
||||||
|
"ct101-mqtt-http": {"name": "MQTT HTTP", "type": "http", "host": "192.168.0.5", "port": 1883, "url": "/"},
|
||||||
|
|
||||||
|
"ct102-frigate": {"name": "Frigate", "type": "ssh", "host": "192.168.0.91", "port": 22},
|
||||||
|
"ct102-frigate-http": {"name": "Frigate Web", "type": "http", "host": "192.168.0.91", "port": 5000, "url": "/"},
|
||||||
|
|
||||||
|
"ct103-adguard": {"name": "AdGuard", "type": "ssh", "host": "192.168.0.92", "port": 22},
|
||||||
|
"ct103-adguard-http": {"name": "AdGuard Web", "type": "http", "host": "192.168.0.92", "port": 80, "url": "/"},
|
||||||
|
"ct103-adguard-https": {"name": "AdGuard HTTPS", "type": "https", "host": "192.168.0.92", "port": 443, "url": "/", "insecure": True},
|
||||||
|
|
||||||
|
"ct105-alpine": {"name": "Alpine-Docker", "type": "ssh", "host": "192.168.0.155", "port": 22},
|
||||||
|
"ct105-alpine-docker": {"name": "Alpine Docker API", "type": "http", "host": "192.168.0.155", "port": 2375, "url": "/"},
|
||||||
|
|
||||||
|
"ct106-zigbee": {"name": "Zigbee2MQTT", "type": "ssh", "host": "192.168.0.177", "port": 22},
|
||||||
|
"ct106-zigbee-http": {"name": "Zigbee2MQTT Web", "type": "http", "host": "192.168.0.177", "port": 8081, "url": "/"},
|
||||||
|
|
||||||
|
"ct200-traefik": {"name": "Traefik Proxy", "type": "ssh", "host": "192.168.0.100", "port": 22},
|
||||||
|
"ct200-traefik-http": {"name": "Traefik HTTP", "type": "http", "host": "192.168.0.100", "port": 80, "url": "/"},
|
||||||
|
"ct200-traefik-https": {"name": "Traefik HTTPS", "type": "https", "host": "192.168.0.100", "port": 443, "url": "/", "insecure": True},
|
||||||
|
|
||||||
|
"ct201-status": {"name": "Status Monitor", "type": "ssh", "host": "192.168.0.201", "port": 22},
|
||||||
|
"ct201-status-http": {"name": "Status Monitor Web", "type": "http", "host": "192.168.0.201", "port": 80, "url": "/"},
|
||||||
|
|
||||||
|
# VM
|
||||||
|
"vm100-ha": {"name": "Home Assistant", "type": "ssh", "host": "192.168.0.43", "port": 22},
|
||||||
|
"vm100-ha-http": {"name": "Home Assistant Web", "type": "http", "host": "192.168.0.43", "port": 8123, "url": "/"},
|
||||||
|
|
||||||
|
# Proxmox hosts
|
||||||
|
"prox": {"name": "Proxmox (prox)", "type": "ssh", "host": "192.168.0.94", "port": 22},
|
||||||
|
"prox2": {"name": "Proxmox (prox2)", "type": "ssh", "host": "192.168.0.95", "port": 22},
|
||||||
|
|
||||||
|
# Raspberry Pi
|
||||||
|
"pi3": {"name": "Raspberry Pi 3", "type": "ssh", "host": "192.168.0.156", "port": 22},
|
||||||
|
|
||||||
|
# External HTTPS endpoints
|
||||||
|
"ha-peis-fr": {"name": "ha.peis.fr", "type": "https", "host": "ha.peis.fr", "port": 443, "url": "/", "insecure": True},
|
||||||
|
"traefik-dash": {"name": "traefik.peis.fr", "type": "https", "host": "traefik.peis.fr", "port": 443, "url": "/dashboard/", "insecure": True},
|
||||||
|
"test-peis-fr": {"name": "test.peis.fr", "type": "https", "host": "test.peis.fr", "port": 443, "url": "/", "insecure": True},
|
||||||
|
"status-peis-fr": {"name": "status.peis.fr", "type": "https", "host": "status.peis.fr", "port": 443, "url": "/", "insecure": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
status_history = {}
|
||||||
|
status_cache = {}
|
||||||
|
lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def check_ssh(host, port, timeout=3):
|
||||||
|
"""Check if SSH port is open."""
|
||||||
|
try:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
result = sock.connect_ex((host, port))
|
||||||
|
sock.close()
|
||||||
|
if result == 0:
|
||||||
|
return {"status": "online", "error": None}
|
||||||
|
else:
|
||||||
|
return {"status": "offline", "error": f"Connection refused (code: {result})"}
|
||||||
|
except socket.timeout:
|
||||||
|
return {"status": "timeout", "error": "Connection timed out"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "offline", "error": str(e)[:100]}
|
||||||
|
|
||||||
|
|
||||||
|
def check_http(host, port, url="/", timeout=5):
|
||||||
|
"""Check HTTP endpoint."""
|
||||||
|
try:
|
||||||
|
full_url = f"http://{host}:{port}{url}"
|
||||||
|
req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"})
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||||
|
return {"status": "online", "status_code": r.status, "error": None}
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return {"status": "online", "status_code": e.code, "error": None} # HTTP error still means service is up
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "offline", "error": str(e)[:100]}
|
||||||
|
|
||||||
|
|
||||||
|
def check_https(host, port, url="/", timeout=5, insecure=False):
|
||||||
|
"""Check HTTPS endpoint."""
|
||||||
|
try:
|
||||||
|
full_url = f"https://{host}:{port}{url}"
|
||||||
|
req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"})
|
||||||
|
if insecure:
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
else:
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
with urllib.request.urlopen(req, context=ctx, timeout=timeout) as r:
|
||||||
|
return {"status": "online", "status_code": r.status, "error": None}
|
||||||
|
except ssl.SSLError as e:
|
||||||
|
return {"status": "ssl_error", "error": f"SSL: {str(e)[:80]}"}
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return {"status": "online", "status_code": e.code, "error": None}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "offline", "error": str(e)[:100]}
|
||||||
|
|
||||||
|
|
||||||
|
def check_service(service_id, config):
|
||||||
|
"""Check a single service based on its type."""
|
||||||
|
st = config["type"]
|
||||||
|
if st == "ssh":
|
||||||
|
return check_ssh(config["host"], config["port"])
|
||||||
|
elif st == "http":
|
||||||
|
return check_http(config["host"], config["port"], config.get("url", "/"))
|
||||||
|
elif st == "https":
|
||||||
|
return check_https(config["host"], config["port"], config.get("url", "/"), config.get("insecure", False))
|
||||||
|
return {"status": "error", "error": f"Unknown type: {st}"}
|
||||||
|
|
||||||
|
|
||||||
|
def update_history(sid, old, new):
|
||||||
|
"""Update status change history."""
|
||||||
|
global status_history
|
||||||
|
if old != new:
|
||||||
|
status_history[sid] = {
|
||||||
|
"old_status": old,
|
||||||
|
"new_status": new,
|
||||||
|
"changed_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_single(sid):
|
||||||
|
"""Check a single service and return its status."""
|
||||||
|
if sid not in SERVICES:
|
||||||
|
return None
|
||||||
|
result = check_service(sid, SERVICES[sid])
|
||||||
|
return {"config": SERVICES[sid], "result": result, "time": datetime.now().isoformat()}
|
||||||
|
|
||||||
|
|
||||||
|
def check_all():
|
||||||
|
"""Check all services and update cache."""
|
||||||
|
global status_cache, status_history
|
||||||
|
new_cache = {}
|
||||||
|
for sid, cfg in SERVICES.items():
|
||||||
|
result = check_service(sid, cfg)
|
||||||
|
old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown")
|
||||||
|
new = result["status"]
|
||||||
|
if old != new:
|
||||||
|
update_history(sid, old, new)
|
||||||
|
new_cache[sid] = {"config": cfg, "result": result, "time": datetime.now().isoformat()}
|
||||||
|
with lock:
|
||||||
|
status_cache = new_cache
|
||||||
|
return new_cache
|
||||||
|
|
||||||
|
|
||||||
|
# HTML Template (using a function to avoid f-string escaping issues)
|
||||||
|
def generate_html():
|
||||||
|
"""Generate the HTML page."""
|
||||||
|
return '''<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Homelab Status Monitor v''' + VERSION + '''</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 20px; }
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; }
|
||||||
|
h1 { color: #60a5fa; text-align: center; margin-bottom: 5px; }
|
||||||
|
.version { text-align: center; color: #64748b; font-size: 0.85em; margin-bottom: 15px; }
|
||||||
|
.controls { text-align: center; margin: 20px 0; position: relative; }
|
||||||
|
button { padding: 8px 16px; background: #3b82f6; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9em; }
|
||||||
|
button:hover { background: #2563eb; }
|
||||||
|
button:disabled { opacity: 0.7; cursor: not-allowed; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 15px; margin-top: 20px; }
|
||||||
|
.card { background: #1e293b; padding: 15px; border-radius: 8px; border-left: 4px solid; position: relative; }
|
||||||
|
.card.online { border-left-color: #22c55e; }
|
||||||
|
.card.offline { border-left-color: #ef4444; }
|
||||||
|
.card.timeout { border-left-color: #f97316; }
|
||||||
|
.card.ssl_error { border-left-color: #f97316; }
|
||||||
|
.card.error { border-left-color: #ef4444; }
|
||||||
|
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
|
||||||
|
.name { font-weight: bold; font-size: 1.1em; flex: 1; }
|
||||||
|
.type { font-size: 0.8em; background: #334155; padding: 2px 8px; border-radius: 12px; color: #94a3b8; }
|
||||||
|
.badge { padding: 4px 12px; border-radius: 12px; font-size: 0.85em; font-weight: bold; }
|
||||||
|
.badge.online { background: #166534; color: #dcfce7; }
|
||||||
|
.badge.offline { background: #991b1b; color: #fca5a5; }
|
||||||
|
.badge.timeout { background: #92400e; color: #fde68a; }
|
||||||
|
.badge.ssl_error { background: #92400e; color: #fde68a; }
|
||||||
|
.badge.error { background: #991b1b; color: #fca5a5; }
|
||||||
|
.details { margin-top: 10px; font-size: 0.85em; color: #94a3b8; }
|
||||||
|
.error { color: #fca5a5; margin-top: 5px; font-size: 0.85em; word-break: break-word; }
|
||||||
|
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin: 20px 0; }
|
||||||
|
.card-sum { background: #1e293b; padding: 15px; border-radius: 8px; text-align: center; }
|
||||||
|
.card-sum .count { font-size: 2em; font-weight: bold; color: #60a5fa; }
|
||||||
|
.card-sum .label { color: #94a3b8; }
|
||||||
|
.history-note { font-size: 0.75em; color: #64748b; margin-top: 5px; }
|
||||||
|
.refresh-btn { position: absolute; top: 10px; right: 10px; background: transparent; border: none; cursor: pointer; font-size: 1.2em; padding: 0; line-height: 1; }
|
||||||
|
.refresh-btn:hover { opacity: 0.8; }
|
||||||
|
.refresh-btn:disabled { cursor: not-allowed; opacity: 0.5; }
|
||||||
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||||
|
.loading-spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid #334155; border-top-color: #60a5fa; border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 8px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Homelab Status Monitor</h1>
|
||||||
|
<div class="version">v''' + VERSION + '''</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="refresh-all" onclick="refreshAll()">Refresh All</button>
|
||||||
|
<button onclick="location.reload()">Reload Page</button>
|
||||||
|
<span id="ts"></span>
|
||||||
|
<span id="global-loading"></span>
|
||||||
|
</div>
|
||||||
|
<div class="summary">
|
||||||
|
<div class="card-sum"><div class="count" id="total">0</div><div class="label">Total</div></div>
|
||||||
|
<div class="card-sum"><div class="count" id="ok">0</div><div class="label">Online</div></div>
|
||||||
|
<div class="card-sum"><div class="count" id="ko">0</div><div class="label">Offline</div></div>
|
||||||
|
<div class="card-sum"><div class="count" id="err">0</div><div class="label">Errors</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid" id="grid"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let data = {};
|
||||||
|
let history = {};
|
||||||
|
let refreshInProgress = false;
|
||||||
|
|
||||||
|
function getClass(s) {
|
||||||
|
var classes = {"online": "online", "offline": "offline", "timeout": "timeout", "error": "error", "ssl_error": "ssl_error"};
|
||||||
|
return classes[s] || "offline";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtHist(sid) {
|
||||||
|
if (history[sid]) {
|
||||||
|
var h = history[sid];
|
||||||
|
return "Changed: " + h.old_status + " -> " + h.new_status + " at " + new Date(h.changed_at).toLocaleTimeString();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGlobalLoad(b) {
|
||||||
|
refreshInProgress = b;
|
||||||
|
var elem = document.getElementById("global-loading");
|
||||||
|
elem.innerHTML = b ? "<span class=\"loading-spinner\"></span>" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
let h = "";
|
||||||
|
let t = 0, ok = 0, ko = 0, err = 0;
|
||||||
|
for (const [id, v] of Object.entries(data)) {
|
||||||
|
const c = v.config, r = v.result, s = r.status;
|
||||||
|
t++;
|
||||||
|
if (s === "online") ok++;
|
||||||
|
else if (s === "offline" || s === "error" || s === "timeout" || s === "ssl_error") ko++;
|
||||||
|
else err++;
|
||||||
|
|
||||||
|
const bc = getClass(s);
|
||||||
|
const pi = c.url ? c.host + ":" + c.port + c.url : c.host + ":" + c.port;
|
||||||
|
|
||||||
|
h += "<div class=\"card " + bc + "\">";
|
||||||
|
h += "<button class=\"refresh-btn\" onclick=\"refreshService('" + id + "')\" title=\"Refresh\">🔄</button>";
|
||||||
|
h += "<div class=\"header\"><div class=\"name\">" + c.name + "</div><span class=\"type\">" + c.type + "</span></div>";
|
||||||
|
h += "<span class=\"badge " + bc + "\">" + s.replace(/_/g, " ") + "</span>";
|
||||||
|
h += "<div class=\"details\">" + pi + "</div>";
|
||||||
|
if (r.error) h += "<div class=\"error\">" + r.error + "</div>";
|
||||||
|
if (r.status_code) h += "<div class=\"details\">Status: " + r.status_code + "</div>";
|
||||||
|
h += "<div class=\"details\">Checked: " + new Date(v.time).toLocaleTimeString() + "</div>";
|
||||||
|
const hist = fmtHist(id);
|
||||||
|
if (hist) h += "<div class=\"history-note\">" + hist + "</div>";
|
||||||
|
h += "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("grid").innerHTML = h || "<p style=\"color:#64748b;text-align:center;\">No services to display</p>";
|
||||||
|
document.getElementById("total").textContent = t;
|
||||||
|
document.getElementById("ok").textContent = ok;
|
||||||
|
document.getElementById("ko").textContent = ko;
|
||||||
|
document.getElementById("err").textContent = err;
|
||||||
|
document.getElementById("ts").textContent = "Updated: " + new Date().toLocaleTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshService(sid) {
|
||||||
|
const btn = event.target;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = "<span class=\"loading-spinner\"></span>";
|
||||||
|
fetch("/api/check/" + encodeURIComponent(sid))
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
data[sid] = d;
|
||||||
|
if (d.history) history[sid] = d.history;
|
||||||
|
render();
|
||||||
|
})
|
||||||
|
.catch(err => console.error("Error:", err))
|
||||||
|
.finally(() => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = "🔄";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAll() {
|
||||||
|
const btn = document.querySelector(".refresh-all");
|
||||||
|
setGlobalLoad(true);
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = "<span class=\"loading-spinner\"></span> Refreshing...";
|
||||||
|
fetch("/api/refresh")
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
data = d.data;
|
||||||
|
history = d.history || {};
|
||||||
|
render();
|
||||||
|
})
|
||||||
|
.catch(err => console.error("Error:", err))
|
||||||
|
.finally(() => {
|
||||||
|
setGlobalLoad(false);
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = "Refresh All";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
fetch("/api/status")
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
data = d.data;
|
||||||
|
history = d.history || {};
|
||||||
|
render();
|
||||||
|
})
|
||||||
|
.catch(err => console.error("Error:", err));
|
||||||
|
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
setInterval(function() {
|
||||||
|
setGlobalLoad(true);
|
||||||
|
fetch("/api/status")
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(function(d) {
|
||||||
|
data = d.data;
|
||||||
|
history = d.history || {};
|
||||||
|
render();
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.error("Error:", err);
|
||||||
|
})
|
||||||
|
.finally(function() {
|
||||||
|
setGlobalLoad(false);
|
||||||
|
});
|
||||||
|
}, 30000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>'''
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
"""HTTP request handler."""
|
||||||
|
|
||||||
|
def log_message(self, *args):
|
||||||
|
"""Suppress default logging."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
"""Handle GET requests."""
|
||||||
|
try:
|
||||||
|
if self.path in ['/', '/index.html']:
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'text/html; charset=utf-8')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(generate_html().encode('utf-8'))
|
||||||
|
|
||||||
|
elif self.path == '/api/status':
|
||||||
|
if not status_cache:
|
||||||
|
check_all()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.send_header('Cache-Control', 'no-cache')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({"data": status_cache, "history": status_history}, default=str).encode('utf-8'))
|
||||||
|
|
||||||
|
elif self.path == '/api/refresh':
|
||||||
|
result = check_all()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({"data": result, "history": status_history}, default=str).encode('utf-8'))
|
||||||
|
|
||||||
|
elif self.path.startswith('/api/check/'):
|
||||||
|
sid = self.path.split('/')[-1]
|
||||||
|
if sid in SERVICES:
|
||||||
|
old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown")
|
||||||
|
rd = check_single(sid)
|
||||||
|
new_st = rd["result"]["status"]
|
||||||
|
if old != new_st:
|
||||||
|
update_history(sid, old, new_st)
|
||||||
|
with lock:
|
||||||
|
status_cache[sid] = rd
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({**rd, "history": status_history.get(sid)}, default=str).encode('utf-8'))
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.send_response(500)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
print(f"Starting Status Monitor v{VERSION}")
|
||||||
|
check_all()
|
||||||
|
server = ThreadingHTTPServer(('', 80), Handler)
|
||||||
|
print(f'Status Monitor v{VERSION} running on port 80')
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Executable
+426
@@ -0,0 +1,426 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Homelab Status Monitor v1.1.0"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
|
VERSION = "1.1.0"
|
||||||
|
|
||||||
|
# Service configuration: name, type (ssh/http/https), host, port, url (for http/https), insecure (for https)
|
||||||
|
SERVICES = {
|
||||||
|
# Containers LXC
|
||||||
|
"ct101-mqtt": {"name": "MQTT", "type": "ssh", "host": "192.168.0.5", "port": 22},
|
||||||
|
"ct101-mqtt-http": {"name": "MQTT HTTP", "type": "http", "host": "192.168.0.5", "port": 1883, "url": "/"},
|
||||||
|
|
||||||
|
"ct102-frigate": {"name": "Frigate", "type": "ssh", "host": "192.168.0.91", "port": 22},
|
||||||
|
"ct102-frigate-http": {"name": "Frigate Web", "type": "http", "host": "192.168.0.91", "port": 5000, "url": "/"},
|
||||||
|
|
||||||
|
"ct103-adguard": {"name": "AdGuard", "type": "ssh", "host": "192.168.0.92", "port": 22},
|
||||||
|
"ct103-adguard-http": {"name": "AdGuard Web", "type": "http", "host": "192.168.0.92", "port": 80, "url": "/"},
|
||||||
|
"ct103-adguard-https": {"name": "AdGuard HTTPS", "type": "https", "host": "192.168.0.92", "port": 443, "url": "/", "insecure": True},
|
||||||
|
|
||||||
|
"ct105-alpine": {"name": "Alpine-Docker", "type": "ssh", "host": "192.168.0.155", "port": 22},
|
||||||
|
"ct105-alpine-docker": {"name": "Alpine Docker API", "type": "http", "host": "192.168.0.155", "port": 2375, "url": "/"},
|
||||||
|
|
||||||
|
"ct106-zigbee": {"name": "Zigbee2MQTT", "type": "ssh", "host": "192.168.0.177", "port": 22},
|
||||||
|
"ct106-zigbee-http": {"name": "Zigbee2MQTT Web", "type": "http", "host": "192.168.0.177", "port": 8081, "url": "/"},
|
||||||
|
|
||||||
|
"ct200-traefik": {"name": "Traefik Proxy", "type": "ssh", "host": "192.168.0.100", "port": 22},
|
||||||
|
"ct200-traefik-http": {"name": "Traefik HTTP", "type": "http", "host": "192.168.0.100", "port": 80, "url": "/"},
|
||||||
|
"ct200-traefik-https": {"name": "Traefik HTTPS", "type": "https", "host": "192.168.0.100", "port": 443, "url": "/", "insecure": True},
|
||||||
|
|
||||||
|
"ct201-status": {"name": "Status Monitor", "type": "ssh", "host": "192.168.0.201", "port": 22},
|
||||||
|
"ct201-status-http": {"name": "Status Monitor Web", "type": "http", "host": "192.168.0.201", "port": 80, "url": "/"},
|
||||||
|
|
||||||
|
# VM
|
||||||
|
"vm100-ha": {"name": "Home Assistant", "type": "ssh", "host": "192.168.0.43", "port": 22},
|
||||||
|
"vm100-ha-http": {"name": "Home Assistant Web", "type": "http", "host": "192.168.0.43", "port": 8123, "url": "/"},
|
||||||
|
|
||||||
|
# Proxmox hosts
|
||||||
|
"prox": {"name": "Proxmox (prox)", "type": "ssh", "host": "192.168.0.94", "port": 22},
|
||||||
|
"prox2": {"name": "Proxmox (prox2)", "type": "ssh", "host": "192.168.0.95", "port": 22},
|
||||||
|
|
||||||
|
# Raspberry Pi
|
||||||
|
"pi3": {"name": "Raspberry Pi 3", "type": "ssh", "host": "192.168.0.156", "port": 22},
|
||||||
|
|
||||||
|
# External HTTPS endpoints
|
||||||
|
"ha-peis-fr": {"name": "ha.peis.fr", "type": "https", "host": "ha.peis.fr", "port": 443, "url": "/", "insecure": True},
|
||||||
|
"traefik-dash": {"name": "traefik.peis.fr", "type": "https", "host": "traefik.peis.fr", "port": 443, "url": "/dashboard/", "insecure": True},
|
||||||
|
"test-peis-fr": {"name": "test.peis.fr", "type": "https", "host": "test.peis.fr", "port": 443, "url": "/", "insecure": True},
|
||||||
|
"status-peis-fr": {"name": "status.peis.fr", "type": "https", "host": "status.peis.fr", "port": 443, "url": "/", "insecure": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
status_history = {}
|
||||||
|
status_cache = {}
|
||||||
|
lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def check_ssh(host, port, timeout=3):
|
||||||
|
"""Check if SSH port is open."""
|
||||||
|
try:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
result = sock.connect_ex((host, port))
|
||||||
|
sock.close()
|
||||||
|
if result == 0:
|
||||||
|
return {"status": "online", "error": None}
|
||||||
|
else:
|
||||||
|
return {"status": "offline", "error": f"Connection refused (code: {result})"}
|
||||||
|
except socket.timeout:
|
||||||
|
return {"status": "timeout", "error": "Connection timed out"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "offline", "error": str(e)[:100]}
|
||||||
|
|
||||||
|
|
||||||
|
def check_http(host, port, url="/", timeout=5):
|
||||||
|
"""Check HTTP endpoint."""
|
||||||
|
try:
|
||||||
|
full_url = f"http://{host}:{port}{url}"
|
||||||
|
req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"})
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||||
|
return {"status": "online", "status_code": r.status, "error": None}
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return {"status": "online", "status_code": e.code, "error": None} # HTTP error still means service is up
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "offline", "error": str(e)[:100]}
|
||||||
|
|
||||||
|
|
||||||
|
def check_https(host, port, url="/", timeout=5, insecure=False):
|
||||||
|
"""Check HTTPS endpoint."""
|
||||||
|
try:
|
||||||
|
full_url = f"https://{host}:{port}{url}"
|
||||||
|
req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"})
|
||||||
|
if insecure:
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
else:
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
with urllib.request.urlopen(req, context=ctx, timeout=timeout) as r:
|
||||||
|
return {"status": "online", "status_code": r.status, "error": None}
|
||||||
|
except ssl.SSLError as e:
|
||||||
|
return {"status": "ssl_error", "error": f"SSL: {str(e)[:80]}"}
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return {"status": "online", "status_code": e.code, "error": None}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "offline", "error": str(e)[:100]}
|
||||||
|
|
||||||
|
|
||||||
|
def check_service(service_id, config):
|
||||||
|
"""Check a single service based on its type."""
|
||||||
|
st = config["type"]
|
||||||
|
if st == "ssh":
|
||||||
|
return check_ssh(config["host"], config["port"])
|
||||||
|
elif st == "http":
|
||||||
|
return check_http(config["host"], config["port"], config.get("url", "/"))
|
||||||
|
elif st == "https":
|
||||||
|
return check_https(config["host"], config["port"], config.get("url", "/"), config.get("insecure", False))
|
||||||
|
return {"status": "error", "error": f"Unknown type: {st}"}
|
||||||
|
|
||||||
|
|
||||||
|
def update_history(sid, old, new):
|
||||||
|
"""Update status change history."""
|
||||||
|
global status_history
|
||||||
|
if old != new:
|
||||||
|
status_history[sid] = {
|
||||||
|
"old_status": old,
|
||||||
|
"new_status": new,
|
||||||
|
"changed_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_single(sid):
|
||||||
|
"""Check a single service and return its status."""
|
||||||
|
if sid not in SERVICES:
|
||||||
|
return None
|
||||||
|
result = check_service(sid, SERVICES[sid])
|
||||||
|
return {"config": SERVICES[sid], "result": result, "time": datetime.now().isoformat()}
|
||||||
|
|
||||||
|
|
||||||
|
def check_all():
|
||||||
|
"""Check all services and update cache."""
|
||||||
|
global status_cache, status_history
|
||||||
|
new_cache = {}
|
||||||
|
for sid, cfg in SERVICES.items():
|
||||||
|
result = check_service(sid, cfg)
|
||||||
|
old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown")
|
||||||
|
new = result["status"]
|
||||||
|
if old != new:
|
||||||
|
update_history(sid, old, new)
|
||||||
|
new_cache[sid] = {"config": cfg, "result": result, "time": datetime.now().isoformat()}
|
||||||
|
with lock:
|
||||||
|
status_cache = new_cache
|
||||||
|
return new_cache
|
||||||
|
|
||||||
|
|
||||||
|
# HTML Template (using a function to avoid f-string escaping issues)
|
||||||
|
def generate_html():
|
||||||
|
"""Generate the HTML page."""
|
||||||
|
return '''<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Homelab Status Monitor v''' + VERSION + '''</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 20px; }
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; }
|
||||||
|
h1 { color: #60a5fa; text-align: center; margin-bottom: 5px; }
|
||||||
|
.version { text-align: center; color: #64748b; font-size: 0.85em; margin-bottom: 15px; }
|
||||||
|
.controls { text-align: center; margin: 20px 0; position: relative; }
|
||||||
|
button { padding: 8px 16px; background: #3b82f6; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9em; }
|
||||||
|
button:hover { background: #2563eb; }
|
||||||
|
button:disabled { opacity: 0.7; cursor: not-allowed; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 15px; margin-top: 20px; }
|
||||||
|
.card { background: #1e293b; padding: 15px; border-radius: 8px; border-left: 4px solid; position: relative; }
|
||||||
|
.card.online { border-left-color: #22c55e; }
|
||||||
|
.card.offline { border-left-color: #ef4444; }
|
||||||
|
.card.timeout { border-left-color: #f97316; }
|
||||||
|
.card.ssl_error { border-left-color: #f97316; }
|
||||||
|
.card.error { border-left-color: #ef4444; }
|
||||||
|
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
|
||||||
|
.name { font-weight: bold; font-size: 1.1em; flex: 1; }
|
||||||
|
.type { font-size: 0.8em; background: #334155; padding: 2px 8px; border-radius: 12px; color: #94a3b8; }
|
||||||
|
.badge { padding: 4px 12px; border-radius: 12px; font-size: 0.85em; font-weight: bold; }
|
||||||
|
.badge.online { background: #166534; color: #dcfce7; }
|
||||||
|
.badge.offline { background: #991b1b; color: #fca5a5; }
|
||||||
|
.badge.timeout { background: #92400e; color: #fde68a; }
|
||||||
|
.badge.ssl_error { background: #92400e; color: #fde68a; }
|
||||||
|
.badge.error { background: #991b1b; color: #fca5a5; }
|
||||||
|
.details { margin-top: 10px; font-size: 0.85em; color: #94a3b8; }
|
||||||
|
.error { color: #fca5a5; margin-top: 5px; font-size: 0.85em; word-break: break-word; }
|
||||||
|
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin: 20px 0; }
|
||||||
|
.card-sum { background: #1e293b; padding: 15px; border-radius: 8px; text-align: center; }
|
||||||
|
.card-sum .count { font-size: 2em; font-weight: bold; color: #60a5fa; }
|
||||||
|
.card-sum .label { color: #94a3b8; }
|
||||||
|
.history-note { font-size: 0.75em; color: #64748b; margin-top: 5px; }
|
||||||
|
.refresh-btn { position: absolute; top: 10px; right: 10px; background: transparent; border: none; cursor: pointer; font-size: 1.2em; padding: 0; line-height: 1; }
|
||||||
|
.refresh-btn:hover { opacity: 0.8; }
|
||||||
|
.refresh-btn:disabled { cursor: not-allowed; opacity: 0.5; }
|
||||||
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||||
|
.loading-spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid #334155; border-top-color: #60a5fa; border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 8px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Homelab Status Monitor</h1>
|
||||||
|
<div class="version">v''' + VERSION + '''</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="refresh-all" onclick="refreshAll()">Refresh All</button>
|
||||||
|
<button onclick="location.reload()">Reload Page</button>
|
||||||
|
<span id="ts"></span>
|
||||||
|
<span id="global-loading"></span>
|
||||||
|
</div>
|
||||||
|
<div class="summary">
|
||||||
|
<div class="card-sum"><div class="count" id="total">0</div><div class="label">Total</div></div>
|
||||||
|
<div class="card-sum"><div class="count" id="ok">0</div><div class="label">Online</div></div>
|
||||||
|
<div class="card-sum"><div class="count" id="ko">0</div><div class="label">Offline</div></div>
|
||||||
|
<div class="card-sum"><div class="count" id="err">0</div><div class="label">Errors</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid" id="grid"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let data = {};
|
||||||
|
let history = {};
|
||||||
|
let refreshInProgress = false;
|
||||||
|
|
||||||
|
function getClass(s) {
|
||||||
|
var classes = {"online": "online", "offline": "offline", "timeout": "timeout", "error": "error", "ssl_error": "ssl_error"};
|
||||||
|
return classes[s] || "offline";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtHist(sid) {
|
||||||
|
if (history[sid]) {
|
||||||
|
var h = history[sid];
|
||||||
|
return "Changed: " + h.old_status + " -> " + h.new_status + " at " + new Date(h.changed_at).toLocaleTimeString();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGlobalLoad(b) {
|
||||||
|
refreshInProgress = b;
|
||||||
|
var elem = document.getElementById("global-loading");
|
||||||
|
elem.innerHTML = b ? "<span class=\"loading-spinner\"></span>" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
let h = "";
|
||||||
|
let t = 0, ok = 0, ko = 0, err = 0;
|
||||||
|
for (const [id, v] of Object.entries(data)) {
|
||||||
|
const c = v.config, r = v.result, s = r.status;
|
||||||
|
t++;
|
||||||
|
if (s === "online") ok++;
|
||||||
|
else if (s === "offline" || s === "error" || s === "timeout" || s === "ssl_error") ko++;
|
||||||
|
else err++;
|
||||||
|
|
||||||
|
const bc = getClass(s);
|
||||||
|
const pi = c.url ? c.host + ":" + c.port + c.url : c.host + ":" + c.port;
|
||||||
|
|
||||||
|
h += "<div class=\"card " + bc + "\">";
|
||||||
|
h += "<button class=\"refresh-btn\" onclick=\"refreshService('" + id + "')\" title=\"Refresh\">🔄</button>";
|
||||||
|
h += "<div class=\"header\"><div class=\"name\">" + c.name + "</div><span class=\"type\">" + c.type + "</span></div>";
|
||||||
|
h += "<span class=\"badge " + bc + "\">" + s.replace(/_/g, " ") + "</span>";
|
||||||
|
h += "<div class=\"details\">" + pi + "</div>";
|
||||||
|
if (r.error) h += "<div class=\"error\">" + r.error + "</div>";
|
||||||
|
if (r.status_code) h += "<div class=\"details\">Status: " + r.status_code + "</div>";
|
||||||
|
h += "<div class=\"details\">Checked: " + new Date(v.time).toLocaleTimeString() + "</div>";
|
||||||
|
const hist = fmtHist(id);
|
||||||
|
if (hist) h += "<div class=\"history-note\">" + hist + "</div>";
|
||||||
|
h += "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("grid").innerHTML = h || "<p style=\"color:#64748b;text-align:center;\">No services to display</p>";
|
||||||
|
document.getElementById("total").textContent = t;
|
||||||
|
document.getElementById("ok").textContent = ok;
|
||||||
|
document.getElementById("ko").textContent = ko;
|
||||||
|
document.getElementById("err").textContent = err;
|
||||||
|
document.getElementById("ts").textContent = "Updated: " + new Date().toLocaleTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshService(sid) {
|
||||||
|
const btn = event.target;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = "<span class=\"loading-spinner\"></span>";
|
||||||
|
fetch("/api/check/" + encodeURIComponent(sid))
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
data[sid] = d;
|
||||||
|
if (d.history) history[sid] = d.history;
|
||||||
|
render();
|
||||||
|
})
|
||||||
|
.catch(err => console.error("Error:", err))
|
||||||
|
.finally(() => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = "🔄";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAll() {
|
||||||
|
const btn = document.querySelector(".refresh-all");
|
||||||
|
setGlobalLoad(true);
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = "<span class=\"loading-spinner\"></span> Refreshing...";
|
||||||
|
fetch("/api/refresh")
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
data = d.data;
|
||||||
|
history = d.history || {};
|
||||||
|
render();
|
||||||
|
})
|
||||||
|
.catch(err => console.error("Error:", err))
|
||||||
|
.finally(() => {
|
||||||
|
setGlobalLoad(false);
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = "Refresh All";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
fetch("/api/status")
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
data = d.data;
|
||||||
|
history = d.history || {};
|
||||||
|
render();
|
||||||
|
})
|
||||||
|
.catch(err => console.error("Error:", err));
|
||||||
|
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
setInterval(function() {
|
||||||
|
setGlobalLoad(true);
|
||||||
|
fetch("/api/status")
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(function(d) {
|
||||||
|
data = d.data;
|
||||||
|
history = d.history || {};
|
||||||
|
render();
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.error("Error:", err);
|
||||||
|
})
|
||||||
|
.finally(function() {
|
||||||
|
setGlobalLoad(false);
|
||||||
|
});
|
||||||
|
}, 30000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>'''
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
"""HTTP request handler."""
|
||||||
|
|
||||||
|
def log_message(self, *args):
|
||||||
|
"""Suppress default logging."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
"""Handle GET requests."""
|
||||||
|
try:
|
||||||
|
if self.path in ['/', '/index.html']:
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'text/html; charset=utf-8')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(generate_html().encode('utf-8'))
|
||||||
|
|
||||||
|
elif self.path == '/api/status':
|
||||||
|
if not status_cache:
|
||||||
|
check_all()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.send_header('Cache-Control', 'no-cache')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({"data": status_cache, "history": status_history}, default=str).encode('utf-8'))
|
||||||
|
|
||||||
|
elif self.path == '/api/refresh':
|
||||||
|
result = check_all()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({"data": result, "history": status_history}, default=str).encode('utf-8'))
|
||||||
|
|
||||||
|
elif self.path.startswith('/api/check/'):
|
||||||
|
sid = self.path.split('/')[-1]
|
||||||
|
if sid in SERVICES:
|
||||||
|
old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown")
|
||||||
|
rd = check_single(sid)
|
||||||
|
new_st = rd["result"]["status"]
|
||||||
|
if old != new_st:
|
||||||
|
update_history(sid, old, new_st)
|
||||||
|
with lock:
|
||||||
|
status_cache[sid] = rd
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({**rd, "history": status_history.get(sid)}, default=str).encode('utf-8'))
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.send_response(500)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
print(f"Starting Status Monitor v{VERSION}")
|
||||||
|
check_all()
|
||||||
|
server = ThreadingHTTPServer(('', 80), Handler)
|
||||||
|
print(f'Status Monitor v{VERSION} running on port 80')
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Executable
+63
@@ -0,0 +1,63 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
homeassistant:
|
||||||
|
rule: "Host(`ha.peis.fr`)"
|
||||||
|
entryPoints:
|
||||||
|
- "websecure"
|
||||||
|
tls:
|
||||||
|
certResolver: "letsencrypt"
|
||||||
|
service: homeassistant
|
||||||
|
middlewares:
|
||||||
|
- "security-headers"
|
||||||
|
|
||||||
|
traefik-public:
|
||||||
|
rule: "Host(`traefik.peis.fr`)"
|
||||||
|
service: api@internal
|
||||||
|
entryPoints:
|
||||||
|
- "websecure"
|
||||||
|
tls:
|
||||||
|
certResolver: "letsencrypt"
|
||||||
|
middlewares:
|
||||||
|
- "auth"
|
||||||
|
|
||||||
|
status-monitor:
|
||||||
|
rule: "Host(`status.peis.fr`)"
|
||||||
|
entryPoints:
|
||||||
|
- "websecure"
|
||||||
|
tls:
|
||||||
|
certResolver: "letsencrypt"
|
||||||
|
service: status-monitor
|
||||||
|
|
||||||
|
services:
|
||||||
|
homeassistant:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://192.168.0.43:8123"
|
||||||
|
|
||||||
|
status-monitor:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://192.168.0.201:80"
|
||||||
|
|
||||||
|
middlewares:
|
||||||
|
auth:
|
||||||
|
basicAuth:
|
||||||
|
users:
|
||||||
|
- "admin:$apr1$xnK9sM6u$q81sJuvBe5R3rfefqDLuk1"
|
||||||
|
security-headers:
|
||||||
|
headers:
|
||||||
|
frameDeny: true
|
||||||
|
sslRedirect: true
|
||||||
|
browserXssFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
stsSeconds: 31536000
|
||||||
|
customResponseHeaders:
|
||||||
|
X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"
|
||||||
|
X-Content-Type-Options: "nosniff"
|
||||||
|
X-Frame-Options: "DENY"
|
||||||
|
X-XSS-Protection: "1; mode=block"
|
||||||
|
Referrer-Policy: "strict-origin-when-cross-origin"
|
||||||
|
Permissions-Policy: "camera=(), microphone=(), geolocation()"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
creer une todo_IA.md ou tu liste au fure et à mesure les améliorations futures possibles par thème.
|
||||||
|
STATUS Monitor: add for each service, url au format *.peis.fr et IP locale. Les 2 doivent être des liens pour permettre l'ouverture du service.
|
||||||
|
STATUS Monitor: la page s'ouvre, en https, ça c'est valide. Par contre je ne vois plus les services, j'ai uniquement la ligne de résumé en haut.
|
||||||
|
sur une LXC proxmox dédiée si c'est possible, installe authentik pour sécuriser traefik, home assistant...
|
||||||
|
rajoute une couche de sécurité avec Pocketid
|
||||||
Executable
+233
@@ -0,0 +1,233 @@
|
|||||||
|
# TODO List - Améliorations Futures Homelab
|
||||||
|
|
||||||
|
## 📋 Organisation
|
||||||
|
- **Priorité** : ⭐ = Faible, ⭐⭐ = Moyenne, ⭐⭐⭐ = Élevée
|
||||||
|
- **Statut** : ✅ = Terminé, 🚧 = En cours, ⏳ = À faire
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Sécurité & Authentification
|
||||||
|
|
||||||
|
### Authentik (⭐⭐⭐)
|
||||||
|
- [ ] Créer un LXC dédié (CT203 ?) pour Authentik
|
||||||
|
- [ ] Installer Authentik via Docker
|
||||||
|
- [ ] Configurer Traefik pour authentik.peis.fr
|
||||||
|
- [ ] Intégrer Authentik avec Traefik (middleware)
|
||||||
|
- [ ] Configurer les providers d'authentification :
|
||||||
|
- [ ] Local (base de données)
|
||||||
|
- [ ] Google OAuth
|
||||||
|
- [ ] GitHub OAuth
|
||||||
|
- [ ] Microsoft OAuth
|
||||||
|
- [ ] Créer des applications dans Authentik pour :
|
||||||
|
- [ ] Traefik Dashboard
|
||||||
|
- [ ] Home Assistant
|
||||||
|
- [ ] Gitea
|
||||||
|
- [ ] Status Monitor
|
||||||
|
- [ ] Configurer le SSO pour tous les services
|
||||||
|
- [ ] Tester l'authentification unique
|
||||||
|
|
||||||
|
### PocketID (⭐⭐)
|
||||||
|
- [ ] Comprendre l'intégration avec Authentik
|
||||||
|
- [ ] Configurer PocketID comme fournisseur d'identité
|
||||||
|
- [ ] Intégrer avec les applications existantes
|
||||||
|
- [ ] Tester le flux d'authentification
|
||||||
|
|
||||||
|
### Sécurité Traefik (⭐⭐⭐)
|
||||||
|
- [ ] Configurer l'authentification basique pour toutes les routes sensibles
|
||||||
|
- [ ] Ajouter des middlewares de sécurité :
|
||||||
|
- [ ] Rate limiting
|
||||||
|
- [ ] IP whitelisting pour certaines routes
|
||||||
|
- [ ] Blocking des pays non autorisés
|
||||||
|
- [ ] Configurer le dashboard Traefik avec authentification forte
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Services Web
|
||||||
|
|
||||||
|
### Status Monitor (CT201)
|
||||||
|
- [ ] **BUG** : La page n'affiche pas la liste des services (seulement le résumé) - À investiguer
|
||||||
|
- [x] Version v1.1.0 déployée
|
||||||
|
- [x] Animation de loading ajoutée
|
||||||
|
- [x] Boutons de refresh par service
|
||||||
|
- [x] Historique des changements de statut
|
||||||
|
- [x] Certificat SSL pour status.peis.fr
|
||||||
|
- [ ] Ajouter les URLs au format *.peis.fr et IP locale comme liens cliquables
|
||||||
|
- [ ] Ajouter des icônes pour chaque type de service
|
||||||
|
- [ ] Ajouter un filtre par type (SSH/HTTP/HTTPS)
|
||||||
|
- [ ] Ajouter un système de notifications (email/Telegram) en cas de downtime
|
||||||
|
- [ ] Ajouter un endpoint API pour récupérer le statut en JSON
|
||||||
|
|
||||||
|
### Gitea (CT202)
|
||||||
|
- [x] LXC 202 créé (192.168.0.202, 10Go stockage)
|
||||||
|
- [x] Docker installé
|
||||||
|
- [x] Gitea lancé via Docker (ports 3000, 2222)
|
||||||
|
- [x] Traefik configuré pour git.peis.fr
|
||||||
|
- [⏳] Attendre la propagation DNS et la génération du certificat SSL
|
||||||
|
- [ ] Configurer Gitea via l'interface web
|
||||||
|
- [ ] Créer un compte admin
|
||||||
|
- [ ] Configurer les dépôts pour :
|
||||||
|
- [ ] Sauvegardes des configurations Homelab
|
||||||
|
- [ ] Code du Status Monitor
|
||||||
|
- [ ] Scripts d'automatisation
|
||||||
|
- [ ] Documentation
|
||||||
|
- [ ] Configurer les webhooks pour CI/CD
|
||||||
|
- [ ] Mettre en place un backup automatique des dépôts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Infrastructure
|
||||||
|
|
||||||
|
### Containers LXC
|
||||||
|
- [x] CT202 (git-server) créé et configuré
|
||||||
|
- [ ] CT203 (authentik) à créer
|
||||||
|
- [ ] Vérifier la haute disponibilité de tous les containers
|
||||||
|
- [ ] Documenter le processus de création de nouveaux LXC
|
||||||
|
|
||||||
|
### Réseau
|
||||||
|
- [ ] Configurer un VLAN dédié pour les services publics
|
||||||
|
- [ ] Mettre en place un firewall entre les containers
|
||||||
|
- [ ] Documenter le schéma réseau complet
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
- [ ] Configurer des sauvegardes automatiques des LXC (CT201, CT202)
|
||||||
|
- [ ] Sauvegarder la configuration Traefik
|
||||||
|
- [ ] Sauvegarder la configuration des services Docker
|
||||||
|
- [ ] Tester la restauration des sauvegardes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Monitoring & Observabilité
|
||||||
|
|
||||||
|
### Status Monitor Améliorations
|
||||||
|
- [ ] Ajouter des métriques de performance (temps de réponse, uptime %)
|
||||||
|
- [ ] Ajouter un graphique d'historique des statuts
|
||||||
|
- [ ] Implémenter un système de health checks plus avancés
|
||||||
|
- [ ] Ajouter la vérification des certificats SSL
|
||||||
|
|
||||||
|
### Logging Centralisé
|
||||||
|
- [ ] Déployer un service de logging (Loki, ELK, Graylog)
|
||||||
|
- [ ] Configurer tous les services pour envoyer leurs logs
|
||||||
|
- [ ] Créer des dashboards de visualisation
|
||||||
|
|
||||||
|
### Métriques
|
||||||
|
- [ ] Déployer Prometheus + Grafana
|
||||||
|
- [ ] Configurer lexport des métriques depuis tous les services
|
||||||
|
- [ ] Créer des dashboards de monitoring
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Sécurité Réseau
|
||||||
|
|
||||||
|
### Pare-feu
|
||||||
|
- [ ] Configurer un pare-feu perimeter (pfSense, OPNsense)
|
||||||
|
- [ ] Bloquer les accès non autorisés entre les containers
|
||||||
|
- [ ] Configurer des règles de NAT pour les services exposés
|
||||||
|
|
||||||
|
### VPN
|
||||||
|
- [ ] Déployer WireGuard ou OpenVPN
|
||||||
|
- [ ] Configurer l'accès sécurisé au homelab depuis l'extérieur
|
||||||
|
- [ ] Intégrer avec Authentik pour l'authentification VPN
|
||||||
|
|
||||||
|
### Surveillance
|
||||||
|
- [ ] Déployer un IDS/IPS (Snort, Suricata)
|
||||||
|
- [ ] Configurer des alertes de sécurité
|
||||||
|
- [ ] Mettre en place un scanning régulier des vulnérabilités
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Applications Mobiles
|
||||||
|
|
||||||
|
- [ ] Créer une application mobile pour le Status Monitor
|
||||||
|
- [ ] Intégrer avec Home Assistant pour les notifications push
|
||||||
|
- [ ] Configurer l'accès sécurisé depuis mobile
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 Automatisation
|
||||||
|
|
||||||
|
- [ ] Automatiser la création de nouveaux LXC avec Ansible
|
||||||
|
- [ ] Automatiser le déploiement des services avec Docker Compose
|
||||||
|
- [ ] Configurer un pipeline CI/CD pour les applications auto-hébergées
|
||||||
|
- [ ] Mettre en place un système de gestion de configuration (GitOps)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
- [ ] Documenter l'architecture complète du homelab
|
||||||
|
- [ ] Créer un guide de déploiement pour nouveaux services
|
||||||
|
- [ ] Documenter les procédures de backup/restauration
|
||||||
|
- [ ] Créer un runbook pour les incidents courants
|
||||||
|
- [ ] Documenter la configuration réseau
|
||||||
|
- [ ] Documenter la configuration de sécurité
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Améliorations UI/UX
|
||||||
|
|
||||||
|
### Status Monitor
|
||||||
|
- [ ] Améliorer le design de la page
|
||||||
|
- [ ] Ajouter un mode sombre/clair
|
||||||
|
- [ ] Rendre la page responsive pour mobile
|
||||||
|
- [ ] Ajouter des animations plus fluides
|
||||||
|
- [ ] Permettre le tri des services par nom/statut/type
|
||||||
|
|
||||||
|
### Tableau de bord unifié
|
||||||
|
- [ ] Créer un tableau de bord qui agrège :
|
||||||
|
- [ ] Status Monitor
|
||||||
|
- [ ] Home Assistant
|
||||||
|
- [ ] Traefik Dashboard
|
||||||
|
- [ ] Gitea
|
||||||
|
- [ ] Authentik
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Maintenance
|
||||||
|
|
||||||
|
- [ ] Mettre à jour régulièrement tous les containers LXC
|
||||||
|
- [ ] Mettre à jour Docker et les images
|
||||||
|
- [ ] Vérifier les certificats SSL (renouvellement automatique)
|
||||||
|
- [ ] Monitorer l'espace disque
|
||||||
|
- [ ] Vérifier les performances
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 Calendrier
|
||||||
|
|
||||||
|
### Cette semaine
|
||||||
|
- [⭐⭐⭐] Résoudre le problème d'affichage du Status Monitor
|
||||||
|
- [⭐⭐⭐] Terminer la configuration de Gitea (attendre DNS + certificat)
|
||||||
|
- [⭐⭐] Créer CT203 pour Authentik
|
||||||
|
|
||||||
|
### Semaine prochaine
|
||||||
|
- [⭐⭐⭐] Installer et configurer Authentik
|
||||||
|
- [⭐⭐] Configurer le SSO pour les services existants
|
||||||
|
- [⭐⭐] Configurer PocketID
|
||||||
|
|
||||||
|
### Dans le mois
|
||||||
|
- [⭐⭐] Déployer Prometheus + Grafana
|
||||||
|
- [⭐] Configurer le logging centralisé
|
||||||
|
- [⭐] Automatiser les sauvegardes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
### Problèmes connus
|
||||||
|
1. **Status Monitor** : La page ne charge pas la liste des services. L'API retourne bien 25 services, donc le problème est côté client (JavaScript). À vérifier :
|
||||||
|
- Erreurs dans la console du navigateur
|
||||||
|
- Problème de CORS
|
||||||
|
- Cache navigateur
|
||||||
|
|
||||||
|
2. **git.peis.fr** : Le certificat SSL n'est pas encore généré. Attendre que le DNS se propage et qu'une requête arrive sur le domaine.
|
||||||
|
|
||||||
|
### Idées futures
|
||||||
|
- Mettre en place un système de monitoring des certificats SSL (expiration)
|
||||||
|
- Configurer un reverse proxy pour SSH via Traefik
|
||||||
|
- Déployer un service de chat auto-hébergé (Matrix, Rocket.Chat)
|
||||||
|
- Intégrer un système de tickets (Request Tracker)
|
||||||
|
- Configurer un NAS dédié pour le stockage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dernière mise à jour : 2026-07-18*
|
||||||
Executable
+149
@@ -0,0 +1,149 @@
|
|||||||
|
# Configuration Traefik Proxy - CT 200 (192.168.0.100)
|
||||||
|
|
||||||
|
## 📋 Structure des fichiers
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/traefik/
|
||||||
|
├── config/
|
||||||
|
│ ├── traefik.yml # Configuration principale Traefik
|
||||||
|
│ └── dynamic.yml # Middlewares et configuration dynamique
|
||||||
|
├── certs/
|
||||||
|
│ └── acme.json # Certificats Let's Encrypt (généré automatiquement)
|
||||||
|
├── logs/
|
||||||
|
├── docker-compose.yml # Traefik principal
|
||||||
|
├── docker-compose-whoami.yml # Service whoami (test.peis.fr)
|
||||||
|
└── docker-compose-ha.yml # Service Home Assistant (ha.peis.fr)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Commandes de déploiement
|
||||||
|
|
||||||
|
### 1. Copier les fichiers sur le serveur
|
||||||
|
|
||||||
|
Depuis votre machine Windows, utilisez SCP pour copier les fichiers :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copier la structure complète
|
||||||
|
scp -r "C:\Users\theo\.vibe\Proxmox\inventair\traefik-config\*" root@192.168.0.100:/opt/traefik/
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou connectez-vous en SSH et créez les fichiers manuellement :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -i "C:\Users\theo\.ssh\id_ed25519" root@192.168.0.100
|
||||||
|
|
||||||
|
# Creer les dossiers
|
||||||
|
mkdir -p /opt/traefik/{config,certs,logs}
|
||||||
|
|
||||||
|
# Creer les fichiers (copier le contenu depuis ce dossier)
|
||||||
|
# Voir les fichiers dans traefik-config/ pour le contenu
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Creer le reseau Docker (si pas deja fait)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create traefik_network
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Demarrer Traefik
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/traefik
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Verifier que Traefik fonctionne
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Voir les conteneurs
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
# Voir les logs
|
||||||
|
docker logs traefik-proxy
|
||||||
|
|
||||||
|
# Verifier l'acces au dashboard (apres obtention du certificat)
|
||||||
|
curl -vk https://traefik.peis.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Deployer les services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Service whoami (test.peis.fr)
|
||||||
|
docker-compose -f docker-compose-whoami.yml up -d
|
||||||
|
|
||||||
|
# Service Home Assistant (ha.peis.fr)
|
||||||
|
docker-compose -f docker-compose-ha.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Verification
|
||||||
|
|
||||||
|
### Acces aux services
|
||||||
|
- **Dashboard Traefik** : https://traefik.peis.fr (admin/admin123)
|
||||||
|
- **Service Whoami** : https://test.peis.fr
|
||||||
|
- **Home Assistant** : https://ha.peis.fr
|
||||||
|
|
||||||
|
### Verification des certificats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Voir les certificats obtenus
|
||||||
|
docker exec traefik-proxy cat /etc/traefik/certs/acme.json | jq '.letsencrypt.Certificates[].Domain.Main'
|
||||||
|
|
||||||
|
# Vérifier dans les logs
|
||||||
|
docker logs traefik-proxy | grep -i certificate
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Authentification Dashboard
|
||||||
|
|
||||||
|
Le mot de passe pour le dashboard Traefik est :
|
||||||
|
- **Utilisateur** : admin
|
||||||
|
- **Mot de passe** : admin123
|
||||||
|
|
||||||
|
Le hash bcrypt utilise est : `$2y$12$sMXYYX3YiSvlrgRka2OB6.15HjK/hDSifu73xL8KA5HICjaxn9H4G`
|
||||||
|
|
||||||
|
Pour changer le mot de passe, generez un nouveau hash avec :
|
||||||
|
```bash
|
||||||
|
# Sur une machine avec htpasswd
|
||||||
|
tpasswd -nb admin nouveaux_mot_de_passe | openssl passwd -apr1 -stdin
|
||||||
|
|
||||||
|
# Ou utilisez un generateur en ligne : https://hostingcanada.org/htpasswd-generator/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Notes importantes
|
||||||
|
|
||||||
|
1. **Let's Encrypt Production** : La configuration utilise l'API de production. Les certificats sont valides et reconnus par tous les navigateurs.
|
||||||
|
|
||||||
|
2. **DNS Requirement** : Assurez-vous que `test.peis.fr` et `ha.peis.fr` existent dans votre DNS Infomaniak avec un CNAME vers `lab.peis.fr`.
|
||||||
|
|
||||||
|
3. **Port Forwarding** : Le routeur doit rediriger TCP 80 et 443 vers 192.168.0.100 (deja configure).
|
||||||
|
|
||||||
|
4. **Acces local** : Pour accéder depuis votre réseau local, ajoutez une entrée dans votre fichier hosts :
|
||||||
|
```
|
||||||
|
192.168.0.100 traefik.peis.fr test.peis.fr ha.peis.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **HA Activé** : Le CT 200 est configure pour le High Availability dans Proxmox.
|
||||||
|
|
||||||
|
## 🛠 Dépannage
|
||||||
|
|
||||||
|
### Problème : 404 sur 192.168.0.100
|
||||||
|
C'est normal ! Traefik ne répond qu'aux noms d'hôte configurés. Utilisez les noms de domaine ou ajoutez une entrée dans /etc/hosts.
|
||||||
|
|
||||||
|
### Problème : Certificat non généré
|
||||||
|
```bash
|
||||||
|
# Verifier le port forwarding
|
||||||
|
docker exec traefik-proxy netstat -tuln | grep 80
|
||||||
|
|
||||||
|
# Verifier la connectivite depuis Internet
|
||||||
|
curl -v http://test.peis.fr/.well-known/acme-challenge/test
|
||||||
|
|
||||||
|
# Verifier les logs
|
||||||
|
docker logs traefik-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problème : Dashboard inaccessible
|
||||||
|
```bash
|
||||||
|
# Verifier que le conteneur traefik-proxy est running
|
||||||
|
docker ps | grep traefik-proxy
|
||||||
|
|
||||||
|
# Verifier les routes
|
||||||
|
docker exec traefik-proxy traefik get routers
|
||||||
|
```
|
||||||
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
# Backup Traefik Configuration - 18/07/2026
|
||||||
|
|
||||||
|
## 📋 Contexte
|
||||||
|
Sauvegarde des fichiers de configuration Traefik après resolution du problème de certificats Let's Encrypt STAGING.
|
||||||
|
|
||||||
|
## 🔍 Problème résolu
|
||||||
|
- **Symptôme** : traefik.peis.fr utilisait un certificat **STAGING** de Let's Encrypt (CN = (STAGING) Dastardly Durum YR1)
|
||||||
|
- **Cause** : Missing `caServer: https://acme-v02.api.letsencrypt.org/directory` dans traefik.yml
|
||||||
|
- **Solution** : Ajout de la ligne caServer pour forcer l'utilisation du serveur de production Let's Encrypt
|
||||||
|
|
||||||
|
## 📁 Fichiers sauvegardés
|
||||||
|
|
||||||
|
| Fichier | Description | Statut |
|
||||||
|
|--------|-------------|--------|
|
||||||
|
| `traefik-final.yml` | Configuration principale avec caServer PRODUCTION | ✅ **À utiliser** |
|
||||||
|
| `dynamic-final.yml` | Configuration dynamique (routers, middlewares) | ✅ OK |
|
||||||
|
| `docker-compose-final.yml` | Docker Compose avec Traefik v2.10 | ✅ OK |
|
||||||
|
| `traefik-original.yml` | Configuration originale (sans caServer) | ⚠️ STAGING |
|
||||||
|
| `dynamic-original.yml` | Configuration dynamique originale | ⚠️ Incomplète |
|
||||||
|
|
||||||
|
## 🚀 Configuration validée
|
||||||
|
|
||||||
|
### traefik-final.yml (PRODUCTION)
|
||||||
|
```yaml
|
||||||
|
certificatesResolvers:
|
||||||
|
letsencrypt:
|
||||||
|
acme:
|
||||||
|
email: theo@peis.fr
|
||||||
|
storage: /etc/traefik/certs/acme.json
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: web
|
||||||
|
caServer: https://acme-v02.api.letsencrypt.org/directory # ← **LIGNE CRITIQUE**
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Vérification
|
||||||
|
Pour confirmer que les certificats sont en production :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl s_client -connect traefik.peis.fr:443 -servername traefik.peis.fr \
|
||||||
|
| openssl x509 -noout -issuer
|
||||||
|
|
||||||
|
# Doit afficher :
|
||||||
|
# issuer= /C=US/O=Let's Encrypt/CN=R3
|
||||||
|
# (et NON "CN = (STAGING) Dastardly Durum YR1")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Restauration
|
||||||
|
Si besoin de restaurer :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sur CT200 (192.168.0.100)
|
||||||
|
cd /opt/traefik/config
|
||||||
|
|
||||||
|
# Remplacer par la version production
|
||||||
|
cp /path/vers/traefik-final.yml ./traefik.yml
|
||||||
|
|
||||||
|
# Supprimer l'ancien acme.json pour forcer la regénération
|
||||||
|
rm /opt/traefik/certs/acme.json
|
||||||
|
|
||||||
|
# Redémarrer Traefik
|
||||||
|
cd /opt/traefik
|
||||||
|
docker-compose down && docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📌 Notes
|
||||||
|
- Email Let's Encrypt : theo@peis.fr
|
||||||
|
- Domaine : peis.fr
|
||||||
|
- Sous-domaines configurés : ha.peis.fr, traefik.peis.fr, freebox.peis.fr
|
||||||
|
- Certificats stockés dans : /opt/traefik/certs/acme.json
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.10
|
||||||
|
container_name: traefik-proxy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./config/traefik.yml:/etc/traefik/traefik.yml:ro
|
||||||
|
- ./config/dynamic.yml:/etc/traefik/dynamic.yml:ro
|
||||||
|
- ./certs:/etc/traefik/certs
|
||||||
|
- ./logs:/var/log/traefik
|
||||||
|
networks:
|
||||||
|
- traefik_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.traefik-public.rule=Host(`traefik.peis.fr`)"
|
||||||
|
- "traefik.http.routers.traefik-public.service=api@internal"
|
||||||
|
- "traefik.http.routers.traefik-public.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.traefik-public.tls=true"
|
||||||
|
- "traefik.http.routers.traefik-public.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.routers.traefik-public.middlewares=auth"
|
||||||
|
- "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$12$$sMXYYX3YiSvlrgRka2OB6.15HjK/hDSifu73xL8KA5HICjaxn9H4G"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik_network:
|
||||||
|
external: true
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
homeassistant:
|
||||||
|
rule: "Host(`ha.peis.fr`)"
|
||||||
|
entryPoints:
|
||||||
|
- "websecure"
|
||||||
|
tls:
|
||||||
|
certResolver: "letsencrypt"
|
||||||
|
service: homeassistant
|
||||||
|
middlewares:
|
||||||
|
- "security-headers"
|
||||||
|
|
||||||
|
traefik-public:
|
||||||
|
rule: "Host(`traefik.peis.fr`)"
|
||||||
|
service: api@internal
|
||||||
|
entryPoints:
|
||||||
|
- "websecure"
|
||||||
|
tls:
|
||||||
|
certResolver: "letsencrypt"
|
||||||
|
middlewares:
|
||||||
|
- "auth"
|
||||||
|
|
||||||
|
services:
|
||||||
|
homeassistant:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://192.168.0.43:8123"
|
||||||
|
|
||||||
|
middlewares:
|
||||||
|
auth:
|
||||||
|
basicAuth:
|
||||||
|
users:
|
||||||
|
- "admin:$apr1$xnK9sM6u$q81sJuvBe5R3rfefqDLuk1"
|
||||||
|
security-headers:
|
||||||
|
headers:
|
||||||
|
frameDeny: true
|
||||||
|
sslRedirect: true
|
||||||
|
browserXssFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
stsSeconds: 31536000
|
||||||
|
customResponseHeaders:
|
||||||
|
X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"
|
||||||
|
X-Content-Type-Options: "nosniff"
|
||||||
|
X-Frame-Options: "DENY"
|
||||||
|
X-XSS-Protection: "1; mode=block"
|
||||||
|
Referrer-Policy: "strict-origin-when-cross-origin"
|
||||||
|
Permissions-Policy: "camera=(), microphone=(), geolocation()"
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
# Configuration dynamique Traefik
|
||||||
|
# Fichier : /opt/traefik/config/dynamic.yml
|
||||||
|
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
# Redirection HTTP -> HTTPS
|
||||||
|
redirect-to-https:
|
||||||
|
redirectScheme:
|
||||||
|
scheme: https
|
||||||
|
permanent: true
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
compress:
|
||||||
|
compress: {}
|
||||||
|
|
||||||
|
# Headers de securite
|
||||||
|
security-headers:
|
||||||
|
headers:
|
||||||
|
frameDeny: true
|
||||||
|
sslRedirect: true
|
||||||
|
browserXssFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
stsSeconds: 31536000
|
||||||
|
customResponseHeaders:
|
||||||
|
X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"
|
||||||
|
X-Content-Type-Options: "nosniff"
|
||||||
|
X-Frame-Options: "DENY"
|
||||||
|
X-XSS-Protection: "1; mode=block"
|
||||||
|
Referrer-Policy: "strict-origin-when-cross-origin"
|
||||||
|
Permissions-Policy: "camera=(), microphone=(), geolocation=()"
|
||||||
|
|
||||||
|
# Router pour Home Assistant (VM 100)
|
||||||
|
routers:
|
||||||
|
homeassistant:
|
||||||
|
rule: "Host(`ha.peis.fr`)"
|
||||||
|
entryPoints:
|
||||||
|
- "websecure"
|
||||||
|
tls:
|
||||||
|
certResolver: "letsencrypt"
|
||||||
|
service: homeassistant
|
||||||
|
middlewares:
|
||||||
|
- "security-headers"
|
||||||
|
|
||||||
|
# Service Home Assistant
|
||||||
|
services:
|
||||||
|
homeassistant:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://192.168.0.43:8123"
|
||||||
|
|
||||||
|
tcp:
|
||||||
|
routers: {}
|
||||||
|
services: {}
|
||||||
|
|
||||||
|
udp:
|
||||||
|
routers: {}
|
||||||
|
services: {}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
# Configuration globale
|
||||||
|
api:
|
||||||
|
dashboard: true
|
||||||
|
insecure: false
|
||||||
|
|
||||||
|
# Entrypoints (ports d'entree)
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
http:
|
||||||
|
redirections:
|
||||||
|
entryPoint:
|
||||||
|
to: websecure
|
||||||
|
scheme: https
|
||||||
|
websecure:
|
||||||
|
address: ":443"
|
||||||
|
|
||||||
|
# Providers (sources de configuration)
|
||||||
|
providers:
|
||||||
|
docker:
|
||||||
|
endpoint: "unix:///var/run/docker.sock"
|
||||||
|
exposedByDefault: false
|
||||||
|
network: traefik_network
|
||||||
|
file:
|
||||||
|
filename: /etc/traefik/dynamic.yml
|
||||||
|
watch: true
|
||||||
|
|
||||||
|
# Certificats SSL - PRODUCTION Let's Encrypt
|
||||||
|
certificatesResolvers:
|
||||||
|
letsencrypt:
|
||||||
|
acme:
|
||||||
|
email: theo@peis.fr
|
||||||
|
storage: /etc/traefik/certs/acme.json
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: web
|
||||||
|
caServer: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
log:
|
||||||
|
level: INFO
|
||||||
|
filePath: /var/log/traefik/traefik.log
|
||||||
|
|
||||||
|
accessLog:
|
||||||
|
filePath: /var/log/traefik/access.log
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
# Configuration globale Traefik v2.10
|
||||||
|
# Fichier : /opt/traefik/config/traefik.yml
|
||||||
|
|
||||||
|
api:
|
||||||
|
dashboard: true
|
||||||
|
insecure: false
|
||||||
|
|
||||||
|
# Entrypoints (ports d'entree)
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
http:
|
||||||
|
redirections:
|
||||||
|
entryPoint:
|
||||||
|
to: websecure
|
||||||
|
scheme: https
|
||||||
|
websecure:
|
||||||
|
address: ":443"
|
||||||
|
|
||||||
|
# Providers (sources de configuration)
|
||||||
|
providers:
|
||||||
|
docker:
|
||||||
|
endpoint: "unix:///var/run/docker.sock"
|
||||||
|
exposedByDefault: false
|
||||||
|
network: traefik_network
|
||||||
|
file:
|
||||||
|
filename: /etc/traefik/dynamic.yml
|
||||||
|
watch: true
|
||||||
|
|
||||||
|
# Certificats SSL - Let's Encrypt PRODUCTION
|
||||||
|
certificatesResolvers:
|
||||||
|
letsencrypt:
|
||||||
|
acme:
|
||||||
|
email: theo@peis.fr
|
||||||
|
storage: /etc/traefik/certs/acme.json
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: web
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
log:
|
||||||
|
level: INFO
|
||||||
|
filePath: /var/log/traefik/traefik.log
|
||||||
|
|
||||||
|
accessLog:
|
||||||
|
filePath: /var/log/traefik/access.log
|
||||||
Executable
+60
@@ -0,0 +1,60 @@
|
|||||||
|
# Configuration dynamique Traefik
|
||||||
|
# Fichier : /opt/traefik/config/dynamic.yml
|
||||||
|
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
# Redirection HTTP -> HTTPS
|
||||||
|
redirect-to-https:
|
||||||
|
redirectScheme:
|
||||||
|
scheme: https
|
||||||
|
permanent: true
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
compress:
|
||||||
|
compress: {}
|
||||||
|
|
||||||
|
# Headers de securite
|
||||||
|
security-headers:
|
||||||
|
headers:
|
||||||
|
frameDeny: true
|
||||||
|
sslRedirect: true
|
||||||
|
browserXssFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
stsSeconds: 31536000
|
||||||
|
customResponseHeaders:
|
||||||
|
X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"
|
||||||
|
X-Content-Type-Options: "nosniff"
|
||||||
|
X-Frame-Options: "DENY"
|
||||||
|
X-XSS-Protection: "1; mode=block"
|
||||||
|
Referrer-Policy: "strict-origin-when-cross-origin"
|
||||||
|
Permissions-Policy: "camera=(), microphone=(), geolocation=()"
|
||||||
|
|
||||||
|
# Router pour Home Assistant (VM 100)
|
||||||
|
routers:
|
||||||
|
homeassistant:
|
||||||
|
rule: "Host(`ha.peis.fr`)"
|
||||||
|
entryPoints:
|
||||||
|
- "websecure"
|
||||||
|
tls:
|
||||||
|
certResolver: "letsencrypt"
|
||||||
|
service: homeassistant
|
||||||
|
middlewares:
|
||||||
|
- "security-headers"
|
||||||
|
|
||||||
|
# Service Home Assistant
|
||||||
|
services:
|
||||||
|
homeassistant:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://192.168.0.43:8123"
|
||||||
|
|
||||||
|
tcp:
|
||||||
|
routers: {}
|
||||||
|
services: {}
|
||||||
|
|
||||||
|
udp:
|
||||||
|
routers: {}
|
||||||
|
services: {}
|
||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
# Configuration globale Traefik v2.10
|
||||||
|
# Fichier : /opt/traefik/config/traefik.yml
|
||||||
|
|
||||||
|
api:
|
||||||
|
dashboard: true
|
||||||
|
insecure: false
|
||||||
|
|
||||||
|
# Entrypoints (ports d'entree)
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
http:
|
||||||
|
redirections:
|
||||||
|
entryPoint:
|
||||||
|
to: websecure
|
||||||
|
scheme: https
|
||||||
|
websecure:
|
||||||
|
address: ":443"
|
||||||
|
|
||||||
|
# Providers (sources de configuration)
|
||||||
|
providers:
|
||||||
|
docker:
|
||||||
|
endpoint: "unix:///var/run/docker.sock"
|
||||||
|
exposedByDefault: false
|
||||||
|
network: traefik_network
|
||||||
|
file:
|
||||||
|
filename: /etc/traefik/dynamic.yml
|
||||||
|
watch: true
|
||||||
|
|
||||||
|
# Certificats SSL - Let's Encrypt PRODUCTION
|
||||||
|
certificatesResolvers:
|
||||||
|
letsencrypt:
|
||||||
|
acme:
|
||||||
|
email: theo@peis.fr
|
||||||
|
storage: /etc/traefik/certs/acme.json
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: web
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
log:
|
||||||
|
level: INFO
|
||||||
|
filePath: /var/log/traefik/traefik.log
|
||||||
|
|
||||||
|
accessLog:
|
||||||
|
filePath: /var/log/traefik/access.log
|
||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.10
|
||||||
|
container_name: traefik-proxy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./config/traefik.yml:/etc/traefik/traefik.yml:ro
|
||||||
|
- ./config/dynamic.yml:/etc/traefik/dynamic.yml:ro
|
||||||
|
- ./certs:/etc/traefik/certs
|
||||||
|
- ./logs:/var/log/traefik
|
||||||
|
networks:
|
||||||
|
- traefik_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.traefik-public.rule=Host(`traefik.peis.fr`)"
|
||||||
|
- "traefik.http.routers.traefik-public.service=api@internal"
|
||||||
|
- "traefik.http.routers.traefik-public.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.traefik-public.tls=true"
|
||||||
|
- "traefik.http.routers.traefik-public.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.routers.traefik-public.middlewares=auth"
|
||||||
|
- "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$12$$sMXYYX3YiSvlrgRka2OB6.15HjK/hDSifu73xL8KA5HICjaxn9H4G"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik_network:
|
||||||
|
external: true
|
||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
whoami:
|
||||||
|
image: traefik/whoami
|
||||||
|
container_name: whoami
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
# Router HTTP -> HTTPS (redirection automatique)
|
||||||
|
- "traefik.http.routers.whoami-http.rule=Host(`test.peis.fr`)"
|
||||||
|
- "traefik.http.routers.whoami-http.entrypoints=web"
|
||||||
|
- "traefik.http.routers.whoami-http.middlewares=redirect-to-https@file"
|
||||||
|
# Router HTTPS (principal)
|
||||||
|
- "traefik.http.routers.whoami.rule=Host(`test.peis.fr`)"
|
||||||
|
- "traefik.http.routers.whoami.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.whoami.tls=true"
|
||||||
|
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
|
||||||
|
# Configuration du service
|
||||||
|
- "traefik.http.services.whoami.loadbalancer.server.port=80"
|
||||||
|
networks:
|
||||||
|
- traefik_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik_network:
|
||||||
|
external: true
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.10
|
||||||
|
container_name: traefik-proxy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./config/traefik.yml:/etc/traefik/traefik.yml:ro
|
||||||
|
- ./config/dynamic.yml:/etc/traefik/dynamic.yml:ro
|
||||||
|
- ./certs:/etc/traefik/certs
|
||||||
|
- ./logs:/var/log/traefik
|
||||||
|
networks:
|
||||||
|
- traefik_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
# Dashboard accessible via traefik.peis.fr (HTTPS avec cert LE)
|
||||||
|
- "traefik.http.routers.traefik-public.rule=Host(`traefik.peis.fr`)"
|
||||||
|
- "traefik.http.routers.traefik-public.service=api@internal"
|
||||||
|
- "traefik.http.routers.traefik-public.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.traefik-public.tls=true"
|
||||||
|
- "traefik.http.routers.traefik-public.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.routers.traefik-public.middlewares=auth"
|
||||||
|
# Middleware d'authentification pour le dashboard
|
||||||
|
- "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$12$$sMXYYX3YiSvlrgRka2OB6.15HjK/hDSifu73xL8KA5HICjaxn9H4G"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik_network:
|
||||||
|
external: true
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
homeassistant:
|
||||||
|
rule: "Host(`ha.peis.fr`)"
|
||||||
|
entryPoints:
|
||||||
|
- "websecure"
|
||||||
|
tls:
|
||||||
|
certResolver: "letsencrypt"
|
||||||
|
service: homeassistant
|
||||||
|
middlewares:
|
||||||
|
- "security-headers"
|
||||||
|
|
||||||
|
traefik-public:
|
||||||
|
rule: "Host(`traefik.peis.fr`)"
|
||||||
|
service: api@internal
|
||||||
|
entryPoints:
|
||||||
|
- "websecure"
|
||||||
|
tls:
|
||||||
|
certResolver: "letsencrypt"
|
||||||
|
middlewares:
|
||||||
|
- "auth"
|
||||||
|
|
||||||
|
services:
|
||||||
|
homeassistant:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://192.168.0.43:8123"
|
||||||
|
|
||||||
|
middlewares:
|
||||||
|
auth:
|
||||||
|
basicAuth:
|
||||||
|
users:
|
||||||
|
- "admin:$apr1$xnK9sM6u$q81sJuvBe5R3rfefqDLuk1"
|
||||||
|
security-headers:
|
||||||
|
headers:
|
||||||
|
frameDeny: true
|
||||||
|
sslRedirect: true
|
||||||
|
browserXssFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
stsSeconds: 31536000
|
||||||
|
customResponseHeaders:
|
||||||
|
X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"
|
||||||
|
X-Content-Type-Options: "nosniff"
|
||||||
|
X-Frame-Options: "DENY"
|
||||||
|
X-XSS-Protection: "1; mode=block"
|
||||||
|
Referrer-Policy: "strict-origin-when-cross-origin"
|
||||||
|
Permissions-Policy: "camera=(), microphone=(), geolocation()"
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
# Configuration globale
|
||||||
|
api:
|
||||||
|
dashboard: true
|
||||||
|
insecure: false
|
||||||
|
|
||||||
|
# Entrypoints (ports d'entree)
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
http:
|
||||||
|
redirections:
|
||||||
|
entryPoint:
|
||||||
|
to: websecure
|
||||||
|
scheme: https
|
||||||
|
websecure:
|
||||||
|
address: ":443"
|
||||||
|
|
||||||
|
# Providers (sources de configuration)
|
||||||
|
providers:
|
||||||
|
docker:
|
||||||
|
endpoint: "unix:///var/run/docker.sock"
|
||||||
|
exposedByDefault: false
|
||||||
|
network: traefik_network
|
||||||
|
file:
|
||||||
|
filename: /etc/traefik/dynamic.yml
|
||||||
|
watch: true
|
||||||
|
|
||||||
|
# Certificats SSL - PRODUCTION Let's Encrypt
|
||||||
|
certificatesResolvers:
|
||||||
|
letsencrypt:
|
||||||
|
acme:
|
||||||
|
email: theo@peis.fr
|
||||||
|
storage: /etc/traefik/certs/acme.json
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: web
|
||||||
|
caServer: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
log:
|
||||||
|
level: INFO
|
||||||
|
filePath: /var/log/traefik/traefik.log
|
||||||
|
|
||||||
|
accessLog:
|
||||||
|
filePath: /var/log/traefik/access.log
|
||||||
Reference in New Issue
Block a user