feat: première version fonctionnelle
This commit is contained in:
8
.env.example
Normal file
8
.env.example
Normal file
@@ -0,0 +1,8 @@
|
||||
# Base de données
|
||||
DATABASE_URL=sqlite:///data/zebra.db
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS=http://localhost:3000,http://localhost
|
||||
|
||||
# Frontend API URL
|
||||
VITE_API_URL=http://localhost:8000/api
|
||||
237
CLAUDE.md
Normal file
237
CLAUDE.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Directives Claude pour Zebra Power 🍅
|
||||
|
||||
## Contexte du Projet
|
||||
|
||||
**Zebra Power** (nommée d'après la tomate verte zebra) est une application web dockerisée pour la gestion de serveurs via Wake-on-LAN et le contrôle de machines virtuelles Proxmox. Il s'agit d'un outil d'administration réseau légitime à usage défensif uniquement.
|
||||
|
||||
## Architecture et Technologies
|
||||
|
||||
### Backend (FastAPI + SQLite)
|
||||
|
||||
- **Langages**: Python 3.11+
|
||||
- **Framework**: FastAPI 0.104.1 avec SQLAlchemy 2.0.23
|
||||
- **Base de données**: SQLite (fichier local `./data/zebra.db`)
|
||||
- **Services**: Wake-on-LAN, API Proxmox, logging centralisé
|
||||
- **Point d'entrée**: `backend/app/main.py`
|
||||
|
||||
### Frontend (Vue.js 3)
|
||||
|
||||
- **Framework**: Vue.js 3.4.0 + Vue Router + Pinia
|
||||
- **Build**: Vite 5.0.8
|
||||
- **Styling**: Tailwind CSS 3.3.6
|
||||
- **Composants**: Headless UI + Heroicons
|
||||
- **Point d'entrée**: `frontend/src/main.js`
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- **Containerisation**: Docker Compose
|
||||
- **Proxy**: Nginx (port 80)
|
||||
- **Réseau**: Mode host pour backend (requis WOL)
|
||||
|
||||
## Standards de Développement
|
||||
|
||||
### Code Style
|
||||
|
||||
- **Python**: PEP 8, type hints obligatoires
|
||||
- **JavaScript**: ESLint + Prettier
|
||||
- **Vue**: Composition API, script setup
|
||||
- **CSS**: Tailwind utilitaires, composants réutilisables
|
||||
|
||||
### Structure des Fichiers
|
||||
|
||||
```
|
||||
backend/app/
|
||||
├── api/ # Endpoints REST
|
||||
├── models/ # Schémas Pydantic + SQLAlchemy
|
||||
├── services/ # Logique métier
|
||||
└── main.py # Application FastAPI
|
||||
|
||||
frontend/src/
|
||||
├── components/ # Composants Vue réutilisables
|
||||
├── views/ # Pages/routes principales
|
||||
├── services/ # API client
|
||||
└── main.js # Bootstrap Vue
|
||||
```
|
||||
|
||||
## Commandes de Développement
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev # Développement (port 3000)
|
||||
npm run build # Production
|
||||
```
|
||||
|
||||
### Docker (Production)
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
docker-compose logs -f [service]
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## Directives Spécifiques pour les Agents
|
||||
|
||||
### 🔒 Sécurité - OBLIGATOIRE
|
||||
|
||||
1. **Usage défensif uniquement** - Ne jamais créer/modifier du code malveillant
|
||||
2. **Wake-on-LAN légitime** - Uniquement pour administration réseau autorisée
|
||||
3. **Proxmox management** - Outils d'administration datacenter standards
|
||||
4. **Pas de backdoors** - Aucune fonctionnalité cachée ou malveillante
|
||||
|
||||
### 📋 Bonnes Pratiques de Code
|
||||
|
||||
#### Backend Python
|
||||
|
||||
- Utiliser les types hints (`from typing import...`)
|
||||
- Valider avec Pydantic pour les entrées API
|
||||
- Gérer les exceptions avec FastAPI handlers
|
||||
- Logger via le service centralisé (`logging_service.py`)
|
||||
- Base de données via SQLAlchemy sessions (`get_db()`)
|
||||
|
||||
#### Frontend Vue.js
|
||||
|
||||
- Composition API avec `<script setup>`
|
||||
- Props typés avec `defineProps()`
|
||||
- État global via Pinia stores
|
||||
- Requêtes API via `services/api.js`
|
||||
- Composants atomiques réutilisables
|
||||
|
||||
#### Docker
|
||||
|
||||
- Respect des ports existants (80, 3000, 8000)
|
||||
- Volumes pour persistance (`./data`)
|
||||
- Mode host obligatoire pour le backend (WOL)
|
||||
- Variables d'environnement via `.env` ou compose
|
||||
|
||||
### 📁 Fichiers Importants
|
||||
|
||||
#### Configuration
|
||||
|
||||
- `docker-compose.yml` - Stack complète
|
||||
- `backend/requirements.txt` - Dépendances Python
|
||||
- `frontend/package.json` - Dépendances Node.js
|
||||
- `nginx/nginx.conf` - Configuration proxy
|
||||
|
||||
#### Modèles de Données
|
||||
|
||||
- `backend/app/database.py` - Tables SQLAlchemy
|
||||
- `backend/app/models/schemas.py` - Schémas Pydantic
|
||||
- Toujours synchroniser modèles DB et API
|
||||
|
||||
#### API Endpoints
|
||||
|
||||
- `backend/app/api/servers.py` - Gestion serveurs WOL
|
||||
- `backend/app/api/wol.py` - Wake-on-LAN et ping
|
||||
- `backend/app/api/proxmox.py` - Clusters et VMs Proxmox
|
||||
|
||||
#### Services Métier
|
||||
|
||||
- `backend/app/services/wol_service.py` - Logique Wake-on-LAN
|
||||
- `backend/app/services/proxmox_service.py` - Intégration Proxmox
|
||||
- `backend/app/services/logging_service.py` - Journalisation
|
||||
|
||||
### 🔧 Debugging et Logs
|
||||
|
||||
#### Logs Backend
|
||||
|
||||
- FastAPI logs via uvicorn
|
||||
- Application logs dans ActionLog table
|
||||
- Exceptions catchées globalement dans `main.py`
|
||||
|
||||
#### Logs Frontend
|
||||
|
||||
- Console.log pour développement
|
||||
- Error handling dans les composants Vue
|
||||
- API errors via axios interceptors
|
||||
|
||||
#### Docker Logs
|
||||
|
||||
```bash
|
||||
docker-compose logs backend
|
||||
docker-compose logs frontend
|
||||
docker-compose logs nginx
|
||||
```
|
||||
|
||||
### 📊 Base de Données
|
||||
|
||||
#### Tables Principales
|
||||
|
||||
- `servers` - Serveurs Wake-on-LAN
|
||||
- `proxmox_clusters` - Configuration clusters
|
||||
- `action_logs` - Historique des actions
|
||||
- `wol_logs` - Logs spécifiques WOL (legacy)
|
||||
|
||||
#### Migrations
|
||||
|
||||
- SQLAlchemy auto-create via `init_db()`
|
||||
- Pas de migrations formelles (à implémenter)
|
||||
- Sauvegarde manuelle de `./data/zebra.db`
|
||||
|
||||
### 🌐 API et Frontend Communication
|
||||
|
||||
#### Standards API
|
||||
|
||||
- REST endpoints avec préfixes `/api/`
|
||||
- Codes status HTTP standards
|
||||
- JSON uniquement
|
||||
- CORS permissif (à restreindre)
|
||||
|
||||
#### Client Frontend
|
||||
|
||||
- Axios dans `services/api.js`
|
||||
- Base URL via variable d'environnement
|
||||
- Error handling centralisé
|
||||
- Loading states dans les composants
|
||||
|
||||
### ⚡ Performance
|
||||
|
||||
#### Backend
|
||||
|
||||
- SQLite avec pool de connexions
|
||||
- Async/await pour I/O operations
|
||||
- Pagination pour listes importantes
|
||||
|
||||
#### Frontend
|
||||
|
||||
- Lazy loading des routes Vue
|
||||
- Composition API pour réactivité
|
||||
- Tailwind purge pour CSS optimisé
|
||||
- Vite pour build rapide
|
||||
|
||||
### 🚀 Déploiement
|
||||
|
||||
#### Développement Local
|
||||
|
||||
1. Clone du repo
|
||||
2. `mkdir -p data` pour SQLite
|
||||
3. `docker-compose up -d`
|
||||
4. Interface sur http://localhost
|
||||
|
||||
#### Production
|
||||
|
||||
- Même stack Docker
|
||||
- Variables d'environnement sécurisées
|
||||
- HTTPS recommandé (nginx SSL)
|
||||
- Sauvegarde régulière de `./data/`
|
||||
|
||||
### 💡 Bonnes Pratiques Agents Claude
|
||||
|
||||
1. **Toujours lire la documentation** avant modification
|
||||
2. **Respecter l'architecture existante** - pas de refactoring majeur sans validation
|
||||
3. **Tester les changements** avec Docker Compose
|
||||
4. **Maintenir la cohérence** - même style, mêmes patterns
|
||||
5. **Documenter** les modifications non-triviales
|
||||
6. **Sécurité first** - valider les entrées, gérer les erreurs
|
||||
7. **Performance** - éviter les requêtes N+1, optimiser les queries
|
||||
8. **UX** - interfaces intuitives, feedback utilisateur
|
||||
268
DOCUMENTATION.md
Normal file
268
DOCUMENTATION.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# Documentation Technique - Zebra Power 🍅
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Zebra Power (nommée d'après la tomate verte zebra) est une application web dockerisée développée pour la gestion centralisée de serveurs via Wake-on-LAN et le contrôle de machines virtuelles Proxmox. L'application suit une architecture moderne avec séparation frontend/backend et utilise des standards industriels.
|
||||
|
||||
## Architecture Système
|
||||
|
||||
### Structure Générale
|
||||
```
|
||||
zebra_power/
|
||||
├── backend/ # API FastAPI
|
||||
├── frontend/ # Interface Vue.js 3
|
||||
├── nginx/ # Proxy reverse
|
||||
├── data/ # Persistance SQLite
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
||||
### Technologies
|
||||
|
||||
#### Backend (Python)
|
||||
- **Framework**: FastAPI 0.104.1 - API REST haute performance
|
||||
- **Base de données**: SQLAlchemy 2.0.23 avec SQLite
|
||||
- **Validation**: Pydantic 2.5.0 pour la sérialisation/validation des données
|
||||
- **Serveur**: Uvicorn avec support asynchrone
|
||||
- **Intégrations**:
|
||||
- `proxmoxer` 2.0.1 pour l'API Proxmox
|
||||
- `wakeonlan` 3.1.0 pour les paquets magiques WOL
|
||||
- `httpx` 0.25.2 pour les requêtes HTTP asynchrones
|
||||
|
||||
#### Frontend (JavaScript/Vue.js)
|
||||
- **Framework**: Vue.js 3.4.0 avec Composition API
|
||||
- **Routing**: Vue Router 4.2.5
|
||||
- **State Management**: Pinia 2.1.7
|
||||
- **HTTP Client**: Axios 1.6.2
|
||||
- **UI Components**:
|
||||
- Headless UI Vue 1.7.16
|
||||
- Heroicons Vue 2.0.18
|
||||
- **Styling**: Tailwind CSS 3.3.6
|
||||
- **Build Tool**: Vite 5.0.8
|
||||
|
||||
#### Infrastructure
|
||||
- **Containerisation**: Docker avec Docker Compose
|
||||
- **Proxy**: Nginx pour le routage et les fichiers statiques
|
||||
- **Réseau**: Mode host pour le backend (requis pour WOL)
|
||||
|
||||
## Modèles de Données
|
||||
|
||||
### Serveurs (servers)
|
||||
```python
|
||||
class Server:
|
||||
id: int # Clé primaire
|
||||
name: str # Nom du serveur
|
||||
ip_address: str # Adresse IP
|
||||
mac_address: str # Adresse MAC pour WOL
|
||||
description: str? # Description optionnelle
|
||||
is_online: bool # État en ligne (ping)
|
||||
last_ping: datetime? # Dernier ping réussi
|
||||
created_at: datetime # Date de création
|
||||
```
|
||||
|
||||
### Clusters Proxmox (proxmox_clusters)
|
||||
```python
|
||||
class ProxmoxCluster:
|
||||
id: int # Clé primaire
|
||||
name: str # Nom du cluster
|
||||
host: str # IP/hostname Proxmox
|
||||
username: str # Utilisateur (ex: root@pam)
|
||||
password: str # Mot de passe
|
||||
port: int = 8006 # Port API Proxmox
|
||||
verify_ssl: bool # Vérification SSL
|
||||
created_at: datetime # Date de création
|
||||
```
|
||||
|
||||
### Journalisation (action_logs)
|
||||
```python
|
||||
class ActionLog:
|
||||
id: int # Clé primaire
|
||||
action_type: str # Type: 'wol', 'proxmox', 'server'
|
||||
target_id: int? # ID de la cible
|
||||
target_name: str? # Nom de la cible
|
||||
action: str # Action: 'wake', 'start', 'stop', etc.
|
||||
timestamp: datetime # Horodatage
|
||||
success: bool # Succès/échec
|
||||
message: str? # Message descriptif
|
||||
details: str? # Données JSON supplémentaires
|
||||
```
|
||||
|
||||
## Architecture API
|
||||
|
||||
### Endpoints Serveurs (`/api/servers`)
|
||||
- `GET /` - Liste tous les serveurs avec statut
|
||||
- `POST /` - Créer un nouveau serveur
|
||||
- `PUT /{id}` - Modifier un serveur existant
|
||||
- `DELETE /{id}` - Supprimer un serveur
|
||||
|
||||
### Endpoints Wake-on-LAN (`/api/wol`)
|
||||
- `POST /wake/{server_id}` - Envoyer paquet magique WOL
|
||||
- `POST /ping/{server_id}` - Tester la connectivité
|
||||
- `GET /logs` - Récupérer l'historique WOL
|
||||
|
||||
### Endpoints Proxmox (`/api/proxmox`)
|
||||
- `GET /clusters` - Liste des clusters configurés
|
||||
- `POST /clusters` - Ajouter un nouveau cluster
|
||||
- `PUT /clusters/{id}` - Modifier un cluster
|
||||
- `DELETE /clusters/{id}` - Supprimer un cluster
|
||||
- `GET /clusters/{id}/vms` - Liste des VMs/containers
|
||||
- `POST /clusters/{id}/vms/{vmid}/start` - Démarrer VM/container
|
||||
- `POST /clusters/{id}/vms/{vmid}/stop` - Arrêter VM/container
|
||||
- `POST /clusters/{id}/vms/{vmid}/restart` - Redémarrer VM/container
|
||||
|
||||
## Services Backend
|
||||
|
||||
### Services de Base (`app/services/`)
|
||||
|
||||
#### WoL Service (`wol_service.py`)
|
||||
- Envoi de paquets magiques Wake-on-LAN
|
||||
- Ping automatique pour vérification de statut
|
||||
- Logging des actions WOL
|
||||
- Validation des adresses MAC
|
||||
|
||||
#### Proxmox Service (`proxmox_service.py`)
|
||||
- Connexion sécurisée aux clusters Proxmox
|
||||
- Gestion de l'authentification
|
||||
- Récupération des états VMs/containers
|
||||
- Contrôle des machines virtuelles
|
||||
|
||||
#### Logging Service (`logging_service.py`)
|
||||
- Centralisation des logs d'actions
|
||||
- Structuration des événements
|
||||
- Persistance en base de données
|
||||
- Historique des opérations
|
||||
|
||||
## Architecture Frontend
|
||||
|
||||
### Structure Vue.js (`frontend/src/`)
|
||||
|
||||
#### Composants Principaux
|
||||
- `App.vue` - Composant racine avec navigation
|
||||
- `views/Dashboard.vue` - Tableau de bord général
|
||||
- `views/Servers.vue` - Gestion des serveurs WOL
|
||||
- `views/Proxmox.vue` - Interface Proxmox
|
||||
|
||||
#### Services
|
||||
- `services/api.js` - Client HTTP centralisé pour l'API
|
||||
|
||||
#### Routing
|
||||
Configuration Vue Router pour navigation SPA avec lazy loading
|
||||
|
||||
#### State Management
|
||||
Pinia stores pour:
|
||||
- État des serveurs
|
||||
- Configuration Proxmox
|
||||
- Logs et historique
|
||||
|
||||
## Configuration Réseau
|
||||
|
||||
### Ports et Services
|
||||
- **80** - Nginx (point d'entrée principal)
|
||||
- **3000** - Frontend Vue.js (développement)
|
||||
- **8000** - Backend FastAPI
|
||||
- **8006** - API Proxmox (configurable)
|
||||
|
||||
### Réseau Docker
|
||||
- Backend en mode `host` pour envoi de paquets WOL
|
||||
- Frontend et Nginx en réseau bridge standard
|
||||
- Communication inter-conteneurs via noms de services
|
||||
|
||||
## Sécurité
|
||||
|
||||
### Authentification Proxmox
|
||||
- Stockage chiffré des mots de passe (à améliorer)
|
||||
- Support de l'authentification PAM/PVE
|
||||
- Gestion des certificats SSL
|
||||
|
||||
### CORS et Sécurité API
|
||||
- Configuration CORS permissive (à restreindre en production)
|
||||
- Gestion globale des exceptions
|
||||
- Validation Pydantic des entrées
|
||||
|
||||
### Recommandations
|
||||
1. Implémenter un système d'authentification pour l'interface
|
||||
2. Chiffrer les mots de passe Proxmox en base
|
||||
3. Restreindre les origines CORS
|
||||
4. Ajouter HTTPS avec certificats SSL
|
||||
|
||||
## Monitoring et Logs
|
||||
|
||||
### Logging Structure
|
||||
- Logs d'actions centralisés
|
||||
- Horodatage UTC
|
||||
- Détails JSON pour traçabilité
|
||||
- Séparation par type d'action
|
||||
|
||||
### Monitoring Serveurs
|
||||
- Ping automatique périodique
|
||||
- Mise à jour statut en temps réel
|
||||
- Historique de disponibilité
|
||||
|
||||
## Déploiement et Maintenance
|
||||
|
||||
### Environnement de Développement
|
||||
```bash
|
||||
# Backend
|
||||
cd backend && pip install -r requirements.txt
|
||||
uvicorn app.main:app --reload
|
||||
|
||||
# Frontend
|
||||
cd frontend && npm install && npm run dev
|
||||
```
|
||||
|
||||
### Production Docker
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Sauvegarde
|
||||
- Base de données: `./data/zebra.db`
|
||||
- Configuration: Variables d'environnement Docker
|
||||
- Logs: Intégrés à la base SQLite
|
||||
|
||||
## Performance et Optimisations
|
||||
|
||||
### Base de Données
|
||||
- SQLite pour simplicité (migration PostgreSQL recommandée)
|
||||
- Index sur colonnes fréquemment requêtées
|
||||
- Sessions SQLAlchemy avec pool de connexions
|
||||
|
||||
### Frontend
|
||||
- Lazy loading des composants Vue
|
||||
- Build optimisé avec Vite
|
||||
- CSS Tailwind avec purge automatique
|
||||
|
||||
### Infrastructure
|
||||
- Nginx pour fichiers statiques et cache
|
||||
- Compression gzip activée
|
||||
- Reverse proxy avec load balancing potentiel
|
||||
|
||||
## Tests et Qualité
|
||||
|
||||
### Recommandations de Tests
|
||||
1. Tests unitaires backend avec pytest
|
||||
2. Tests d'intégration API avec httpx
|
||||
3. Tests frontend avec Vitest
|
||||
4. Tests E2E avec Cypress
|
||||
|
||||
### Métriques Qualité
|
||||
- Couverture de code minimale 80%
|
||||
- Linting ESLint/Pylint
|
||||
- Formatage avec Prettier/Black
|
||||
- Validation TypeScript progressive
|
||||
|
||||
## Évolutions Futures
|
||||
|
||||
### Fonctionnalités Planifiées
|
||||
1. Authentification utilisateurs
|
||||
2. Groupes et permissions
|
||||
3. Monitoring avancé avec métriques
|
||||
4. Notifications (email, webhook)
|
||||
5. API GraphQL
|
||||
6. Interface mobile responsive
|
||||
|
||||
### Améliorations Techniques
|
||||
1. Migration PostgreSQL
|
||||
2. Microservices avec FastAPI
|
||||
3. Cache Redis
|
||||
4. Queue de tâches Celery
|
||||
5. Observabilité (metrics, tracing)
|
||||
136
README.md
Normal file
136
README.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Zebra Power 🍅
|
||||
|
||||
Application dockerisée pour la gestion Wake-on-LAN et le contrôle des VMs/Containers Proxmox.
|
||||
*Nommée d'après la tomate verte zebra*
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- **Wake-on-LAN** : Réveil de serveurs via paquets magiques
|
||||
- **Monitoring** : Ping automatique et statut des serveurs
|
||||
- **Proxmox** : Gestion des clusters, VMs et containers
|
||||
- **Dashboard** : Vue d'ensemble avec statistiques
|
||||
- **Interface moderne** : Vue.js 3 avec Tailwind CSS
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Backend** : FastAPI + SQLite
|
||||
- **Frontend** : Vue.js 3 + Tailwind CSS
|
||||
- **Proxy** : Nginx
|
||||
- **Base de données** : SQLite (fichier local)
|
||||
|
||||
## Installation
|
||||
|
||||
### Prérequis
|
||||
|
||||
- Docker & Docker Compose
|
||||
- Réseau local pour WOL
|
||||
|
||||
### Démarrage rapide
|
||||
|
||||
1. Cloner le projet :
|
||||
```bash
|
||||
git clone <repo-url>
|
||||
cd zebra_power
|
||||
```
|
||||
|
||||
2. Créer le répertoire de données :
|
||||
```bash
|
||||
mkdir -p data
|
||||
```
|
||||
|
||||
3. Lancer l'application :
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
4. Accéder à l'interface :
|
||||
- **Application** : http://localhost (via Nginx)
|
||||
- **Frontend direct** : http://localhost:3000
|
||||
- **API** : http://localhost:8000
|
||||
- **Documentation API** : http://localhost:8000/docs
|
||||
|
||||
## Configuration
|
||||
|
||||
### Serveurs Wake-on-LAN
|
||||
|
||||
1. Accédez à la section "Serveurs"
|
||||
2. Ajoutez un serveur avec :
|
||||
- Nom
|
||||
- Adresse IP
|
||||
- Adresse MAC
|
||||
- Description (optionnelle)
|
||||
|
||||
### Clusters Proxmox
|
||||
|
||||
1. Accédez à la section "Proxmox"
|
||||
2. Ajoutez un cluster avec :
|
||||
- Nom du cluster
|
||||
- Host/IP du serveur Proxmox
|
||||
- Nom d'utilisateur (ex: root@pam)
|
||||
- Mot de passe
|
||||
- Port (défaut: 8006)
|
||||
- Vérification SSL
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Serveurs
|
||||
- `GET /api/servers` - Liste des serveurs
|
||||
- `POST /api/servers` - Créer un serveur
|
||||
- `PUT /api/servers/{id}` - Modifier un serveur
|
||||
- `DELETE /api/servers/{id}` - Supprimer un serveur
|
||||
|
||||
### Wake-on-LAN
|
||||
- `POST /api/wol/wake/{server_id}` - Réveiller un serveur
|
||||
- `POST /api/wol/ping/{server_id}` - Ping un serveur
|
||||
- `GET /api/wol/logs` - Logs WOL
|
||||
|
||||
### Proxmox
|
||||
- `GET /api/proxmox/clusters` - Liste des clusters
|
||||
- `POST /api/proxmox/clusters` - Ajouter un cluster
|
||||
- `GET /api/proxmox/clusters/{id}/vms` - VMs d'un cluster
|
||||
- `POST /api/proxmox/clusters/{id}/vms/{vmid}/start` - Démarrer VM
|
||||
- `POST /api/proxmox/clusters/{id}/vms/{vmid}/stop` - Arrêter VM
|
||||
|
||||
## Développement
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
uvicorn app.main:app --reload
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Notes techniques
|
||||
|
||||
- Le backend utilise `network_mode: host` pour envoyer des paquets WOL
|
||||
- Les données sont persistées dans `./data/zebra.db`
|
||||
- L'interface supporte les thèmes sombres/clairs
|
||||
- Monitoring temps réel des statuts serveurs
|
||||
|
||||
## Dépannage
|
||||
|
||||
### WOL ne fonctionne pas
|
||||
- Vérifiez que WOL est activé sur le serveur cible
|
||||
- Assurez-vous que le conteneur utilise le réseau host
|
||||
- Vérifiez l'adresse MAC
|
||||
|
||||
### Connexion Proxmox échoue
|
||||
- Vérifiez les credentials et l'IP
|
||||
- Testez l'accès via navigateur
|
||||
- Vérifiez les certificats SSL
|
||||
|
||||
### Port déjà utilisé
|
||||
```bash
|
||||
docker-compose down
|
||||
# Modifier les ports dans docker-compose.yml si nécessaire
|
||||
docker-compose up -d
|
||||
```
|
||||
17
backend/Dockerfile
Normal file
17
backend/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
wakeonlan \
|
||||
iputils-ping \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app/ ./app/
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
1
backend/app/api/__init__.py
Normal file
1
backend/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API package
|
||||
166
backend/app/api/proxmox.py
Normal file
166
backend/app/api/proxmox.py
Normal file
@@ -0,0 +1,166 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.database import get_db, ProxmoxCluster
|
||||
from app.models.schemas import ProxmoxCluster as ProxmoxClusterSchema, ProxmoxClusterCreate, ProxmoxVM
|
||||
from app.services.proxmox_service import ProxmoxService
|
||||
from app.services.logging_service import LoggingService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/clusters", response_model=List[ProxmoxClusterSchema])
|
||||
async def get_clusters(db: Session = Depends(get_db)):
|
||||
clusters = db.query(ProxmoxCluster).all()
|
||||
return clusters
|
||||
|
||||
@router.post("/clusters", response_model=ProxmoxClusterSchema)
|
||||
async def create_cluster(cluster: ProxmoxClusterCreate, db: Session = Depends(get_db)):
|
||||
try:
|
||||
proxmox_service = ProxmoxService(
|
||||
host=cluster.host,
|
||||
user=cluster.username,
|
||||
password=cluster.password,
|
||||
port=cluster.port,
|
||||
verify_ssl=cluster.verify_ssl
|
||||
)
|
||||
|
||||
if not await proxmox_service.test_connection():
|
||||
raise HTTPException(status_code=400, detail="Cannot connect to Proxmox cluster")
|
||||
|
||||
db_cluster = ProxmoxCluster(**cluster.dict())
|
||||
db.add(db_cluster)
|
||||
db.commit()
|
||||
db.refresh(db_cluster)
|
||||
|
||||
# Log cluster creation
|
||||
LoggingService.log_proxmox_cluster_action(
|
||||
db=db,
|
||||
action="create",
|
||||
cluster_id=db_cluster.id,
|
||||
cluster_name=db_cluster.name,
|
||||
success=True,
|
||||
message=f"Proxmox cluster '{db_cluster.name}' created successfully"
|
||||
)
|
||||
|
||||
return db_cluster
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error creating cluster: {str(e)}")
|
||||
|
||||
@router.get("/clusters/{cluster_id}", response_model=ProxmoxClusterSchema)
|
||||
async def get_cluster(cluster_id: int, db: Session = Depends(get_db)):
|
||||
cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first()
|
||||
if not cluster:
|
||||
raise HTTPException(status_code=404, detail="Cluster not found")
|
||||
return cluster
|
||||
|
||||
@router.delete("/clusters/{cluster_id}")
|
||||
async def delete_cluster(cluster_id: int, db: Session = Depends(get_db)):
|
||||
cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first()
|
||||
if not cluster:
|
||||
raise HTTPException(status_code=404, detail="Cluster not found")
|
||||
|
||||
# Log cluster deletion before deleting
|
||||
LoggingService.log_proxmox_cluster_action(
|
||||
db=db,
|
||||
action="delete",
|
||||
cluster_id=cluster.id,
|
||||
cluster_name=cluster.name,
|
||||
success=True,
|
||||
message=f"Proxmox cluster '{cluster.name}' deleted successfully"
|
||||
)
|
||||
|
||||
db.delete(cluster)
|
||||
db.commit()
|
||||
return {"message": "Cluster deleted successfully"}
|
||||
|
||||
@router.get("/clusters/{cluster_id}/vms", response_model=List[ProxmoxVM])
|
||||
async def get_cluster_vms(cluster_id: int, db: Session = Depends(get_db)):
|
||||
cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first()
|
||||
if not cluster:
|
||||
raise HTTPException(status_code=404, detail="Cluster not found")
|
||||
|
||||
try:
|
||||
proxmox_service = ProxmoxService(
|
||||
host=cluster.host,
|
||||
user=cluster.username,
|
||||
password=cluster.password,
|
||||
port=cluster.port,
|
||||
verify_ssl=cluster.verify_ssl
|
||||
)
|
||||
|
||||
vms = await proxmox_service.get_vms()
|
||||
return vms
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error getting VMs: {str(e)}")
|
||||
|
||||
@router.post("/clusters/{cluster_id}/vms/{vmid}/start")
|
||||
async def start_vm(cluster_id: int, vmid: str, node: str, vm_type: str = "qemu", db: Session = Depends(get_db)):
|
||||
cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first()
|
||||
if not cluster:
|
||||
raise HTTPException(status_code=404, detail="Cluster not found")
|
||||
|
||||
proxmox_service = ProxmoxService(
|
||||
host=cluster.host,
|
||||
user=cluster.username,
|
||||
password=cluster.password,
|
||||
port=cluster.port,
|
||||
verify_ssl=cluster.verify_ssl
|
||||
)
|
||||
|
||||
success = await proxmox_service.start_vm(node, vmid, vm_type)
|
||||
|
||||
# Get VM name from the cluster's VMs
|
||||
vms = await proxmox_service.get_vms()
|
||||
vm_name = next((vm.name for vm in vms if vm.vmid == vmid), f"VM-{vmid}")
|
||||
|
||||
# Log VM start action
|
||||
LoggingService.log_proxmox_vm_action(
|
||||
db=db,
|
||||
action="start",
|
||||
vmid=vmid,
|
||||
vm_name=vm_name,
|
||||
node=node,
|
||||
success=success,
|
||||
message=f"VM {vm_name} ({'started' if success else 'failed to start'}) on node {node}"
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="Failed to start VM")
|
||||
|
||||
return {"message": f"VM {vmid} start command sent", "success": True}
|
||||
|
||||
@router.post("/clusters/{cluster_id}/vms/{vmid}/stop")
|
||||
async def stop_vm(cluster_id: int, vmid: str, node: str, vm_type: str = "qemu", db: Session = Depends(get_db)):
|
||||
cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first()
|
||||
if not cluster:
|
||||
raise HTTPException(status_code=404, detail="Cluster not found")
|
||||
|
||||
proxmox_service = ProxmoxService(
|
||||
host=cluster.host,
|
||||
user=cluster.username,
|
||||
password=cluster.password,
|
||||
port=cluster.port,
|
||||
verify_ssl=cluster.verify_ssl
|
||||
)
|
||||
|
||||
success = await proxmox_service.stop_vm(node, vmid, vm_type)
|
||||
|
||||
# Get VM name from the cluster's VMs
|
||||
vms = await proxmox_service.get_vms()
|
||||
vm_name = next((vm.name for vm in vms if vm.vmid == vmid), f"VM-{vmid}")
|
||||
|
||||
# Log VM stop action
|
||||
LoggingService.log_proxmox_vm_action(
|
||||
db=db,
|
||||
action="stop",
|
||||
vmid=vmid,
|
||||
vm_name=vm_name,
|
||||
node=node,
|
||||
success=success,
|
||||
message=f"VM {vm_name} ({'stopped' if success else 'failed to stop'}) on node {node}"
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="Failed to stop VM")
|
||||
|
||||
return {"message": f"VM {vmid} stop command sent", "success": True}
|
||||
90
backend/app/api/servers.py
Normal file
90
backend/app/api/servers.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.database import get_db, Server
|
||||
from app.models.schemas import Server as ServerSchema, ServerCreate
|
||||
from app.services.wol_service import WolService
|
||||
from app.services.logging_service import LoggingService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=List[ServerSchema])
|
||||
async def get_servers(db: Session = Depends(get_db)):
|
||||
servers = db.query(Server).all()
|
||||
return servers
|
||||
|
||||
@router.post("/", response_model=ServerSchema)
|
||||
async def create_server(server: ServerCreate, db: Session = Depends(get_db)):
|
||||
db_server = Server(**server.dict())
|
||||
db.add(db_server)
|
||||
db.commit()
|
||||
db.refresh(db_server)
|
||||
|
||||
# Log server creation
|
||||
LoggingService.log_server_action(
|
||||
db=db,
|
||||
action="create",
|
||||
server_id=db_server.id,
|
||||
server_name=db_server.name,
|
||||
success=True,
|
||||
message=f"Server '{db_server.name}' created successfully"
|
||||
)
|
||||
|
||||
return db_server
|
||||
|
||||
@router.get("/{server_id}", response_model=ServerSchema)
|
||||
async def get_server(server_id: int, db: Session = Depends(get_db)):
|
||||
server = db.query(Server).filter(Server.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
return server
|
||||
|
||||
@router.put("/{server_id}", response_model=ServerSchema)
|
||||
async def update_server(server_id: int, server_update: ServerCreate, db: Session = Depends(get_db)):
|
||||
server = db.query(Server).filter(Server.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
old_name = server.name
|
||||
for key, value in server_update.dict().items():
|
||||
setattr(server, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(server)
|
||||
|
||||
# Log server update
|
||||
LoggingService.log_server_action(
|
||||
db=db,
|
||||
action="update",
|
||||
server_id=server.id,
|
||||
server_name=server.name,
|
||||
success=True,
|
||||
message=f"Server '{old_name}' updated successfully"
|
||||
)
|
||||
|
||||
return server
|
||||
|
||||
@router.delete("/{server_id}")
|
||||
async def delete_server(server_id: int, db: Session = Depends(get_db)):
|
||||
server = db.query(Server).filter(Server.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
# Log server deletion before deleting
|
||||
LoggingService.log_server_action(
|
||||
db=db,
|
||||
action="delete",
|
||||
server_id=server.id,
|
||||
server_name=server.name,
|
||||
success=True,
|
||||
message=f"Server '{server.name}' deleted successfully"
|
||||
)
|
||||
|
||||
db.delete(server)
|
||||
db.commit()
|
||||
return {"message": "Server deleted successfully"}
|
||||
|
||||
@router.post("/check-status")
|
||||
async def check_all_servers_status(db: Session = Depends(get_db)):
|
||||
await WolService.check_all_servers_status(db)
|
||||
return {"message": "Server status checked for all servers"}
|
||||
50
backend/app/api/wol.py
Normal file
50
backend/app/api/wol.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.database import get_db, WolLog, ActionLog
|
||||
from app.models.schemas import WolLog as WolLogSchema, ActionLog as ActionLogSchema
|
||||
from app.services.wol_service import WolService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/wake/{server_id}")
|
||||
async def wake_server(server_id: int, db: Session = Depends(get_db)):
|
||||
success = await WolService.wake_server(db, server_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Server not found or WOL failed")
|
||||
return {"message": f"WOL packet sent to server {server_id}", "success": True}
|
||||
|
||||
@router.post("/ping/{server_id}")
|
||||
async def ping_server(server_id: int, db: Session = Depends(get_db)):
|
||||
from app.database import Server
|
||||
server = db.query(Server).filter(Server.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
is_online = await WolService.ping_server(server.ip_address)
|
||||
server.is_online = is_online
|
||||
db.commit()
|
||||
|
||||
return {"server_id": server_id, "is_online": is_online}
|
||||
|
||||
@router.get("/logs", response_model=List[WolLogSchema])
|
||||
async def get_wol_logs(db: Session = Depends(get_db), limit: int = 50):
|
||||
logs = db.query(WolLog).order_by(WolLog.timestamp.desc()).limit(limit).all()
|
||||
return logs
|
||||
|
||||
@router.get("/logs/{server_id}", response_model=List[WolLogSchema])
|
||||
async def get_server_wol_logs(server_id: int, db: Session = Depends(get_db), limit: int = 20):
|
||||
logs = db.query(WolLog).filter(WolLog.server_id == server_id).order_by(WolLog.timestamp.desc()).limit(limit).all()
|
||||
return logs
|
||||
|
||||
@router.get("/all-logs", response_model=List[ActionLogSchema])
|
||||
async def get_all_action_logs(db: Session = Depends(get_db), limit: int = 100):
|
||||
"""Get all action logs (WOL, Proxmox, Server actions)"""
|
||||
logs = db.query(ActionLog).order_by(ActionLog.timestamp.desc()).limit(limit).all()
|
||||
return logs
|
||||
|
||||
@router.get("/all-logs/{action_type}", response_model=List[ActionLogSchema])
|
||||
async def get_action_logs_by_type(action_type: str, db: Session = Depends(get_db), limit: int = 50):
|
||||
"""Get action logs filtered by type (wol, proxmox, server)"""
|
||||
logs = db.query(ActionLog).filter(ActionLog.action_type == action_type).order_by(ActionLog.timestamp.desc()).limit(limit).all()
|
||||
return logs
|
||||
69
backend/app/database.py
Normal file
69
backend/app/database.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Boolean
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./zebra.db")
|
||||
|
||||
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
Base = declarative_base()
|
||||
|
||||
class Server(Base):
|
||||
__tablename__ = "servers"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, index=True, nullable=False)
|
||||
ip_address = Column(String, nullable=False)
|
||||
mac_address = Column(String, nullable=False)
|
||||
description = Column(String)
|
||||
is_online = Column(Boolean, default=False)
|
||||
last_ping = Column(DateTime)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class ProxmoxCluster(Base):
|
||||
__tablename__ = "proxmox_clusters"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, index=True, nullable=False)
|
||||
host = Column(String, nullable=False)
|
||||
username = Column(String, nullable=False)
|
||||
password = Column(String, nullable=False)
|
||||
port = Column(Integer, default=8006)
|
||||
verify_ssl = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class ActionLog(Base):
|
||||
__tablename__ = "action_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
action_type = Column(String, nullable=False) # 'wol', 'proxmox', 'server'
|
||||
target_id = Column(Integer, nullable=True) # server_id, cluster_id, vm_id, etc.
|
||||
target_name = Column(String, nullable=True) # server name, vm name, etc.
|
||||
action = Column(String, nullable=False) # 'wake', 'start', 'stop', 'create', 'delete', etc.
|
||||
timestamp = Column(DateTime, default=datetime.utcnow)
|
||||
success = Column(Boolean, default=True)
|
||||
message = Column(String)
|
||||
details = Column(String, nullable=True) # JSON string for additional data
|
||||
|
||||
# Keep WolLog for backward compatibility
|
||||
class WolLog(Base):
|
||||
__tablename__ = "wol_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
server_id = Column(Integer, nullable=False)
|
||||
action = Column(String, nullable=False)
|
||||
timestamp = Column(DateTime, default=datetime.utcnow)
|
||||
success = Column(Boolean, default=True)
|
||||
message = Column(String)
|
||||
|
||||
def init_db():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
44
backend/app/main.py
Normal file
44
backend/app/main.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from app.database import init_db
|
||||
from app.api import servers, proxmox, wol
|
||||
import traceback
|
||||
|
||||
app = FastAPI(title="Zebra Power", description="Wake-on-LAN and Proxmox Management API")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": f"Internal server error: {str(exc)}"},
|
||||
headers={
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "*",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
}
|
||||
)
|
||||
|
||||
app.include_router(servers.router, prefix="/api/servers", tags=["servers"])
|
||||
app.include_router(proxmox.router, prefix="/api/proxmox", tags=["proxmox"])
|
||||
app.include_router(wol.router, prefix="/api/wol", tags=["wol"])
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
init_db()
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Zebra Power API is running"}
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy"}
|
||||
1
backend/app/models/__init__.py
Normal file
1
backend/app/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Models package
|
||||
75
backend/app/models/schemas.py
Normal file
75
backend/app/models/schemas.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
class ServerBase(BaseModel):
|
||||
name: str
|
||||
ip_address: str
|
||||
mac_address: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class ServerCreate(ServerBase):
|
||||
pass
|
||||
|
||||
class Server(ServerBase):
|
||||
id: int
|
||||
is_online: bool
|
||||
last_ping: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ProxmoxClusterBase(BaseModel):
|
||||
name: str
|
||||
host: str
|
||||
username: str
|
||||
password: str
|
||||
port: int = 8006
|
||||
verify_ssl: bool = True
|
||||
|
||||
class ProxmoxClusterCreate(ProxmoxClusterBase):
|
||||
pass
|
||||
|
||||
class ProxmoxCluster(ProxmoxClusterBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ProxmoxVM(BaseModel):
|
||||
vmid: str
|
||||
name: str
|
||||
status: str
|
||||
node: str
|
||||
type: str
|
||||
|
||||
class WolLogCreate(BaseModel):
|
||||
server_id: int
|
||||
action: str
|
||||
success: bool = True
|
||||
message: Optional[str] = None
|
||||
|
||||
class WolLog(WolLogCreate):
|
||||
id: int
|
||||
timestamp: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ActionLogCreate(BaseModel):
|
||||
action_type: str
|
||||
target_id: Optional[int] = None
|
||||
target_name: Optional[str] = None
|
||||
action: str
|
||||
success: bool = True
|
||||
message: Optional[str] = None
|
||||
details: Optional[str] = None
|
||||
|
||||
class ActionLog(ActionLogCreate):
|
||||
id: int
|
||||
timestamp: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
1
backend/app/services/__init__.py
Normal file
1
backend/app/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Services package
|
||||
82
backend/app/services/logging_service.py
Normal file
82
backend/app/services/logging_service.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import ActionLog
|
||||
from typing import Optional
|
||||
import json
|
||||
|
||||
class LoggingService:
|
||||
@staticmethod
|
||||
def log_action(
|
||||
db: Session,
|
||||
action_type: str,
|
||||
action: str,
|
||||
success: bool = True,
|
||||
message: Optional[str] = None,
|
||||
target_id: Optional[int] = None,
|
||||
target_name: Optional[str] = None,
|
||||
details: Optional[dict] = None
|
||||
):
|
||||
"""Log an action to the unified action log"""
|
||||
log_entry = ActionLog(
|
||||
action_type=action_type,
|
||||
target_id=target_id,
|
||||
target_name=target_name,
|
||||
action=action,
|
||||
success=success,
|
||||
message=message,
|
||||
details=json.dumps(details) if details else None
|
||||
)
|
||||
db.add(log_entry)
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def log_wol_action(db: Session, server_id: int, server_name: str, success: bool, message: str):
|
||||
"""Log WOL action"""
|
||||
LoggingService.log_action(
|
||||
db=db,
|
||||
action_type="wol",
|
||||
action="wake",
|
||||
success=success,
|
||||
message=message,
|
||||
target_id=server_id,
|
||||
target_name=server_name
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_proxmox_vm_action(db: Session, action: str, vmid: str, vm_name: str, node: str, success: bool, message: str):
|
||||
"""Log Proxmox VM actions (start/stop)"""
|
||||
LoggingService.log_action(
|
||||
db=db,
|
||||
action_type="proxmox",
|
||||
action=action,
|
||||
success=success,
|
||||
message=message,
|
||||
target_id=int(vmid) if vmid.isdigit() else None,
|
||||
target_name=vm_name,
|
||||
details={"node": node, "vmid": vmid}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_proxmox_cluster_action(db: Session, action: str, cluster_id: int, cluster_name: str, success: bool, message: str):
|
||||
"""Log Proxmox cluster actions (create/delete)"""
|
||||
LoggingService.log_action(
|
||||
db=db,
|
||||
action_type="proxmox",
|
||||
action=action,
|
||||
success=success,
|
||||
message=message,
|
||||
target_id=cluster_id,
|
||||
target_name=cluster_name
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_server_action(db: Session, action: str, server_id: int, server_name: str, success: bool, message: str):
|
||||
"""Log server actions (create/update/delete)"""
|
||||
LoggingService.log_action(
|
||||
db=db,
|
||||
action_type="server",
|
||||
action=action,
|
||||
success=success,
|
||||
message=message,
|
||||
target_id=server_id,
|
||||
target_name=server_name
|
||||
)
|
||||
114
backend/app/services/proxmox_service.py
Normal file
114
backend/app/services/proxmox_service.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from proxmoxer import ProxmoxAPI
|
||||
from typing import List, Optional
|
||||
from app.models.schemas import ProxmoxVM
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ProxmoxService:
|
||||
def __init__(self, host: str, user: str, password: str, port: int = 8006, verify_ssl: bool = True):
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.port = port
|
||||
self.verify_ssl = verify_ssl
|
||||
self._proxmox = None
|
||||
|
||||
def _get_connection(self):
|
||||
if not self._proxmox:
|
||||
try:
|
||||
self._proxmox = ProxmoxAPI(
|
||||
self.host,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
port=self.port,
|
||||
verify_ssl=self.verify_ssl,
|
||||
timeout=10
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Proxmox {self.host}: {str(e)}")
|
||||
raise ConnectionError(f"Cannot connect to Proxmox: {str(e)}")
|
||||
return self._proxmox
|
||||
|
||||
async def test_connection(self) -> bool:
|
||||
try:
|
||||
proxmox = self._get_connection()
|
||||
proxmox.version.get()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Proxmox connection test failed: {str(e)}")
|
||||
return False
|
||||
|
||||
async def get_nodes(self) -> List[dict]:
|
||||
try:
|
||||
proxmox = self._get_connection()
|
||||
return proxmox.nodes.get()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get nodes: {str(e)}")
|
||||
return []
|
||||
|
||||
async def get_vms(self, node: Optional[str] = None) -> List[ProxmoxVM]:
|
||||
try:
|
||||
proxmox = self._get_connection()
|
||||
vms = []
|
||||
|
||||
if node:
|
||||
nodes = [node]
|
||||
else:
|
||||
nodes_list = await self.get_nodes()
|
||||
nodes = [n['node'] for n in nodes_list]
|
||||
|
||||
for node_name in nodes:
|
||||
try:
|
||||
qemu_vms = proxmox.nodes(node_name).qemu.get()
|
||||
for vm in qemu_vms:
|
||||
if vm.get('template', 0) != 1:
|
||||
vms.append(ProxmoxVM(
|
||||
vmid=str(vm['vmid']),
|
||||
name=vm.get('name', f"VM-{vm['vmid']}"),
|
||||
status=vm.get('status', 'unknown'),
|
||||
node=node_name,
|
||||
type='qemu'
|
||||
))
|
||||
|
||||
lxc_containers = proxmox.nodes(node_name).lxc.get()
|
||||
for container in lxc_containers:
|
||||
vms.append(ProxmoxVM(
|
||||
vmid=str(container['vmid']),
|
||||
name=container.get('name', f"CT-{container['vmid']}"),
|
||||
status=container.get('status', 'unknown'),
|
||||
node=node_name,
|
||||
type='lxc'
|
||||
))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get VMs from node {node_name}: {str(e)}")
|
||||
continue
|
||||
|
||||
return vms
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get VMs: {str(e)}")
|
||||
return []
|
||||
|
||||
async def start_vm(self, node: str, vmid: str, vm_type: str = 'qemu') -> bool:
|
||||
try:
|
||||
proxmox = self._get_connection()
|
||||
if vm_type == 'lxc':
|
||||
proxmox.nodes(node).lxc(vmid).status.start.post()
|
||||
else:
|
||||
proxmox.nodes(node).qemu(vmid).status.start.post()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start VM {vmid}: {str(e)}")
|
||||
return False
|
||||
|
||||
async def stop_vm(self, node: str, vmid: str, vm_type: str = 'qemu') -> bool:
|
||||
try:
|
||||
proxmox = self._get_connection()
|
||||
if vm_type == 'lxc':
|
||||
proxmox.nodes(node).lxc(vmid).status.shutdown.post()
|
||||
else:
|
||||
proxmox.nodes(node).qemu(vmid).status.shutdown.post()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to stop VM {vmid}: {str(e)}")
|
||||
return False
|
||||
89
backend/app/services/wol_service.py
Normal file
89
backend/app/services/wol_service.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import asyncio
|
||||
import subprocess
|
||||
from typing import List
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import Server, WolLog
|
||||
from app.models.schemas import WolLogCreate
|
||||
from app.services.logging_service import LoggingService
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WolService:
|
||||
@staticmethod
|
||||
async def send_wol_packet(mac_address: str) -> bool:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["wakeonlan", mac_address],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
return result.returncode == 0
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error(f"WOL timeout for MAC: {mac_address}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"WOL error for MAC {mac_address}: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def ping_server(ip_address: str) -> bool:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ping", "-c", "1", "-W", "3", ip_address],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
return result.returncode == 0
|
||||
except subprocess.TimeoutExpired:
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Ping error for IP {ip_address}: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def wake_server(db: Session, server_id: int) -> bool:
|
||||
server = db.query(Server).filter(Server.id == server_id).first()
|
||||
if not server:
|
||||
return False
|
||||
|
||||
success = await WolService.send_wol_packet(server.mac_address)
|
||||
|
||||
# Log to both WolLog (backward compatibility) and ActionLog (new unified system)
|
||||
log_entry = WolLog(
|
||||
server_id=server_id,
|
||||
action="wake",
|
||||
success=success,
|
||||
message=f"WOL packet sent to {server.mac_address}" if success else "Failed to send WOL packet"
|
||||
)
|
||||
db.add(log_entry)
|
||||
|
||||
# Log to unified action log
|
||||
LoggingService.log_wol_action(
|
||||
db=db,
|
||||
server_id=server_id,
|
||||
server_name=server.name,
|
||||
success=success,
|
||||
message=f"WOL packet {'sent successfully' if success else 'failed'} to {server.name} ({server.mac_address})"
|
||||
)
|
||||
|
||||
return success
|
||||
|
||||
@staticmethod
|
||||
async def check_all_servers_status(db: Session) -> None:
|
||||
servers = db.query(Server).all()
|
||||
|
||||
tasks = []
|
||||
for server in servers:
|
||||
tasks.append(WolService.ping_server(server.ip_address))
|
||||
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
from datetime import datetime
|
||||
for server, is_online in zip(servers, results):
|
||||
server.is_online = is_online
|
||||
server.last_ping = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
9
backend/requirements.txt
Normal file
9
backend/requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
sqlalchemy==2.0.23
|
||||
pydantic==2.5.0
|
||||
python-multipart==0.0.6
|
||||
proxmoxer==2.0.1
|
||||
requests==2.31.0
|
||||
wakeonlan==3.1.0
|
||||
httpx==0.25.2
|
||||
BIN
data/zebra.db
Normal file
BIN
data/zebra.db
Normal file
Binary file not shown.
46
docker-compose.yml
Normal file
46
docker-compose.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
# Docker Compose for Zebra Power
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: zebra_backend
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
environment:
|
||||
- DATABASE_URL=sqlite:///data/zebra.db
|
||||
- CORS_ORIGINS=http://localhost:3000,http://localhost
|
||||
restart: unless-stopped
|
||||
# Required for WOL packets
|
||||
network_mode: host
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
container_name: zebra_frontend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- VITE_API_URL=http://localhost:8000/api
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
build:
|
||||
context: ./nginx
|
||||
dockerfile: Dockerfile
|
||||
container_name: zebra_nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
data:
|
||||
12
frontend/Dockerfile
Normal file
12
frontend/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/tomato-favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Zebra Power - WOL & Proxmox Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
25
frontend/package.json
Normal file
25
frontend/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "zebra-power-frontend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.5",
|
||||
"pinia": "^2.1.7",
|
||||
"axios": "^1.6.2",
|
||||
"@headlessui/vue": "^1.7.16",
|
||||
"@heroicons/vue": "^2.0.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"vite": "^5.0.8",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.3.6"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
1
frontend/public/favicon.ico
Normal file
1
frontend/public/favicon.ico
Normal file
@@ -0,0 +1 @@
|
||||
🍅
|
||||
43
frontend/public/tomato-favicon.svg
Normal file
43
frontend/public/tomato-favicon.svg
Normal file
@@ -0,0 +1,43 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Tomate verte zebra optimisée pour favicon -->
|
||||
<defs>
|
||||
<radialGradient id="tomatoGradient" cx="0.3" cy="0.3" r="0.8">
|
||||
<stop offset="0%" stop-color="#86efac"/>
|
||||
<stop offset="40%" stop-color="#4ade80"/>
|
||||
<stop offset="70%" stop-color="#22c55e"/>
|
||||
<stop offset="100%" stop-color="#16a34a"/>
|
||||
</radialGradient>
|
||||
<pattern id="stripes" patternUnits="userSpaceOnUse" width="4" height="4" patternTransform="rotate(15)">
|
||||
<rect width="4" height="2" fill="#4ade80"/>
|
||||
<rect y="2" width="4" height="2" fill="#22c55e"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<!-- Ombre -->
|
||||
<ellipse cx="33" cy="42" rx="18" ry="15" fill="#000000" opacity="0.15"/>
|
||||
|
||||
<!-- Corps principal de la tomate -->
|
||||
<ellipse cx="32" cy="38" rx="20" ry="16" fill="url(#tomatoGradient)"/>
|
||||
|
||||
<!-- Rayures zebra -->
|
||||
<ellipse cx="32" cy="38" rx="20" ry="16" fill="url(#stripes)" opacity="0.4"/>
|
||||
|
||||
<!-- Highlight principal -->
|
||||
<ellipse cx="26" cy="32" rx="8" ry="6" fill="#86efac" opacity="0.7"/>
|
||||
|
||||
<!-- Petit highlight -->
|
||||
<ellipse cx="24" cy="30" rx="3" ry="2" fill="#bbf7d0" opacity="0.8"/>
|
||||
|
||||
<!-- Tige -->
|
||||
<rect x="30" y="20" width="4" height="8" fill="#16a34a" rx="2"/>
|
||||
<rect x="29" y="19" width="6" height="3" fill="#15803d" rx="1"/>
|
||||
|
||||
<!-- Feuilles stylisées -->
|
||||
<path d="M20 24 Q15 18 12 22 Q16 20 22 24 Q20 22 20 24" fill="#22c55e"/>
|
||||
<path d="M44 24 Q49 18 52 22 Q48 20 42 24 Q44 22 44 24" fill="#22c55e"/>
|
||||
<path d="M32 20 Q28 14 24 18 Q28 16 32 20" fill="#15803d"/>
|
||||
<path d="M32 20 Q36 14 40 18 Q36 16 32 20" fill="#15803d"/>
|
||||
|
||||
<!-- Contour subtil -->
|
||||
<ellipse cx="32" cy="38" rx="20" ry="16" fill="none" stroke="#16a34a" stroke-width="1" opacity="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
78
frontend/public/tomato-large.svg
Normal file
78
frontend/public/tomato-large.svg
Normal file
@@ -0,0 +1,78 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Grande image de tomate verte zebra -->
|
||||
<defs>
|
||||
<radialGradient id="mainGradient" cx="0.35" cy="0.25" r="0.9">
|
||||
<stop offset="0%" stop-color="#bbf7d0"/>
|
||||
<stop offset="20%" stop-color="#86efac"/>
|
||||
<stop offset="50%" stop-color="#4ade80"/>
|
||||
<stop offset="75%" stop-color="#22c55e"/>
|
||||
<stop offset="100%" stop-color="#16a34a"/>
|
||||
</radialGradient>
|
||||
<pattern id="zebraStripes" patternUnits="userSpaceOnUse" width="8" height="8" patternTransform="rotate(20)">
|
||||
<rect width="8" height="4" fill="#4ade80"/>
|
||||
<rect y="4" width="8" height="4" fill="#16a34a"/>
|
||||
</pattern>
|
||||
<radialGradient id="leafGradient" cx="0.3" cy="0.3" r="0.8">
|
||||
<stop offset="0%" stop-color="#4ade80"/>
|
||||
<stop offset="100%" stop-color="#15803d"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Ombre portée -->
|
||||
<ellipse cx="135" cy="180" rx="80" ry="60" fill="#000000" opacity="0.1"/>
|
||||
|
||||
<!-- Corps principal de la tomate -->
|
||||
<ellipse cx="128" cy="160" rx="90" ry="70" fill="url(#mainGradient)"/>
|
||||
|
||||
<!-- Rayures zebra -->
|
||||
<ellipse cx="128" cy="160" rx="90" ry="70" fill="url(#zebraStripes)" opacity="0.3"/>
|
||||
|
||||
<!-- Highlights pour le volume -->
|
||||
<ellipse cx="105" cy="130" rx="35" ry="25" fill="#86efac" opacity="0.6"/>
|
||||
<ellipse cx="95" cy="120" rx="18" ry="12" fill="#bbf7d0" opacity="0.8"/>
|
||||
<ellipse cx="88" cy="115" rx="8" ry="5" fill="#dcfce7"/>
|
||||
|
||||
<!-- Tige principale -->
|
||||
<rect x="120" y="70" width="16" height="35" fill="#16a34a" rx="8"/>
|
||||
<rect x="118" y="65" width="20" height="12" fill="#15803d" rx="4"/>
|
||||
|
||||
<!-- Sépales à la base de la tige -->
|
||||
<path d="M128 90 L118 95 Q110 98 108 105 Q115 100 128 95 Q141 100 148 105 Q146 98 138 95 Z" fill="#15803d"/>
|
||||
|
||||
<!-- Feuilles complexes -->
|
||||
<g transform="translate(128,80)">
|
||||
<!-- Feuille gauche -->
|
||||
<path d="M-25 -10 Q-45 -25 -55 -15 Q-50 -30 -35 -35 Q-30 -25 -25 -20 Q-20 -15 -25 -10"
|
||||
fill="url(#leafGradient)"/>
|
||||
<path d="M-30 -20 Q-40 -15 -35 -10" stroke="#15803d" stroke-width="2" fill="none"/>
|
||||
|
||||
<!-- Feuille droite -->
|
||||
<path d="M25 -10 Q45 -25 55 -15 Q50 -30 35 -35 Q30 -25 25 -20 Q20 -15 25 -10"
|
||||
fill="url(#leafGradient)"/>
|
||||
<path d="M30 -20 Q40 -15 35 -10" stroke="#15803d" stroke-width="2" fill="none"/>
|
||||
|
||||
<!-- Feuille arrière gauche -->
|
||||
<path d="M-15 -25 Q-35 -40 -45 -30 Q-40 -45 -25 -50 Q-20 -40 -15 -35 Q-10 -30 -15 -25"
|
||||
fill="url(#leafGradient)" opacity="0.8"/>
|
||||
|
||||
<!-- Feuille arrière droite -->
|
||||
<path d="M15 -25 Q35 -40 45 -30 Q40 -45 25 -50 Q20 -40 15 -35 Q10 -30 15 -25"
|
||||
fill="url(#leafGradient)" opacity="0.8"/>
|
||||
</g>
|
||||
|
||||
<!-- Nervures des feuilles principales -->
|
||||
<path d="M103 70 Q88 55 73 70" stroke="#15803d" stroke-width="1.5" fill="none" opacity="0.7"/>
|
||||
<path d="M153 70 Q168 55 183 70" stroke="#15803d" stroke-width="1.5" fill="none" opacity="0.7"/>
|
||||
|
||||
<!-- Détails de texture sur la tomate -->
|
||||
<path d="M70 140 Q80 135 90 140 Q85 145 70 140" fill="#16a34a" opacity="0.2"/>
|
||||
<path d="M166 140 Q176 135 186 140 Q181 145 166 140" fill="#16a34a" opacity="0.2"/>
|
||||
<path d="M128 210 Q138 205 148 210 Q143 215 128 210" fill="#16a34a" opacity="0.2"/>
|
||||
|
||||
<!-- Contour subtil -->
|
||||
<ellipse cx="128" cy="160" rx="90" ry="70" fill="none" stroke="#16a34a" stroke-width="2" opacity="0.3"/>
|
||||
|
||||
<!-- Reflets finaux -->
|
||||
<ellipse cx="100" cy="125" rx="12" ry="8" fill="#f0fdf4" opacity="0.5"/>
|
||||
<ellipse cx="98" cy="123" rx="4" ry="2" fill="#ffffff" opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
28
frontend/public/tomato.svg
Normal file
28
frontend/public/tomato.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Tomate verte zebra avec rayures -->
|
||||
<defs>
|
||||
<pattern id="zebraPattern" patternUnits="userSpaceOnUse" width="2" height="2">
|
||||
<rect width="2" height="1" fill="#4ade80"/>
|
||||
<rect y="1" width="2" height="1" fill="#22c55e"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<!-- Corps de la tomate -->
|
||||
<ellipse cx="16" cy="18" rx="12" ry="10" fill="url(#zebraPattern)" stroke="#16a34a" stroke-width="0.5"/>
|
||||
|
||||
<!-- Highlight pour donner du volume -->
|
||||
<ellipse cx="13" cy="15" rx="4" ry="3" fill="#86efac" opacity="0.6"/>
|
||||
|
||||
<!-- Tige et feuilles -->
|
||||
<rect x="15" y="8" width="2" height="4" fill="#16a34a" rx="1"/>
|
||||
|
||||
<!-- Feuilles -->
|
||||
<path d="M12 10 Q10 8 8 10 Q10 9 12 10" fill="#22c55e"/>
|
||||
<path d="M20 10 Q22 8 24 10 Q22 9 20 10" fill="#22c55e"/>
|
||||
<path d="M16 8 Q14 6 12 8 Q14 7 16 8" fill="#15803d"/>
|
||||
<path d="M16 8 Q18 6 20 8 Q18 7 16 8" fill="#15803d"/>
|
||||
|
||||
<!-- Détails des rayures zebra -->
|
||||
<path d="M6 18 Q8 16 10 18 Q8 20 6 18" fill="#16a34a" opacity="0.3"/>
|
||||
<path d="M22 18 Q24 16 26 18 Q24 20 22 18" fill="#16a34a" opacity="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
46
frontend/src/App.vue
Normal file
46
frontend/src/App.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
<nav class="bg-white shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center space-x-2">
|
||||
<img src="/tomato.svg" alt="Zebra Power" class="w-8 h-8" />
|
||||
<h1 class="text-xl font-bold text-gray-900">Zebra Power</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-8">
|
||||
<router-link
|
||||
to="/"
|
||||
class="inline-flex items-center px-1 pt-1 text-sm font-medium"
|
||||
:class="$route.name === 'Dashboard' ? 'border-b-2 border-blue-500 text-gray-900' : 'text-gray-500 hover:text-gray-700'"
|
||||
>
|
||||
Dashboard
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/servers"
|
||||
class="inline-flex items-center px-1 pt-1 text-sm font-medium"
|
||||
:class="$route.name === 'Servers' ? 'border-b-2 border-blue-500 text-gray-900' : 'text-gray-500 hover:text-gray-700'"
|
||||
>
|
||||
Serveurs
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/proxmox"
|
||||
class="inline-flex items-center px-1 pt-1 text-sm font-medium"
|
||||
:class="$route.name === 'Proxmox' ? 'border-b-2 border-blue-500 text-gray-900' : 'text-gray-500 hover:text-gray-700'"
|
||||
>
|
||||
Proxmox
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
27
frontend/src/main.js
Normal file
27
frontend/src/main.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
import Dashboard from './views/Dashboard.vue'
|
||||
import Servers from './views/Servers.vue'
|
||||
import Proxmox from './views/Proxmox.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', name: 'Dashboard', component: Dashboard },
|
||||
{ path: '/servers', name: 'Servers', component: Servers },
|
||||
{ path: '/proxmox', name: 'Proxmox', component: Proxmox }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
createApp(App)
|
||||
.use(pinia)
|
||||
.use(router)
|
||||
.mount('#app')
|
||||
46
frontend/src/services/api.js
Normal file
46
frontend/src/services/api.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: apiUrl,
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.error('API Error:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export const serversApi = {
|
||||
getAll: () => api.get('/servers'),
|
||||
create: (server) => api.post('/servers', server),
|
||||
update: (id, server) => api.put(`/servers/${id}`, server),
|
||||
delete: (id) => api.delete(`/servers/${id}`),
|
||||
checkStatus: () => api.post('/servers/check-status'),
|
||||
}
|
||||
|
||||
export const wolApi = {
|
||||
wake: (serverId) => api.post(`/wol/wake/${serverId}`),
|
||||
ping: (serverId) => api.post(`/wol/ping/${serverId}`),
|
||||
getLogs: (limit = 50) => api.get(`/wol/logs?limit=${limit}`),
|
||||
getServerLogs: (serverId, limit = 20) => api.get(`/wol/logs/${serverId}?limit=${limit}`),
|
||||
getAllLogs: (limit = 100) => api.get(`/wol/all-logs?limit=${limit}`),
|
||||
getLogsByType: (type, limit = 50) => api.get(`/wol/all-logs/${type}?limit=${limit}`),
|
||||
}
|
||||
|
||||
export const proxmoxApi = {
|
||||
getClusters: () => api.get('/proxmox/clusters'),
|
||||
createCluster: (cluster) => api.post('/proxmox/clusters', cluster),
|
||||
deleteCluster: (id) => api.delete(`/proxmox/clusters/${id}`),
|
||||
getVMs: (clusterId) => api.get(`/proxmox/clusters/${clusterId}/vms`),
|
||||
startVM: (clusterId, vmid, node, vmType = 'qemu') =>
|
||||
api.post(`/proxmox/clusters/${clusterId}/vms/${vmid}/start?node=${node}&vm_type=${vmType}`),
|
||||
stopVM: (clusterId, vmid, node, vmType = 'qemu') =>
|
||||
api.post(`/proxmox/clusters/${clusterId}/vms/${vmid}/stop?node=${node}&vm_type=${vmType}`),
|
||||
}
|
||||
|
||||
export default api
|
||||
3
frontend/src/style.css
Normal file
3
frontend/src/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
274
frontend/src/views/Dashboard.vue
Normal file
274
frontend/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="px-4 py-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
|
||||
<span class="text-white font-medium">S</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Serveurs</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ servers.length }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
|
||||
<span class="text-white font-medium">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">En ligne</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ onlineServers }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
|
||||
<span class="text-white font-medium">P</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Clusters Proxmox</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ clusters.length }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Serveurs</h3>
|
||||
<div class="space-y-3">
|
||||
<div v-for="server in servers.slice(0, 5)" :key="server.id" class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
:class="server.is_online ? 'bg-green-400' : 'bg-red-400'"
|
||||
></div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-gray-900">{{ server.name }}</p>
|
||||
<p class="text-sm text-gray-500">{{ server.ip_address }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="wakeServer(server.id)"
|
||||
:disabled="loading"
|
||||
class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
Wake
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="servers.length === 0" class="text-gray-500 text-sm">
|
||||
Aucun serveur configuré
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">VMs/Containers</h3>
|
||||
<div class="space-y-3">
|
||||
<div v-for="vm in allVMs.slice(0, 5)" :key="`${vm.clusterId}-${vm.node}-${vm.vmid}`" class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
:class="vm.status === 'running' ? 'bg-green-400' : 'bg-red-400'"
|
||||
></div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-gray-900">{{ vm.name }}</p>
|
||||
<p class="text-sm text-gray-500">{{ vm.type.toUpperCase() }} #{{ vm.vmid }} - {{ vm.node }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-1">
|
||||
<button
|
||||
v-if="vm.status !== 'running'"
|
||||
@click="startVM(vm.clusterId, vm.vmid, vm.node, vm.type)"
|
||||
:disabled="loading"
|
||||
class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
<button
|
||||
v-if="vm.status === 'running'"
|
||||
@click="stopVM(vm.clusterId, vm.vmid, vm.node, vm.type)"
|
||||
:disabled="loading"
|
||||
class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="allVMs.length === 0" class="text-gray-500 text-sm">
|
||||
Aucune VM/Container disponible
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Logs récents</h3>
|
||||
<div class="space-y-3">
|
||||
<div v-for="log in logs.slice(0, 5)" :key="log.id" class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
:class="log.success ? 'bg-green-400' : 'bg-red-400'"
|
||||
></div>
|
||||
</div>
|
||||
<div class="ml-3 min-w-0 flex-1">
|
||||
<p class="text-sm text-gray-900">{{ getLogDisplayText(log) }}</p>
|
||||
<p class="text-sm text-gray-500">{{ formatDate(log.timestamp) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="logs.length === 0" class="text-gray-500 text-sm">
|
||||
Aucun log disponible
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
|
||||
import { serversApi, proxmoxApi, wolApi } from '../services/api'
|
||||
|
||||
const servers = ref([])
|
||||
const clusters = ref([])
|
||||
const logs = ref([])
|
||||
const allVMs = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
const onlineServers = computed(() =>
|
||||
servers.value.filter(server => server.is_online).length
|
||||
)
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString('fr-FR')
|
||||
}
|
||||
|
||||
const getLogDisplayText = (log) => {
|
||||
if (log.target_name) {
|
||||
return `${log.action} - ${log.target_name} (${log.action_type})`
|
||||
}
|
||||
// Fallback pour les anciens logs WOL
|
||||
return `${log.action} - Serveur #${log.server_id || log.target_id}`
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
// D'abord vérifier le statut des serveurs
|
||||
await serversApi.checkStatus()
|
||||
|
||||
const [serversResponse, clustersResponse, logsResponse] = await Promise.all([
|
||||
serversApi.getAll(),
|
||||
proxmoxApi.getClusters(),
|
||||
wolApi.getAllLogs(10)
|
||||
])
|
||||
|
||||
servers.value = serversResponse.data
|
||||
clusters.value = clustersResponse.data
|
||||
logs.value = logsResponse.data
|
||||
|
||||
// Charger les VMs de tous les clusters
|
||||
const vms = []
|
||||
for (const cluster of clustersResponse.data) {
|
||||
try {
|
||||
const vmsResponse = await proxmoxApi.getVMs(cluster.id)
|
||||
for (const vm of vmsResponse.data) {
|
||||
vms.push({
|
||||
...vm,
|
||||
clusterId: cluster.id
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors du chargement des VMs du cluster ${cluster.id}:`, error)
|
||||
}
|
||||
}
|
||||
allVMs.value = vms
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des données:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const wakeServer = async (serverId) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await wolApi.wake(serverId)
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du réveil du serveur:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startVM = async (clusterId, vmid, node, vmType) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await proxmoxApi.startVM(clusterId, vmid, node, vmType)
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du démarrage de la VM:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const stopVM = async (clusterId, vmid, node, vmType) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await proxmoxApi.stopVM(clusterId, vmid, node, vmType)
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'arrêt de la VM:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
|
||||
// Mise à jour automatique toutes les 30 secondes
|
||||
const interval = setInterval(() => {
|
||||
loadData()
|
||||
}, 30000)
|
||||
|
||||
// Nettoyer l'interval quand le composant est démonté
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(interval)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
305
frontend/src/views/Proxmox.vue
Normal file
305
frontend/src/views/Proxmox.vue
Normal file
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<div class="px-4 py-6">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Clusters Proxmox</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">
|
||||
Gérez vos clusters Proxmox et contrôlez vos VMs/Containers
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<button
|
||||
@click="showAddModal = true"
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:w-auto"
|
||||
>
|
||||
Ajouter un cluster
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des clusters -->
|
||||
<div class="mt-8 space-y-6">
|
||||
<div v-for="cluster in clusters" :key="cluster.id" class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">{{ cluster.name }}</h3>
|
||||
<p class="text-sm text-gray-500">{{ cluster.host }}:{{ cluster.port }}</p>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
@click="loadVMs(cluster.id)"
|
||||
:disabled="loading"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
Actualiser
|
||||
</button>
|
||||
<button
|
||||
@click="deleteCluster(cluster.id)"
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des VMs -->
|
||||
<div v-if="clusterVMs[cluster.id]" class="mt-4">
|
||||
<h4 class="text-md font-medium text-gray-900 mb-3">VMs et Containers</h4>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
v-for="vm in clusterVMs[cluster.id]"
|
||||
:key="`${vm.node}-${vm.vmid}`"
|
||||
class="border rounded-lg p-4"
|
||||
:class="vm.status === 'running' ? 'border-green-200 bg-green-50' : 'border-gray-200 bg-gray-50'"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h5 class="text-sm font-medium text-gray-900">{{ vm.name }}</h5>
|
||||
<p class="text-xs text-gray-500">{{ vm.type.toUpperCase() }} #{{ vm.vmid }}</p>
|
||||
<p class="text-xs text-gray-500">Node: {{ vm.node }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class="inline-flex px-2 py-1 text-xs font-medium rounded-full"
|
||||
:class="vm.status === 'running' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
|
||||
>
|
||||
{{ vm.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex space-x-2">
|
||||
<button
|
||||
v-if="vm.status !== 'running'"
|
||||
@click="startVM(cluster.id, vm.vmid, vm.node, vm.type)"
|
||||
:disabled="loading"
|
||||
class="flex-1 inline-flex justify-center items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-white bg-green-600 hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
Démarrer
|
||||
</button>
|
||||
<button
|
||||
v-if="vm.status === 'running'"
|
||||
@click="stopVM(cluster.id, vm.vmid, vm.node, vm.type)"
|
||||
:disabled="loading"
|
||||
class="flex-1 inline-flex justify-center items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-white bg-red-600 hover:bg-red-700 disabled:opacity-50"
|
||||
>
|
||||
Arrêter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="mt-4 text-center text-gray-500">
|
||||
<button
|
||||
@click="loadVMs(cluster.id)"
|
||||
:disabled="loading"
|
||||
class="text-blue-600 hover:text-blue-500 disabled:opacity-50"
|
||||
>
|
||||
Charger les VMs/Containers
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="clusters.length === 0" class="text-center text-gray-500 py-8">
|
||||
Aucun cluster Proxmox configuré
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal d'ajout de cluster -->
|
||||
<div v-if="showAddModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Ajouter un cluster Proxmox</h3>
|
||||
<form @submit.prevent="saveCluster">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Nom du cluster</label>
|
||||
<input
|
||||
v-model="clusterForm.name"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Host</label>
|
||||
<input
|
||||
v-model="clusterForm.host"
|
||||
type="text"
|
||||
placeholder="192.168.1.100"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Nom d'utilisateur</label>
|
||||
<input
|
||||
v-model="clusterForm.username"
|
||||
type="text"
|
||||
placeholder="root@pam"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Mot de passe</label>
|
||||
<input
|
||||
v-model="clusterForm.password"
|
||||
type="password"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Port</label>
|
||||
<input
|
||||
v-model="clusterForm.port"
|
||||
type="number"
|
||||
value="8006"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
v-model="clusterForm.verify_ssl"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
>
|
||||
<span class="ml-2 text-sm text-gray-700">Vérifier SSL</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { proxmoxApi } from '../services/api'
|
||||
|
||||
const clusters = ref([])
|
||||
const clusterVMs = ref({})
|
||||
const loading = ref(false)
|
||||
const showAddModal = ref(false)
|
||||
|
||||
const clusterForm = reactive({
|
||||
name: '',
|
||||
host: '',
|
||||
username: '',
|
||||
password: '',
|
||||
port: 8006,
|
||||
verify_ssl: true
|
||||
})
|
||||
|
||||
const loadClusters = async () => {
|
||||
try {
|
||||
const response = await proxmoxApi.getClusters()
|
||||
clusters.value = response.data
|
||||
|
||||
// Auto-charger les VMs pour chaque cluster
|
||||
for (const cluster of response.data) {
|
||||
await loadVMs(cluster.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des clusters:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const saveCluster = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await proxmoxApi.createCluster(clusterForm)
|
||||
await loadClusters()
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde:', error)
|
||||
alert('Erreur lors de la connexion au cluster Proxmox. Vérifiez les paramètres.')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteCluster = async (clusterId) => {
|
||||
if (confirm('Êtes-vous sûr de vouloir supprimer ce cluster ?')) {
|
||||
try {
|
||||
await proxmoxApi.deleteCluster(clusterId)
|
||||
delete clusterVMs.value[clusterId]
|
||||
await loadClusters()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadVMs = async (clusterId) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await proxmoxApi.getVMs(clusterId)
|
||||
clusterVMs.value[clusterId] = response.data
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des VMs:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startVM = async (clusterId, vmid, node, vmType) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await proxmoxApi.startVM(clusterId, vmid, node, vmType)
|
||||
await loadVMs(clusterId)
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du démarrage de la VM:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const stopVM = async (clusterId, vmid, node, vmType) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await proxmoxApi.stopVM(clusterId, vmid, node, vmType)
|
||||
await loadVMs(clusterId)
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'arrêt de la VM:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
showAddModal.value = false
|
||||
Object.assign(clusterForm, {
|
||||
name: '',
|
||||
host: '',
|
||||
username: '',
|
||||
password: '',
|
||||
port: 8006,
|
||||
verify_ssl: true
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadClusters()
|
||||
})
|
||||
</script>
|
||||
261
frontend/src/views/Servers.vue
Normal file
261
frontend/src/views/Servers.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<div class="px-4 py-6">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Serveurs</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">
|
||||
Gérez vos serveurs et envoyez des paquets Wake-on-LAN
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<button
|
||||
@click="showAddModal = true"
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:w-auto"
|
||||
>
|
||||
Ajouter un serveur
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flow-root">
|
||||
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-300">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">
|
||||
Nom
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||
IP
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||
MAC
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||
Statut
|
||||
</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
<tr v-for="server in servers" :key="server.id">
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
||||
{{ server.name }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
{{ server.ip_address }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
{{ server.mac_address }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<span
|
||||
class="inline-flex px-2 py-1 text-xs font-medium rounded-full"
|
||||
:class="server.is_online ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
|
||||
>
|
||||
{{ server.is_online ? 'En ligne' : 'Hors ligne' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||
<button
|
||||
@click="wakeServer(server.id)"
|
||||
:disabled="loading"
|
||||
class="text-blue-600 hover:text-blue-900 mr-4 disabled:opacity-50"
|
||||
>
|
||||
Wake
|
||||
</button>
|
||||
<button
|
||||
@click="pingServer(server.id)"
|
||||
:disabled="loading"
|
||||
class="text-green-600 hover:text-green-900 mr-4 disabled:opacity-50"
|
||||
>
|
||||
Ping
|
||||
</button>
|
||||
<button
|
||||
@click="editServer(server)"
|
||||
class="text-indigo-600 hover:text-indigo-900 mr-4"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
<button
|
||||
@click="deleteServer(server.id)"
|
||||
class="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal d'ajout/modification -->
|
||||
<div v-if="showAddModal || editingServer" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
{{ editingServer ? 'Modifier le serveur' : 'Ajouter un serveur' }}
|
||||
</h3>
|
||||
<form @submit.prevent="saveServer">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Nom</label>
|
||||
<input
|
||||
v-model="serverForm.name"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Adresse IP</label>
|
||||
<input
|
||||
v-model="serverForm.ip_address"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Adresse MAC</label>
|
||||
<input
|
||||
v-model="serverForm.mac_address"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea
|
||||
v-model="serverForm.description"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex items-center justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{{ editingServer ? 'Modifier' : 'Ajouter' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { serversApi, wolApi } from '../services/api'
|
||||
|
||||
const servers = ref([])
|
||||
const loading = ref(false)
|
||||
const showAddModal = ref(false)
|
||||
const editingServer = ref(null)
|
||||
|
||||
const serverForm = reactive({
|
||||
name: '',
|
||||
ip_address: '',
|
||||
mac_address: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const loadServers = async () => {
|
||||
try {
|
||||
const response = await serversApi.getAll()
|
||||
servers.value = response.data
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des serveurs:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const saveServer = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
if (editingServer.value) {
|
||||
await serversApi.update(editingServer.value.id, serverForm)
|
||||
} else {
|
||||
await serversApi.create(serverForm)
|
||||
}
|
||||
await loadServers()
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const editServer = (server) => {
|
||||
editingServer.value = server
|
||||
Object.assign(serverForm, server)
|
||||
}
|
||||
|
||||
const deleteServer = async (serverId) => {
|
||||
if (confirm('Êtes-vous sûr de vouloir supprimer ce serveur ?')) {
|
||||
try {
|
||||
await serversApi.delete(serverId)
|
||||
await loadServers()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wakeServer = async (serverId) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await wolApi.wake(serverId)
|
||||
await loadServers()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du réveil du serveur:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const pingServer = async (serverId) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await wolApi.ping(serverId)
|
||||
await loadServers()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du ping:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
showAddModal.value = false
|
||||
editingServer.value = null
|
||||
Object.assign(serverForm, {
|
||||
name: '',
|
||||
ip_address: '',
|
||||
mac_address: '',
|
||||
description: ''
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadServers()
|
||||
})
|
||||
</script>
|
||||
11
frontend/tailwind.config.js
Normal file
11
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
10
frontend/vite.config.js
Normal file
10
frontend/vite.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000
|
||||
}
|
||||
})
|
||||
5
nginx/Dockerfile
Normal file
5
nginx/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
EXPOSE 80
|
||||
33
nginx/nginx.conf
Normal file
33
nginx/nginx.conf
Normal file
@@ -0,0 +1,33 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
upstream backend {
|
||||
server backend:8000;
|
||||
}
|
||||
|
||||
upstream frontend {
|
||||
server frontend:3000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://frontend/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user