parent
0c58db798a
commit
a0e9d3e2a0
Binary file not shown.
@ -0,0 +1,766 @@
|
|||||||
|
# 🚀 Git Pusher for Splunk
|
||||||
|
|
||||||
|
**Version 2.1** | Application Splunk pour déployer vos applications vers Git et le Search Head Cluster
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Table des matières
|
||||||
|
|
||||||
|
- [Présentation](#-présentation)
|
||||||
|
- [Fonctionnalités](#-fonctionnalités)
|
||||||
|
- [Architecture](#-architecture)
|
||||||
|
- [Installation - Serveur Source](#-installation---serveur-source)
|
||||||
|
- [Installation - SH Deployer Agent](#-installation---sh-deployer-agent)
|
||||||
|
- [Configuration](#-configuration)
|
||||||
|
- [Configuration HTTPS](#-configuration-https)
|
||||||
|
- [Système de Licence](#-système-de-licence)
|
||||||
|
- [Utilisation](#-utilisation)
|
||||||
|
- [Sécurité](#-sécurité)
|
||||||
|
- [Personnalisation du Dashboard](#-personnalisation-du-dashboard)
|
||||||
|
- [Dépannage](#-dépannage)
|
||||||
|
- [API Reference](#-api-reference)
|
||||||
|
- [Changelog](#-changelog)
|
||||||
|
- [Support](#-support)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Présentation
|
||||||
|
|
||||||
|
**Git Pusher** est une application Splunk Enterprise qui permet de :
|
||||||
|
1. **Versionner** vos applications Splunk dans un repository Git
|
||||||
|
2. **Déployer automatiquement** vers un Search Head Cluster via le SH Deployer
|
||||||
|
|
||||||
|
Le workflow complet en un clic :
|
||||||
|
```
|
||||||
|
Splunk Source → Push Git → Pull SH Deployer → Apply Bundle → Search Head Cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
Idéal pour :
|
||||||
|
- 📦 Sauvegarder vos configurations Splunk
|
||||||
|
- 🔄 Versionner vos dashboards et applications
|
||||||
|
- 👥 Collaborer en équipe sur les développements Splunk
|
||||||
|
- 🚀 Mettre en place un workflow CI/CD pour Splunk
|
||||||
|
- 🎯 Déployer automatiquement vers votre Search Head Cluster
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Fonctionnalités
|
||||||
|
|
||||||
|
| Fonctionnalité | Description |
|
||||||
|
|----------------|-------------|
|
||||||
|
| **Push vers Git** | Déployez une ou plusieurs applications Splunk vers votre repository Git |
|
||||||
|
| **Déploiement SH Cluster** | Déploiement automatique vers le Search Head Cluster après le push Git |
|
||||||
|
| **Interface moderne** | Dashboard intuitif avec sélection visuelle des applications |
|
||||||
|
| **Multi-repository** | Support de GitHub, GitLab, Gitea, Bitbucket et tout serveur Git |
|
||||||
|
| **Support HTTPS** | Communication sécurisée avec certificats SSL |
|
||||||
|
| **Système de licence** | Gestion par fichier `.lic` sécurisé |
|
||||||
|
| **Credentials sécurisés** | Chiffrement des mots de passe Splunk |
|
||||||
|
| **Logs détaillés** | Traçabilité complète des opérations |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗 Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ SERVEUR SOURCE │
|
||||||
|
│ ┌─────────────┐ ┌─────────────────────────────────────┐ │
|
||||||
|
│ │ Splunk │────▶│ Git Pusher Server (:9999) │ │
|
||||||
|
│ │ Dashboard │ │ 1. Push apps vers Git │ │
|
||||||
|
│ │ (HTTPS) │ │ 2. Appelle le SH Deployer Agent │ │
|
||||||
|
│ └─────────────┘ └──────────────────┬──────────────────┘ │
|
||||||
|
└─────────────────────────────────────────┼───────────────────────┘
|
||||||
|
│ HTTPS (:9998)
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ SH DEPLOYER │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Deployer Agent (:9998) │ │
|
||||||
|
│ │ 1. Git pull dans /opt/splunk/etc/shcluster/apps/ │ │
|
||||||
|
│ │ 2. Exécute: splunk apply shcluster-bundle │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ SEARCH HEAD CLUSTER │
|
||||||
|
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
||||||
|
│ │ SH1 │ │ SH2 │ │ SH3 │ │
|
||||||
|
│ └───────────┘ └───────────┘ └───────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Structure des fichiers
|
||||||
|
|
||||||
|
```
|
||||||
|
SERVEUR SOURCE - pusher_app_prem/
|
||||||
|
├── bin/
|
||||||
|
│ ├── git_pusher.py # Serveur HTTP/HTTPS principal (port 9999)
|
||||||
|
│ ├── license_validator.py # Validation des licences
|
||||||
|
│ ├── license_generator.py # Génération des licences (vendeur)
|
||||||
|
│ ├── credentials_manager.py # Gestion sécurisée des credentials
|
||||||
|
│ └── start_git_pusher.sh # Script de démarrage
|
||||||
|
├── appserver/static/
|
||||||
|
│ ├── git_pusher.js # Logique JavaScript principale
|
||||||
|
│ └── license_validation.js # Interface de gestion des licences
|
||||||
|
├── default/data/ui/views/
|
||||||
|
│ └── git_pusher_-_deploy_applications.xml # Dashboard principal
|
||||||
|
├── local/
|
||||||
|
│ ├── license.lic # Fichier de licence (après activation)
|
||||||
|
│ ├── .credentials # Credentials chiffrés
|
||||||
|
│ ├── .key # Clé de chiffrement
|
||||||
|
│ └── certs/ # Certificats SSL
|
||||||
|
│ ├── server.crt
|
||||||
|
│ └── server.key
|
||||||
|
└── README.md
|
||||||
|
|
||||||
|
SH DEPLOYER - deployer_agent/
|
||||||
|
├── bin/
|
||||||
|
│ ├── deployer_agent.py # Agent de déploiement (port 9998)
|
||||||
|
│ └── start_deployer_agent.sh # Script de démarrage
|
||||||
|
└── local/
|
||||||
|
└── certs/ # Certificats SSL
|
||||||
|
├── server.crt
|
||||||
|
└── server.key
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📥 Installation - Serveur Source
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
|
||||||
|
- Splunk Enterprise 8.x ou supérieur
|
||||||
|
- Python 3.7+
|
||||||
|
- Git installé sur le serveur (`yum install git` ou `apt install git`)
|
||||||
|
- OpenSSL (pour la génération des certificats HTTPS)
|
||||||
|
- Accès réseau vers votre repository Git
|
||||||
|
|
||||||
|
### Étapes d'installation
|
||||||
|
|
||||||
|
#### 1. Extraire l'application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copier l'application dans Splunk
|
||||||
|
cp -r pusher_app_prem /opt/splunk/etc/apps/
|
||||||
|
|
||||||
|
# Définir les permissions
|
||||||
|
chown -R splunk:splunk /opt/splunk/etc/apps/pusher_app_prem
|
||||||
|
chmod +x /opt/splunk/etc/apps/pusher_app_prem/bin/*.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Configurer les credentials Splunk
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/splunk/etc/apps/pusher_app_prem/bin/
|
||||||
|
./start_git_pusher.sh credentials setup
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
==================================================
|
||||||
|
Git Pusher - Credentials Setup
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Splunk Username [admin]: admin
|
||||||
|
Splunk Password: ********
|
||||||
|
Confirm Password: ********
|
||||||
|
|
||||||
|
✓ Credentials saved securely!
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Configurer HTTPS (si Splunk est en HTTPS)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Créer le dossier des certificats
|
||||||
|
mkdir -p /opt/splunk/etc/apps/pusher_app_prem/local/certs
|
||||||
|
|
||||||
|
# Générer un certificat auto-signé (valide 365 jours)
|
||||||
|
openssl req -x509 -newkey rsa:4096 \
|
||||||
|
-keyout /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key \
|
||||||
|
-out /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.crt \
|
||||||
|
-days 365 -nodes -subj "/CN=git-pusher"
|
||||||
|
|
||||||
|
# Définir les permissions
|
||||||
|
chmod 600 /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key
|
||||||
|
chown -R splunk:splunk /opt/splunk/etc/apps/pusher_app_prem/local/certs/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Démarrer le serveur Git Pusher
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start_git_pusher.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Ouvrir le firewall (si nécessaire)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# FirewallD
|
||||||
|
sudo firewall-cmd --add-port=9999/tcp --permanent
|
||||||
|
sudo firewall-cmd --reload
|
||||||
|
|
||||||
|
# UFW
|
||||||
|
sudo ufw allow 9999/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Redémarrer Splunk
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/opt/splunk/bin/splunk restart
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7. Accepter le certificat dans le navigateur
|
||||||
|
|
||||||
|
1. Ouvrir : `https://VOTRE_IP_SPLUNK:9999/health`
|
||||||
|
2. Accepter le certificat auto-signé
|
||||||
|
3. Rafraîchir le dashboard (Ctrl+Shift+R)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📥 Installation - SH Deployer Agent
|
||||||
|
|
||||||
|
### Sur le serveur SH Deployer
|
||||||
|
|
||||||
|
#### 1. Créer l'application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Créer les dossiers
|
||||||
|
mkdir -p /opt/splunk/etc/apps/deployer_agent/bin
|
||||||
|
mkdir -p /opt/splunk/etc/apps/deployer_agent/local/certs
|
||||||
|
|
||||||
|
# Copier les fichiers
|
||||||
|
cp deployer_agent.py /opt/splunk/etc/apps/deployer_agent/bin/
|
||||||
|
cp start_deployer_agent.sh /opt/splunk/etc/apps/deployer_agent/bin/
|
||||||
|
|
||||||
|
# Rendre exécutable
|
||||||
|
chmod +x /opt/splunk/etc/apps/deployer_agent/bin/start_deployer_agent.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Configurer le token d'authentification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano /opt/splunk/etc/apps/deployer_agent/bin/deployer_agent.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Modifier la ligne `AUTH_TOKEN` :
|
||||||
|
```python
|
||||||
|
AUTH_TOKEN = "votre_token_secret_personnalise"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Configurer le Search Head Captain
|
||||||
|
|
||||||
|
Dans le même fichier, trouver la fonction `apply_shcluster_bundle` et modifier l'IP du captain :
|
||||||
|
```python
|
||||||
|
target_uri = "https://IP_DU_CAPTAIN:8089"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Générer les certificats SSL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/splunk/etc/apps/deployer_agent/bin/
|
||||||
|
./start_deployer_agent.sh gencerts
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Définir les permissions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chown -R splunk:splunk /opt/splunk/etc/apps/deployer_agent
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Ouvrir le firewall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo firewall-cmd --add-port=9998/tcp --permanent
|
||||||
|
sudo firewall-cmd --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7. Démarrer l'agent
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start_deployer_agent.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8. Vérifier le statut
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start_deployer_agent.sh status
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 9. Accepter le certificat
|
||||||
|
|
||||||
|
Ouvrir dans un navigateur : `https://IP_SH_DEPLOYER:9998/health`
|
||||||
|
Accepter le certificat auto-signé.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
### Commandes Git Pusher (Serveur Source)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/splunk/etc/apps/pusher_app_prem/bin/
|
||||||
|
|
||||||
|
# Démarrer le serveur
|
||||||
|
./start_git_pusher.sh start
|
||||||
|
|
||||||
|
# Arrêter le serveur
|
||||||
|
./start_git_pusher.sh stop
|
||||||
|
|
||||||
|
# Redémarrer le serveur
|
||||||
|
./start_git_pusher.sh restart
|
||||||
|
|
||||||
|
# Voir le statut
|
||||||
|
./start_git_pusher.sh status
|
||||||
|
|
||||||
|
# Voir les logs
|
||||||
|
./start_git_pusher.sh logs
|
||||||
|
./start_git_pusher.sh logs -f # Mode follow
|
||||||
|
|
||||||
|
# Gestion des credentials
|
||||||
|
./start_git_pusher.sh credentials setup
|
||||||
|
./start_git_pusher.sh credentials status
|
||||||
|
./start_git_pusher.sh credentials delete
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commandes Deployer Agent (SH Deployer)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/splunk/etc/apps/deployer_agent/bin/
|
||||||
|
|
||||||
|
# Démarrer l'agent
|
||||||
|
./start_deployer_agent.sh start
|
||||||
|
|
||||||
|
# Arrêter l'agent
|
||||||
|
./start_deployer_agent.sh stop
|
||||||
|
|
||||||
|
# Redémarrer l'agent
|
||||||
|
./start_deployer_agent.sh restart
|
||||||
|
|
||||||
|
# Voir le statut
|
||||||
|
./start_deployer_agent.sh status
|
||||||
|
|
||||||
|
# Voir les logs
|
||||||
|
./start_deployer_agent.sh logs
|
||||||
|
./start_deployer_agent.sh logs -f
|
||||||
|
|
||||||
|
# Générer les certificats SSL
|
||||||
|
./start_deployer_agent.sh gencerts
|
||||||
|
|
||||||
|
# Tester la connexion
|
||||||
|
./start_deployer_agent.sh test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration du SH Deployer dans l'interface
|
||||||
|
|
||||||
|
1. Ouvrir le dashboard Git Pusher
|
||||||
|
2. Cliquer sur **⚙️ Configure** dans la section "Deploy to Search Head Cluster"
|
||||||
|
3. Remplir :
|
||||||
|
- **Host** : IP du SH Deployer (ex: 10.10.40.14)
|
||||||
|
- **Port** : 9998
|
||||||
|
- **Token** : Le même token que dans `deployer_agent.py`
|
||||||
|
4. Cliquer sur **Save & Test**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Configuration HTTPS
|
||||||
|
|
||||||
|
### Git Pusher Server (Port 9999)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /opt/splunk/etc/apps/pusher_app_prem/local/certs
|
||||||
|
|
||||||
|
openssl req -x509 -newkey rsa:4096 \
|
||||||
|
-keyout /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key \
|
||||||
|
-out /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.crt \
|
||||||
|
-days 365 -nodes -subj "/CN=git-pusher"
|
||||||
|
|
||||||
|
chmod 600 /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key
|
||||||
|
chown -R splunk:splunk /opt/splunk/etc/apps/pusher_app_prem/local/certs/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployer Agent (Port 9998)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /opt/splunk/etc/apps/deployer_agent/local/certs
|
||||||
|
|
||||||
|
openssl req -x509 -newkey rsa:4096 \
|
||||||
|
-keyout /opt/splunk/etc/apps/deployer_agent/local/certs/server.key \
|
||||||
|
-out /opt/splunk/etc/apps/deployer_agent/local/certs/server.crt \
|
||||||
|
-days 365 -nodes -subj "/CN=deployer-agent"
|
||||||
|
|
||||||
|
chmod 600 /opt/splunk/etc/apps/deployer_agent/local/certs/server.key
|
||||||
|
chown -R splunk:splunk /opt/splunk/etc/apps/deployer_agent/local/certs/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérifier HTTPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Git Pusher
|
||||||
|
curl -k https://localhost:9999/health
|
||||||
|
|
||||||
|
# Deployer Agent
|
||||||
|
curl -k https://localhost:9998/health
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Système de Licence
|
||||||
|
|
||||||
|
Git Pusher utilise un système de licence par fichier `.lic` pour activer les fonctionnalités.
|
||||||
|
|
||||||
|
### Types de licences
|
||||||
|
|
||||||
|
| Type | Durée | Apps max | Pushes/jour | Fonctionnalités |
|
||||||
|
|------|-------|----------|-------------|-----------------|
|
||||||
|
| **Trial** | 14 jours | 3 | 5 | Push basique |
|
||||||
|
| **Starter** | 1 an | 10 | 50 | + Push programmé |
|
||||||
|
| **Professional** | 1 an | Illimité | Illimité | + Multi-repo, Support prioritaire |
|
||||||
|
| **Enterprise** | 1 an | Illimité | Illimité | + SH Cluster deployment, API |
|
||||||
|
|
||||||
|
### Activer une licence
|
||||||
|
|
||||||
|
1. Ouvrir le dashboard Git Pusher
|
||||||
|
2. Glisser-déposer le fichier `.lic`
|
||||||
|
3. Cliquer sur "Activer"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Utilisation
|
||||||
|
|
||||||
|
### Workflow complet : Push Git + Déploiement SH Cluster
|
||||||
|
|
||||||
|
1. **Ouvrir le dashboard** Git Pusher
|
||||||
|
|
||||||
|
2. **Sélectionner les applications** à déployer
|
||||||
|
|
||||||
|
3. **Configurer Git** :
|
||||||
|
- Repository URL : `https://github.com/user/repo.git`
|
||||||
|
- Branch : `main`
|
||||||
|
- Token : Personal Access Token
|
||||||
|
|
||||||
|
4. **Activer le déploiement SH Cluster** :
|
||||||
|
- Cocher "Enable automatic deployment"
|
||||||
|
- (Optionnel) Remplir les credentials Splunk si différents de l'admin par défaut
|
||||||
|
|
||||||
|
5. **Cliquer sur "Deploy to Git"**
|
||||||
|
|
||||||
|
Le processus automatique :
|
||||||
|
```
|
||||||
|
✅ Push vers Git
|
||||||
|
✅ Pull sur le SH Deployer
|
||||||
|
✅ Apply shcluster-bundle
|
||||||
|
✅ Mise à jour du Search Head Cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
### Obtenir un token Git
|
||||||
|
|
||||||
|
#### GitHub
|
||||||
|
1. Settings → Developer settings → Personal access tokens
|
||||||
|
2. Generate new token
|
||||||
|
3. Cocher : `repo` (Full control)
|
||||||
|
|
||||||
|
#### GitLab
|
||||||
|
1. Preferences → Access Tokens
|
||||||
|
2. Create personal access token
|
||||||
|
3. Scopes : `write_repository`
|
||||||
|
|
||||||
|
#### Gitea
|
||||||
|
1. Settings → Applications → Generate New Token
|
||||||
|
2. Permissions : `repository: write`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Sécurité
|
||||||
|
|
||||||
|
### Credentials Splunk
|
||||||
|
|
||||||
|
- **Chiffrés** avec une clé dérivée de l'ID machine
|
||||||
|
- **Stockés** dans un fichier avec permissions `600`
|
||||||
|
- **Jamais visibles** en clair
|
||||||
|
|
||||||
|
### Communication HTTPS
|
||||||
|
|
||||||
|
- SSL/TLS entre tous les composants
|
||||||
|
- Certificats auto-signés supportés
|
||||||
|
- Validation du token d'authentification
|
||||||
|
|
||||||
|
### Token Deployer Agent
|
||||||
|
|
||||||
|
- Authentification par token entre Git Pusher et Deployer Agent
|
||||||
|
- Token configurable dans les deux composants
|
||||||
|
- Doit être identique des deux côtés
|
||||||
|
|
||||||
|
### Bonnes pratiques
|
||||||
|
|
||||||
|
1. ✅ Utiliser HTTPS pour tous les composants
|
||||||
|
2. ✅ Changer le token par défaut du Deployer Agent
|
||||||
|
3. ✅ Utiliser des tokens Git avec permissions minimales
|
||||||
|
4. ✅ Restreindre l'accès réseau au port 9998/9999
|
||||||
|
5. ✅ Renouveler les certificats régulièrement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Personnalisation du Dashboard
|
||||||
|
|
||||||
|
### Masquer les boutons d'édition
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano /opt/splunk/etc/apps/pusher_app_prem/default/data/ui/views/git_pusher_-_deploy_applications.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
Modifier la première ligne :
|
||||||
|
```xml
|
||||||
|
<dashboard version="1.1" script="license_validation.js, git_pusher.js" hideEdit="true" hideExport="true">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options disponibles
|
||||||
|
|
||||||
|
| Attribut | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `hideEdit="true"` | Masque le bouton "Modifier" |
|
||||||
|
| `hideExport="true"` | Masque le bouton "Exporter" |
|
||||||
|
| `hideTitle="true"` | Masque le titre du dashboard |
|
||||||
|
| `hideSplunkBar="true"` | Masque la barre Splunk en haut |
|
||||||
|
| `hideAppBar="true"` | Masque la barre de l'application |
|
||||||
|
| `hideFooter="true"` | Masque le footer |
|
||||||
|
|
||||||
|
### Appliquer les changements
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf /opt/splunk/var/run/splunk/appserver/*
|
||||||
|
/opt/splunk/bin/splunk restart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Dépannage
|
||||||
|
|
||||||
|
### Les boutons ne fonctionnent pas
|
||||||
|
|
||||||
|
Le fichier JavaScript doit exposer les fonctions globalement. Vérifier que le bloc suivant est présent à la fin de `git_pusher.js` :
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Exposer les fonctions globalement
|
||||||
|
window.showDeployerConfigModal = showDeployerConfigModal;
|
||||||
|
window.closeDeployerConfigModal = closeDeployerConfigModal;
|
||||||
|
window.saveDeployerConfigFromModal = saveDeployerConfigFromModal;
|
||||||
|
window.toggleDeployerAuth = toggleDeployerAuth;
|
||||||
|
|
||||||
|
// Attacher les événements aux boutons
|
||||||
|
(function attachButtonEvents() {
|
||||||
|
function tryAttach() {
|
||||||
|
var pushBtn = document.getElementById('push-btn');
|
||||||
|
if (pushBtn) {
|
||||||
|
pushBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
pushDashboards();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var buttons = document.querySelectorAll('button.btn');
|
||||||
|
buttons.forEach(function(btn) {
|
||||||
|
if (btn.textContent.includes('Reset')) {
|
||||||
|
btn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
resetForm(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var configBtn = document.querySelector('.deployer-config-btn');
|
||||||
|
if (configBtn) {
|
||||||
|
configBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
showDeployerConfigModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pushBtn) setTimeout(tryAttach, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
setTimeout(tryAttach, 1000);
|
||||||
|
} else {
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
setTimeout(tryAttach, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis vider le cache :
|
||||||
|
```bash
|
||||||
|
rm -rf /opt/splunk/var/run/splunk/appserver/*
|
||||||
|
/opt/splunk/bin/splunk restart
|
||||||
|
```
|
||||||
|
|
||||||
|
Et dans le navigateur : **Ctrl+Shift+R**
|
||||||
|
|
||||||
|
### Erreur 401 Unauthorized (Deployer)
|
||||||
|
|
||||||
|
Le token ne correspond pas entre les deux composants.
|
||||||
|
|
||||||
|
1. Vérifier le token sur le SH Deployer :
|
||||||
|
```bash
|
||||||
|
grep "AUTH_TOKEN" /opt/splunk/etc/apps/deployer_agent/bin/deployer_agent.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Configurer le même token dans l'interface (⚙️ Configure)
|
||||||
|
|
||||||
|
3. Tester :
|
||||||
|
```bash
|
||||||
|
curl -k -H "X-Auth-Token: VOTRE_TOKEN" https://IP_DEPLOYER:9998/status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreur "tcsetattr: Inappropriate ioctl for device"
|
||||||
|
|
||||||
|
La commande `splunk apply shcluster-bundle` attend une entrée interactive.
|
||||||
|
|
||||||
|
Solution : Modifier la fonction `apply_shcluster_bundle` dans `deployer_agent.py` pour utiliser `echo 'y' |` :
|
||||||
|
|
||||||
|
```python
|
||||||
|
shell_cmd = f"echo 'y' | {SPLUNK_BIN} apply shcluster-bundle -target {target_uri} -auth {auth_user}:'{escaped_pass}' -preserve-lookups true"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreur CORS
|
||||||
|
|
||||||
|
Vider le cache Splunk et navigateur :
|
||||||
|
```bash
|
||||||
|
rm -rf /opt/splunk/var/run/splunk/appserver/*
|
||||||
|
/opt/splunk/bin/splunk restart
|
||||||
|
```
|
||||||
|
Puis **Ctrl+Shift+R** dans le navigateur.
|
||||||
|
|
||||||
|
### Le SH Deployer ne répond pas
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier le statut
|
||||||
|
./start_deployer_agent.sh status
|
||||||
|
|
||||||
|
# Voir les logs
|
||||||
|
tail -50 /opt/splunk/var/log/splunk/deployer_agent.log
|
||||||
|
|
||||||
|
# Redémarrer
|
||||||
|
./start_deployer_agent.sh restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Le bundle ne s'applique pas
|
||||||
|
|
||||||
|
1. Vérifier que le pull a fonctionné :
|
||||||
|
```bash
|
||||||
|
ls -la /opt/splunk/etc/shcluster/apps/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Vérifier les logs :
|
||||||
|
```bash
|
||||||
|
grep -i "bundle\|error" /opt/splunk/var/log/splunk/deployer_agent.log | tail -30
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Tester manuellement :
|
||||||
|
```bash
|
||||||
|
echo 'y' | /opt/splunk/bin/splunk apply shcluster-bundle -target https://CAPTAIN_IP:8089 -auth admin:'password' -preserve-lookups true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vider le cache Splunk
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf /opt/splunk/var/run/splunk/appserver/*
|
||||||
|
/opt/splunk/bin/splunk restart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📡 API Reference
|
||||||
|
|
||||||
|
### Git Pusher Server (Port 9999)
|
||||||
|
|
||||||
|
| Méthode | Endpoint | Description |
|
||||||
|
|---------|----------|-------------|
|
||||||
|
| `GET` | `/health` | Health check |
|
||||||
|
| `GET` | `/license` | Statut de la licence |
|
||||||
|
| `GET` | `/license/hostname` | Hostname Splunk |
|
||||||
|
| `GET` | `/deployer/health` | Santé du SH Deployer |
|
||||||
|
| `GET` | `/deployer/status` | Statut du SH Deployer |
|
||||||
|
| `GET` | `/deployer/config` | Configuration du SH Deployer |
|
||||||
|
| `POST` | `/license/upload` | Uploader une licence |
|
||||||
|
| `POST` | `/push` | Pousser les applications |
|
||||||
|
|
||||||
|
### Deployer Agent (Port 9998)
|
||||||
|
|
||||||
|
| Méthode | Endpoint | Description |
|
||||||
|
|---------|----------|-------------|
|
||||||
|
| `GET` | `/health` | Health check (pas d'auth) |
|
||||||
|
| `GET` | `/status` | Statut du déploiement |
|
||||||
|
| `GET` | `/apps` | Liste des apps |
|
||||||
|
| `GET` | `/history` | Historique des déploiements |
|
||||||
|
| `POST` | `/pull` | Git pull |
|
||||||
|
| `POST` | `/deploy` | Apply shcluster-bundle |
|
||||||
|
| `POST` | `/pull-and-deploy` | Pull + Deploy en une opération |
|
||||||
|
|
||||||
|
### Exemple : Push avec déploiement SH Cluster
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -k -X POST "https://localhost:9999/push?git_url=https://github.com/user/repo.git&git_branch=main&git_token=TOKEN&commit_message=Update&apps=[{\"id\":\"my_app\"}]&user=admin&deploy_to_shcluster=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Changelog
|
||||||
|
|
||||||
|
### Version 2.1.0 (Février 2026)
|
||||||
|
- 🚀 **Nouveau** : Déploiement automatique vers Search Head Cluster
|
||||||
|
- 🔧 **Nouveau** : Agent Deployer pour le SH Deployer
|
||||||
|
- ⚙️ **Nouveau** : Interface de configuration du SH Deployer
|
||||||
|
- 🔐 **Amélioration** : Gestion des caractères spéciaux dans les mots de passe
|
||||||
|
- 🛠️ **Correction** : Mode non-interactif pour shcluster-bundle
|
||||||
|
|
||||||
|
### Version 2.0.0 (Février 2026)
|
||||||
|
- ✨ Nouveau système de licence par fichier `.lic`
|
||||||
|
- 🔐 Credentials Splunk chiffrés
|
||||||
|
- 🔒 Support HTTPS avec certificats SSL
|
||||||
|
- 🎨 Interface utilisateur modernisée
|
||||||
|
- 📊 Badge de licence en temps réel
|
||||||
|
- 🔧 Script de gestion amélioré
|
||||||
|
- 🛡️ Options pour masquer les boutons d'édition du dashboard
|
||||||
|
- 📝 Logs détaillés
|
||||||
|
- 🐛 Correction des problèmes CORS
|
||||||
|
|
||||||
|
### Version 1.0.0
|
||||||
|
- 🚀 Version initiale
|
||||||
|
- Push d'applications vers Git
|
||||||
|
- Interface basique
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
### Obtenir de l'aide
|
||||||
|
|
||||||
|
- 📧 Email : support@gitpusher.com
|
||||||
|
- 🌐 Site web : https://gitpusher.com
|
||||||
|
- 📖 Documentation : https://docs.gitpusher.com
|
||||||
|
|
||||||
|
### Signaler un bug
|
||||||
|
|
||||||
|
Incluez dans votre rapport :
|
||||||
|
1. Version de Git Pusher
|
||||||
|
2. Version de Splunk
|
||||||
|
3. Configuration (HTTP/HTTPS)
|
||||||
|
4. Architecture (standalone, SH Cluster)
|
||||||
|
5. Logs Git Pusher (`/opt/splunk/var/log/splunk/git_pusher.log`)
|
||||||
|
6. Logs Deployer Agent (`/opt/splunk/var/log/splunk/deployer_agent.log`)
|
||||||
|
7. Erreurs de la console navigateur (F12)
|
||||||
|
8. Étapes pour reproduire le problème
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Licence
|
||||||
|
|
||||||
|
Git Pusher est un logiciel propriétaire. Une licence valide est requise pour son utilisation.
|
||||||
|
|
||||||
|
© 2026 Git Pusher - Tous droits réservés
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Made with ❤️ for Splunk administrators
|
||||||
|
</p>
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,391 @@
|
|||||||
|
// ============================================
|
||||||
|
// GIT PUSHER - CONFIGURATION PAGE
|
||||||
|
// Version 2.1 - Compatible Splunk
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
require([
|
||||||
|
'jquery',
|
||||||
|
'splunkjs/mvc',
|
||||||
|
'splunkjs/mvc/simplexml/ready!'
|
||||||
|
], function($, mvc) {
|
||||||
|
|
||||||
|
console.log('Git Pusher Config v2.1 initializing...');
|
||||||
|
|
||||||
|
// Configuration par défaut
|
||||||
|
var DEFAULT_CONFIG = {
|
||||||
|
api: {
|
||||||
|
url: '',
|
||||||
|
port: 9999,
|
||||||
|
useProxy: true
|
||||||
|
},
|
||||||
|
deployer: {
|
||||||
|
enabled: false,
|
||||||
|
host: '',
|
||||||
|
port: 9998,
|
||||||
|
token: '',
|
||||||
|
useSSL: true
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
checkInterval: 24
|
||||||
|
},
|
||||||
|
advanced: {
|
||||||
|
logLevel: 'INFO',
|
||||||
|
timeout: 30,
|
||||||
|
gitTimeout: 120
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// URL de l'API pour la config
|
||||||
|
function getConfigApiUrl() {
|
||||||
|
var hostname = window.location.hostname;
|
||||||
|
var protocol = window.location.protocol;
|
||||||
|
|
||||||
|
// Essayer de charger depuis localStorage
|
||||||
|
try {
|
||||||
|
var stored = localStorage.getItem('git_pusher_config');
|
||||||
|
if (stored) {
|
||||||
|
var config = JSON.parse(stored);
|
||||||
|
if (config.api && config.api.url) {
|
||||||
|
var url = config.api.url;
|
||||||
|
if (!config.api.useProxy && config.api.port) {
|
||||||
|
url = url.replace(/\/$/, '') + ':' + config.api.port;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
// Auto-détection
|
||||||
|
if (hostname === 'myprivspldev.jp-engineering.fr') {
|
||||||
|
return protocol + '//myprivspldev-api.jp-engineering.fr';
|
||||||
|
}
|
||||||
|
if (hostname === 'myprivspldev-api.jp-engineering.fr') {
|
||||||
|
return protocol + '//' + hostname;
|
||||||
|
}
|
||||||
|
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname) || hostname === 'localhost') {
|
||||||
|
return protocol + '//' + hostname + ':9999';
|
||||||
|
}
|
||||||
|
return protocol + '//' + hostname + ':9999';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// CHARGEMENT DE LA CONFIGURATION
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function loadConfig() {
|
||||||
|
console.log('Loading configuration...');
|
||||||
|
var apiUrl = getConfigApiUrl();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: apiUrl + '/config',
|
||||||
|
method: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(config) {
|
||||||
|
console.log('Config loaded:', config);
|
||||||
|
applyConfigToForm(config);
|
||||||
|
showMessage('Configuration chargée', 'success');
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.log('No server config, using defaults:', error);
|
||||||
|
applyConfigToForm(DEFAULT_CONFIG);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loadLicenseStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyConfigToForm(config) {
|
||||||
|
// API
|
||||||
|
$('#api-url').val(config.api ? config.api.url || '' : '');
|
||||||
|
$('#api-port').val(config.api ? config.api.port || 9999 : 9999);
|
||||||
|
$('#use-proxy').prop('checked', config.api ? config.api.useProxy !== false : true);
|
||||||
|
|
||||||
|
// Deployer
|
||||||
|
$('#deployer-enabled').prop('checked', config.deployer ? config.deployer.enabled || false : false);
|
||||||
|
$('#deployer-host').val(config.deployer ? config.deployer.host || '' : '');
|
||||||
|
$('#deployer-port').val(config.deployer ? config.deployer.port || 9998 : 9998);
|
||||||
|
$('#deployer-token').val(config.deployer ? config.deployer.token || '' : '');
|
||||||
|
$('#deployer-use-ssl').prop('checked', config.deployer ? config.deployer.useSSL !== false : true);
|
||||||
|
|
||||||
|
// Licence
|
||||||
|
$('#license-check-interval').val(config.license ? config.license.checkInterval || 24 : 24);
|
||||||
|
|
||||||
|
// Avancé
|
||||||
|
$('#log-level').val(config.advanced ? config.advanced.logLevel || 'INFO' : 'INFO');
|
||||||
|
$('#timeout').val(config.advanced ? config.advanced.timeout || 30 : 30);
|
||||||
|
$('#git-timeout').val(config.advanced ? config.advanced.gitTimeout || 120 : 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfigFromForm() {
|
||||||
|
return {
|
||||||
|
api: {
|
||||||
|
url: $('#api-url').val().trim(),
|
||||||
|
port: parseInt($('#api-port').val()) || 9999,
|
||||||
|
useProxy: $('#use-proxy').is(':checked')
|
||||||
|
},
|
||||||
|
deployer: {
|
||||||
|
enabled: $('#deployer-enabled').is(':checked'),
|
||||||
|
host: $('#deployer-host').val().trim(),
|
||||||
|
port: parseInt($('#deployer-port').val()) || 9998,
|
||||||
|
token: $('#deployer-token').val(),
|
||||||
|
useSSL: $('#deployer-use-ssl').is(':checked')
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
checkInterval: parseInt($('#license-check-interval').val()) || 24
|
||||||
|
},
|
||||||
|
advanced: {
|
||||||
|
logLevel: $('#log-level').val(),
|
||||||
|
timeout: parseInt($('#timeout').val()) || 30,
|
||||||
|
gitTimeout: parseInt($('#git-timeout').val()) || 120
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// SAUVEGARDE DE LA CONFIGURATION
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function saveConfig() {
|
||||||
|
console.log('Saving configuration...');
|
||||||
|
var config = getConfigFromForm();
|
||||||
|
var apiUrl = getConfigApiUrl();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: apiUrl + '/config',
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(config),
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(result) {
|
||||||
|
console.log('Save result:', result);
|
||||||
|
if (result.success) {
|
||||||
|
showMessage('✅ Configuration sauvegardée avec succès !', 'success');
|
||||||
|
// Sauvegarder aussi dans localStorage
|
||||||
|
localStorage.setItem('git_pusher_config', JSON.stringify(config));
|
||||||
|
} else {
|
||||||
|
showMessage('❌ Erreur: ' + (result.error || 'Échec de la sauvegarde'), 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('Save error:', error);
|
||||||
|
showMessage('❌ Erreur de connexion au serveur: ' + error, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetConfig() {
|
||||||
|
if (confirm('Voulez-vous vraiment réinitialiser la configuration ?')) {
|
||||||
|
applyConfigToForm(DEFAULT_CONFIG);
|
||||||
|
showMessage('Configuration réinitialisée (non sauvegardée)', 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// TESTS DE CONNEXION
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function testApiConnection() {
|
||||||
|
console.log('Testing API connection...');
|
||||||
|
var $status = $('#api-status');
|
||||||
|
$status.removeClass('connected disconnected').text('● Test en cours...');
|
||||||
|
|
||||||
|
var apiUrl = $('#api-url').val().trim();
|
||||||
|
|
||||||
|
if (!apiUrl) {
|
||||||
|
apiUrl = getConfigApiUrl();
|
||||||
|
} else if (!$('#use-proxy').is(':checked')) {
|
||||||
|
var port = $('#api-port').val() || 9999;
|
||||||
|
if (apiUrl.indexOf(':' + port) === -1) {
|
||||||
|
apiUrl = apiUrl.replace(/\/$/, '') + ':' + port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Testing URL:', apiUrl);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: apiUrl + '/health',
|
||||||
|
method: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
timeout: 10000,
|
||||||
|
success: function(data) {
|
||||||
|
console.log('API health:', data);
|
||||||
|
$status.addClass('connected').text('● Connecté');
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('API test failed:', error);
|
||||||
|
$status.addClass('disconnected').text('● Échec connexion');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testDeployerConnection() {
|
||||||
|
console.log('Testing Deployer connection...');
|
||||||
|
var $status = $('#deployer-status');
|
||||||
|
$status.removeClass('connected disconnected').text('● Test en cours...');
|
||||||
|
|
||||||
|
var host = $('#deployer-host').val().trim();
|
||||||
|
var port = $('#deployer-port').val() || 9998;
|
||||||
|
var useSSL = $('#deployer-use-ssl').is(':checked');
|
||||||
|
var token = $('#deployer-token').val();
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
$status.addClass('disconnected').text('● Adresse manquante');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var protocol = useSSL ? 'https' : 'http';
|
||||||
|
var url;
|
||||||
|
|
||||||
|
// Si c'est un nom de domaine (contient des lettres et des points, pas une IP)
|
||||||
|
// Ne pas ajouter le port (le proxy gère)
|
||||||
|
if (/^[a-zA-Z]/.test(host) && host.indexOf('.') > -1 && !/^(\d{1,3}\.){3}\d{1,3}$/.test(host)) {
|
||||||
|
// C'est un domaine, pas de port
|
||||||
|
url = protocol + '://' + host + '/health';
|
||||||
|
} else {
|
||||||
|
// C'est une IP ou localhost, ajouter le port
|
||||||
|
url = protocol + '://' + host + ':' + port + '/health';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Testing Deployer URL:', url);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
method: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'X-Auth-Token': token
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
console.log('Deployer health:', data);
|
||||||
|
$status.addClass('connected').text('● Connecté');
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('Deployer test failed:', error);
|
||||||
|
$status.addClass('disconnected').text('● Échec connexion');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// STATUT DE LA LICENCE
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function loadLicenseStatus() {
|
||||||
|
var $status = $('#license-status');
|
||||||
|
|
||||||
|
try {
|
||||||
|
var stored = localStorage.getItem('git_pusher_license');
|
||||||
|
|
||||||
|
if (stored) {
|
||||||
|
var parsed = JSON.parse(stored);
|
||||||
|
var licenseData = parsed.licenseData;
|
||||||
|
|
||||||
|
if (licenseData) {
|
||||||
|
var expires = new Date(licenseData.expires);
|
||||||
|
var now = new Date();
|
||||||
|
var daysRemaining = Math.ceil((expires - now) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (daysRemaining > 0) {
|
||||||
|
$status.html(
|
||||||
|
'<span class="config-status connected">● Active</span>' +
|
||||||
|
'<br><small>Type: ' + licenseData.type_name + ' | Expire: ' + licenseData.expires + ' (' + daysRemaining + 'j)</small>'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$status.html(
|
||||||
|
'<span class="config-status disconnected">● Expirée</span>' +
|
||||||
|
'<br><small>Expirée le ' + licenseData.expires + '</small>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$status.html('<span class="config-status disconnected">● Non installée</span>');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lecture licence:', error);
|
||||||
|
$status.html('<span class="config-status disconnected">● Erreur</span>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// UTILITAIRES
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function showMessage(message, type) {
|
||||||
|
var $msg = $('#config-message');
|
||||||
|
$msg.text(message).removeClass('success error').addClass(type).show();
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
$msg.fadeOut();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ATTACHER LES ÉVÉNEMENTS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function attachEvents() {
|
||||||
|
console.log('Attaching events...');
|
||||||
|
|
||||||
|
// Bouton Test API
|
||||||
|
$('#test-api-btn').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log('Test API clicked');
|
||||||
|
testApiConnection();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bouton Test Deployer
|
||||||
|
$('#test-deployer-btn').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log('Test Deployer clicked');
|
||||||
|
testDeployerConnection();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bouton Sauvegarder
|
||||||
|
$('#save-btn').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log('Save clicked');
|
||||||
|
saveConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bouton Réinitialiser
|
||||||
|
$('#reset-btn').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log('Reset clicked');
|
||||||
|
resetConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Events attached to buttons');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// INITIALISATION
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Attendre que le DOM soit complètement prêt
|
||||||
|
function init() {
|
||||||
|
if ($('#api-url').length > 0) {
|
||||||
|
console.log('DOM ready, initializing...');
|
||||||
|
attachEvents();
|
||||||
|
loadConfig();
|
||||||
|
} else {
|
||||||
|
console.log('DOM not ready, retrying...');
|
||||||
|
setTimeout(init, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(init, 500);
|
||||||
|
|
||||||
|
// Exposer globalement pour le debug
|
||||||
|
window.gitPusherConfig = {
|
||||||
|
saveConfig: saveConfig,
|
||||||
|
resetConfig: resetConfig,
|
||||||
|
testApiConnection: testApiConnection,
|
||||||
|
testDeployerConnection: testDeployerConnection,
|
||||||
|
loadConfig: loadConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Git Pusher Config module loaded');
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
|||||||
|
This is where you put any scripts you want to add to this app.
|
||||||
Binary file not shown.
@ -0,0 +1,271 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Git Pusher - Credentials Manager
|
||||||
|
Gère les credentials de manière sécurisée via Splunk storage/passwords
|
||||||
|
ou via un fichier chiffré local.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Chemin vers le fichier de credentials chiffré
|
||||||
|
CREDENTIALS_FILE = "/opt/splunk/etc/apps/pusher_app_prem/local/.credentials"
|
||||||
|
ENCRYPTION_KEY_FILE = "/opt/splunk/etc/apps/pusher_app_prem/local/.key"
|
||||||
|
|
||||||
|
|
||||||
|
def get_machine_id():
|
||||||
|
"""Obtenir un identifiant unique de la machine pour le chiffrement"""
|
||||||
|
machine_id = ""
|
||||||
|
|
||||||
|
# Essayer différentes sources
|
||||||
|
try:
|
||||||
|
with open('/etc/machine-id', 'r') as f:
|
||||||
|
machine_id = f.read().strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not machine_id:
|
||||||
|
try:
|
||||||
|
import socket
|
||||||
|
machine_id = socket.gethostname()
|
||||||
|
except:
|
||||||
|
machine_id = "default_key"
|
||||||
|
|
||||||
|
return machine_id
|
||||||
|
|
||||||
|
|
||||||
|
def derive_key(password, salt=None):
|
||||||
|
"""Dériver une clé de chiffrement à partir d'un mot de passe"""
|
||||||
|
if salt is None:
|
||||||
|
salt = get_machine_id().encode()
|
||||||
|
|
||||||
|
# Utiliser PBKDF2-like avec SHA256
|
||||||
|
key = hashlib.pbkdf2_hmac(
|
||||||
|
'sha256',
|
||||||
|
password.encode(),
|
||||||
|
salt,
|
||||||
|
100000
|
||||||
|
)
|
||||||
|
return base64.b64encode(key).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def simple_encrypt(data, key):
|
||||||
|
"""Chiffrement simple XOR (pour une sécurité basique)"""
|
||||||
|
# Pour une vraie sécurité, utiliser cryptography.fernet
|
||||||
|
key_bytes = key.encode() * (len(data) // len(key) + 1)
|
||||||
|
encrypted = bytes([a ^ b for a, b in zip(data.encode(), key_bytes[:len(data)])])
|
||||||
|
return base64.b64encode(encrypted).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def simple_decrypt(encrypted_data, key):
|
||||||
|
"""Déchiffrement simple XOR"""
|
||||||
|
try:
|
||||||
|
data = base64.b64decode(encrypted_data)
|
||||||
|
key_bytes = key.encode() * (len(data) // len(key) + 1)
|
||||||
|
decrypted = bytes([a ^ b for a, b in zip(data, key_bytes[:len(data)])])
|
||||||
|
return decrypted.decode()
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_encryption_key():
|
||||||
|
"""Obtenir ou créer la clé de chiffrement"""
|
||||||
|
if os.path.exists(ENCRYPTION_KEY_FILE):
|
||||||
|
with open(ENCRYPTION_KEY_FILE, 'r') as f:
|
||||||
|
return f.read().strip()
|
||||||
|
else:
|
||||||
|
# Générer une nouvelle clé basée sur la machine
|
||||||
|
key = derive_key(get_machine_id())
|
||||||
|
|
||||||
|
# Sauvegarder la clé
|
||||||
|
os.makedirs(os.path.dirname(ENCRYPTION_KEY_FILE), exist_ok=True)
|
||||||
|
with open(ENCRYPTION_KEY_FILE, 'w') as f:
|
||||||
|
f.write(key)
|
||||||
|
|
||||||
|
# Protéger le fichier
|
||||||
|
os.chmod(ENCRYPTION_KEY_FILE, 0o600)
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def save_credentials(username, password):
|
||||||
|
"""Sauvegarder les credentials de manière chiffrée"""
|
||||||
|
key = get_encryption_key()
|
||||||
|
|
||||||
|
credentials = {
|
||||||
|
'username': username,
|
||||||
|
'password': simple_encrypt(password, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(CREDENTIALS_FILE), exist_ok=True)
|
||||||
|
|
||||||
|
with open(CREDENTIALS_FILE, 'w') as f:
|
||||||
|
json.dump(credentials, f)
|
||||||
|
|
||||||
|
# Protéger le fichier
|
||||||
|
os.chmod(CREDENTIALS_FILE, 0o600)
|
||||||
|
|
||||||
|
print(f"✓ Credentials saved securely to {CREDENTIALS_FILE}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_credentials():
|
||||||
|
"""Charger les credentials chiffrés"""
|
||||||
|
if not os.path.exists(CREDENTIALS_FILE):
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
try:
|
||||||
|
key = get_encryption_key()
|
||||||
|
|
||||||
|
with open(CREDENTIALS_FILE, 'r') as f:
|
||||||
|
credentials = json.load(f)
|
||||||
|
|
||||||
|
username = credentials.get('username')
|
||||||
|
encrypted_password = credentials.get('password')
|
||||||
|
|
||||||
|
password = simple_decrypt(encrypted_password, key)
|
||||||
|
|
||||||
|
return username, password
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading credentials: {e}", file=sys.stderr)
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def delete_credentials():
|
||||||
|
"""Supprimer les credentials stockés"""
|
||||||
|
if os.path.exists(CREDENTIALS_FILE):
|
||||||
|
os.remove(CREDENTIALS_FILE)
|
||||||
|
print("✓ Credentials deleted")
|
||||||
|
else:
|
||||||
|
print("No credentials file found")
|
||||||
|
|
||||||
|
|
||||||
|
def get_credentials_for_script():
|
||||||
|
"""
|
||||||
|
Fonction utilisée par le script de démarrage pour obtenir les credentials.
|
||||||
|
Retourne username et password, ou None si non configurés.
|
||||||
|
"""
|
||||||
|
username, password = load_credentials()
|
||||||
|
|
||||||
|
if username and password:
|
||||||
|
return username, password
|
||||||
|
|
||||||
|
# Fallback sur les variables d'environnement
|
||||||
|
env_user = os.environ.get('SPLUNK_USERNAME')
|
||||||
|
env_pass = os.environ.get('SPLUNK_PASSWORD')
|
||||||
|
|
||||||
|
if env_user and env_pass:
|
||||||
|
return env_user, env_pass
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# CLI
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def interactive_setup():
|
||||||
|
"""Configuration interactive des credentials"""
|
||||||
|
print("=" * 50)
|
||||||
|
print("Git Pusher - Credentials Setup")
|
||||||
|
print("=" * 50)
|
||||||
|
print()
|
||||||
|
print("This will securely store your Splunk credentials.")
|
||||||
|
print(f"Credentials will be encrypted and saved to:")
|
||||||
|
print(f" {CREDENTIALS_FILE}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
username = input("Splunk Username [admin]: ").strip() or "admin"
|
||||||
|
|
||||||
|
import getpass
|
||||||
|
password = getpass.getpass("Splunk Password: ")
|
||||||
|
|
||||||
|
if not password:
|
||||||
|
print("Error: Password cannot be empty")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Confirmer
|
||||||
|
password2 = getpass.getpass("Confirm Password: ")
|
||||||
|
|
||||||
|
if password != password2:
|
||||||
|
print("Error: Passwords do not match")
|
||||||
|
return
|
||||||
|
|
||||||
|
save_credentials(username, password)
|
||||||
|
print()
|
||||||
|
print("✓ Credentials configured successfully!")
|
||||||
|
print()
|
||||||
|
print("You can now start Git Pusher without specifying credentials:")
|
||||||
|
print(" ./start_git_pusher.sh start")
|
||||||
|
|
||||||
|
|
||||||
|
def show_status():
|
||||||
|
"""Afficher le statut des credentials"""
|
||||||
|
print("=" * 50)
|
||||||
|
print("Credentials Status")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
if os.path.exists(CREDENTIALS_FILE):
|
||||||
|
print(f"✓ Credentials file exists: {CREDENTIALS_FILE}")
|
||||||
|
|
||||||
|
username, password = load_credentials()
|
||||||
|
if username:
|
||||||
|
print(f" Username: {username}")
|
||||||
|
print(f" Password: {'*' * len(password) if password else 'ERROR'}")
|
||||||
|
else:
|
||||||
|
print(" Error: Could not decrypt credentials")
|
||||||
|
else:
|
||||||
|
print("✗ No credentials file found")
|
||||||
|
print()
|
||||||
|
print("Run: python credentials_manager.py setup")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
show_status()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
command = sys.argv[1]
|
||||||
|
|
||||||
|
if command == 'setup':
|
||||||
|
interactive_setup()
|
||||||
|
|
||||||
|
elif command == 'status':
|
||||||
|
show_status()
|
||||||
|
|
||||||
|
elif command == 'delete':
|
||||||
|
delete_credentials()
|
||||||
|
|
||||||
|
elif command == 'get':
|
||||||
|
# Pour utilisation dans les scripts bash
|
||||||
|
username, password = get_credentials_for_script()
|
||||||
|
if username and password:
|
||||||
|
print(f"{username}")
|
||||||
|
print(f"{password}")
|
||||||
|
else:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
elif command == 'export':
|
||||||
|
# Exporter pour utilisation dans bash
|
||||||
|
username, password = get_credentials_for_script()
|
||||||
|
if username and password:
|
||||||
|
print(f"export SPLUNK_USERNAME='{username}'")
|
||||||
|
print(f"export SPLUNK_PASSWORD='{password}'")
|
||||||
|
else:
|
||||||
|
print("# No credentials found", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Usage: python credentials_manager.py [setup|status|delete|get|export]")
|
||||||
|
print()
|
||||||
|
print("Commands:")
|
||||||
|
print(" setup - Configure credentials interactively")
|
||||||
|
print(" status - Show credentials status")
|
||||||
|
print(" delete - Delete stored credentials")
|
||||||
|
print(" get - Output credentials (for scripts)")
|
||||||
|
print(" export - Output as bash export commands")
|
||||||
@ -0,0 +1 @@
|
|||||||
|
2001609
|
||||||
@ -0,0 +1,984 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Git Pusher - Main Server
|
||||||
|
Serveur HTTP pour pousser les applications Splunk vers Git
|
||||||
|
et déployer vers le Search Head Cluster via le SH Deployer
|
||||||
|
|
||||||
|
Avec système de licence par fichier .lic
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import ssl
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
from datetime import datetime
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
|
# Importer le validateur de licence
|
||||||
|
# En production, ce fichier sera dans le même dossier
|
||||||
|
try:
|
||||||
|
from license_validator import (
|
||||||
|
validate_license,
|
||||||
|
save_license_file,
|
||||||
|
check_limits,
|
||||||
|
increment_usage,
|
||||||
|
get_splunk_hostname,
|
||||||
|
get_usage_stats,
|
||||||
|
parse_license_content
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
# Fallback pour le développement
|
||||||
|
print("Warning: license_validator not found, running without license checks")
|
||||||
|
def validate_license(): return {"valid": True, "type": "dev", "days_remaining": 999}
|
||||||
|
def save_license_file(c): return {"success": True}
|
||||||
|
def check_limits(): return {"allowed": True}
|
||||||
|
def increment_usage(): return {}
|
||||||
|
def get_splunk_hostname(): return "dev-host"
|
||||||
|
def get_usage_stats(): return {}
|
||||||
|
def parse_license_content(c): return {}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# CONFIGURATION
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Chemins Splunk
|
||||||
|
SPLUNK_HOME = os.environ.get('SPLUNK_HOME', '/opt/splunk')
|
||||||
|
APP_HOME = os.path.join(SPLUNK_HOME, 'etc', 'apps', 'pusher_app_prem')
|
||||||
|
CONFIG_FILE = os.path.join(APP_HOME, 'local', 'config.json')
|
||||||
|
|
||||||
|
# Configuration par défaut
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
"api": {
|
||||||
|
"url": "",
|
||||||
|
"port": 9999,
|
||||||
|
"useProxy": True
|
||||||
|
},
|
||||||
|
"deployer": {
|
||||||
|
"enabled": False,
|
||||||
|
"host": "",
|
||||||
|
"port": 9998,
|
||||||
|
"token": "",
|
||||||
|
"useSSL": True
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"checkInterval": 24
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"logLevel": "INFO",
|
||||||
|
"timeout": 30,
|
||||||
|
"gitTimeout": 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
"""Charger la configuration depuis le fichier"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(CONFIG_FILE):
|
||||||
|
with open(CONFIG_FILE, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
# Fusionner avec la config par défaut pour les clés manquantes
|
||||||
|
return {**DEFAULT_CONFIG, **config}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur chargement config: {e}")
|
||||||
|
return DEFAULT_CONFIG.copy()
|
||||||
|
|
||||||
|
def save_config(config):
|
||||||
|
"""Sauvegarder la configuration dans le fichier"""
|
||||||
|
try:
|
||||||
|
local_dir = os.path.join(APP_HOME, 'local')
|
||||||
|
os.makedirs(local_dir, exist_ok=True)
|
||||||
|
|
||||||
|
with open(CONFIG_FILE, 'w') as f:
|
||||||
|
json.dump(config, f, indent=2)
|
||||||
|
|
||||||
|
os.chmod(CONFIG_FILE, 0o600)
|
||||||
|
logger.info(f"Configuration sauvegardée: {CONFIG_FILE}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur sauvegarde config: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Charger la configuration au démarrage
|
||||||
|
APP_CONFIG = load_config()
|
||||||
|
|
||||||
|
# Configuration du SH Deployer (depuis la config ou valeurs par défaut)
|
||||||
|
SH_DEPLOYER_CONFIG = {
|
||||||
|
"enabled": APP_CONFIG.get("deployer", {}).get("enabled", False),
|
||||||
|
"host": APP_CONFIG.get("deployer", {}).get("host", ""),
|
||||||
|
"port": APP_CONFIG.get("deployer", {}).get("port", 9998),
|
||||||
|
"use_ssl": APP_CONFIG.get("deployer", {}).get("useSSL", True),
|
||||||
|
"token": APP_CONFIG.get("deployer", {}).get("token", ""),
|
||||||
|
"timeout": APP_CONFIG.get("advanced", {}).get("timeout", 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configuration du logging
|
||||||
|
log_dir = '/opt/splunk/var/log/splunk'
|
||||||
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
|
||||||
|
log_level = getattr(logging, APP_CONFIG.get("advanced", {}).get("logLevel", "INFO"), logging.INFO)
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=log_level,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(os.path.join(log_dir, 'git_pusher.log')),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger('git_pusher')
|
||||||
|
|
||||||
|
|
||||||
|
class GitPusherRequestHandler(BaseHTTPRequestHandler):
|
||||||
|
"""Handler pour les requêtes HTTP"""
|
||||||
|
|
||||||
|
def send_cors_headers(self):
|
||||||
|
"""Envoyer les headers CORS complets"""
|
||||||
|
# Permettre toutes les origines
|
||||||
|
origin = self.headers.get('Origin', '*')
|
||||||
|
self.send_header('Access-Control-Allow-Origin', origin)
|
||||||
|
self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT, DELETE')
|
||||||
|
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin, X-Splunk-Form-Key, X-Auth-Token')
|
||||||
|
self.send_header('Access-Control-Allow-Credentials', 'true')
|
||||||
|
self.send_header('Access-Control-Max-Age', '86400') # Cache preflight 24h
|
||||||
|
|
||||||
|
def do_OPTIONS(self):
|
||||||
|
"""Traiter les requêtes OPTIONS (CORS preflight)"""
|
||||||
|
logger.info(f"OPTIONS request from {self.headers.get('Origin', 'unknown')}")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
# Important: ne rien écrire dans le body pour OPTIONS
|
||||||
|
return
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
"""Traiter les requêtes GET"""
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.send_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed_url = urlparse(self.path)
|
||||||
|
path = parsed_url.path
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ENDPOINTS LICENCE
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
if path == '/license' or path == '/license/status':
|
||||||
|
# Récupérer le statut de la licence
|
||||||
|
validation = validate_license()
|
||||||
|
usage = get_usage_stats()
|
||||||
|
hostname = get_splunk_hostname()
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"status": "valid" if validation.get("valid") else "invalid",
|
||||||
|
"hostname": hostname,
|
||||||
|
"license": validation if validation.get("valid") else None,
|
||||||
|
"error": validation.get("error") if not validation.get("valid") else None,
|
||||||
|
"error_code": validation.get("error_code") if not validation.get("valid") else None,
|
||||||
|
"usage": usage
|
||||||
|
}
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
|
||||||
|
elif path == '/license/hostname':
|
||||||
|
# Juste le hostname
|
||||||
|
response = {"hostname": get_splunk_hostname()}
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
|
||||||
|
elif path == '/license/file':
|
||||||
|
# Charger la licence depuis le fichier sur le serveur
|
||||||
|
license_file = os.path.join(APP_HOME, 'local', 'license.lic')
|
||||||
|
|
||||||
|
if os.path.exists(license_file):
|
||||||
|
try:
|
||||||
|
with open(license_file, 'r') as f:
|
||||||
|
license_content = f.read()
|
||||||
|
response = {
|
||||||
|
"success": True,
|
||||||
|
"content": license_content
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
response = {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Erreur lecture fichier: {str(e)}"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
response = {
|
||||||
|
"success": False,
|
||||||
|
"error": "Aucun fichier de licence sur le serveur"
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ENDPOINT CONFIGURATION
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
elif path == '/config':
|
||||||
|
# Retourner la configuration actuelle
|
||||||
|
config = load_config()
|
||||||
|
# Masquer le token pour la sécurité
|
||||||
|
if 'deployer' in config and 'token' in config['deployer']:
|
||||||
|
config['deployer']['token'] = '***' if config['deployer']['token'] else ''
|
||||||
|
self.wfile.write(json.dumps(config).encode())
|
||||||
|
|
||||||
|
elif path == '/health':
|
||||||
|
# Health check
|
||||||
|
response = {
|
||||||
|
"status": "ok",
|
||||||
|
"service": "git_pusher",
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"sh_deployer": {
|
||||||
|
"enabled": SH_DEPLOYER_CONFIG.get("enabled", True),
|
||||||
|
"host": SH_DEPLOYER_CONFIG.get("host"),
|
||||||
|
"port": SH_DEPLOYER_CONFIG.get("port")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ENDPOINTS SH DEPLOYER
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
elif path == '/deployer/health':
|
||||||
|
# Vérifier la santé du SH Deployer
|
||||||
|
result = call_deployer_agent("/health")
|
||||||
|
if result.get("success"):
|
||||||
|
response = {
|
||||||
|
"status": "ok",
|
||||||
|
"deployer": result.get("data"),
|
||||||
|
"config": {
|
||||||
|
"host": SH_DEPLOYER_CONFIG.get("host"),
|
||||||
|
"port": SH_DEPLOYER_CONFIG.get("port")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
response = {
|
||||||
|
"status": "error",
|
||||||
|
"error": result.get("error"),
|
||||||
|
"config": {
|
||||||
|
"host": SH_DEPLOYER_CONFIG.get("host"),
|
||||||
|
"port": SH_DEPLOYER_CONFIG.get("port")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
|
||||||
|
elif path == '/deployer/status':
|
||||||
|
# Statut du SH Deployer
|
||||||
|
result = get_deployer_status()
|
||||||
|
self.wfile.write(json.dumps(result).encode())
|
||||||
|
|
||||||
|
elif path == '/deployer/config':
|
||||||
|
# Configuration actuelle du SH Deployer
|
||||||
|
response = {
|
||||||
|
"enabled": SH_DEPLOYER_CONFIG.get("enabled", True),
|
||||||
|
"host": SH_DEPLOYER_CONFIG.get("host"),
|
||||||
|
"port": SH_DEPLOYER_CONFIG.get("port"),
|
||||||
|
"use_ssl": SH_DEPLOYER_CONFIG.get("use_ssl", True)
|
||||||
|
}
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
|
||||||
|
else:
|
||||||
|
response = {"error": "Unknown endpoint", "path": path}
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"GET error: {e}")
|
||||||
|
self.wfile.write(json.dumps({"error": str(e)}).encode())
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
"""Traiter les requêtes POST"""
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.send_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed_url = urlparse(self.path)
|
||||||
|
path = parsed_url.path
|
||||||
|
query_params = parse_qs(parsed_url.query)
|
||||||
|
|
||||||
|
logger.info(f"POST request to {path}")
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ENDPOINT CONFIGURATION
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
if path == '/config':
|
||||||
|
# Sauvegarder la configuration
|
||||||
|
content_length = int(self.headers.get('Content-Length', 0))
|
||||||
|
body = self.rfile.read(content_length).decode('utf-8')
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_config = json.loads(body)
|
||||||
|
|
||||||
|
# Charger la config existante pour préserver le token si masqué
|
||||||
|
existing_config = load_config()
|
||||||
|
|
||||||
|
# Si le token est masqué (***), garder l'ancien
|
||||||
|
if new_config.get('deployer', {}).get('token') == '***':
|
||||||
|
new_config['deployer']['token'] = existing_config.get('deployer', {}).get('token', '')
|
||||||
|
|
||||||
|
# Sauvegarder
|
||||||
|
if save_config(new_config):
|
||||||
|
# Recharger la config globale
|
||||||
|
global APP_CONFIG, SH_DEPLOYER_CONFIG
|
||||||
|
APP_CONFIG = load_config()
|
||||||
|
SH_DEPLOYER_CONFIG = {
|
||||||
|
"enabled": APP_CONFIG.get("deployer", {}).get("enabled", False),
|
||||||
|
"host": APP_CONFIG.get("deployer", {}).get("host", ""),
|
||||||
|
"port": APP_CONFIG.get("deployer", {}).get("port", 9998),
|
||||||
|
"use_ssl": APP_CONFIG.get("deployer", {}).get("useSSL", True),
|
||||||
|
"token": APP_CONFIG.get("deployer", {}).get("token", ""),
|
||||||
|
"timeout": APP_CONFIG.get("advanced", {}).get("timeout", 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
response = {"success": True, "message": "Configuration sauvegardée"}
|
||||||
|
else:
|
||||||
|
response = {"success": False, "error": "Erreur lors de la sauvegarde"}
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
response = {"success": False, "error": f"JSON invalide: {str(e)}"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur sauvegarde config: {e}")
|
||||||
|
response = {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ENDPOINTS LICENCE
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
elif path == '/license/upload' or path == '/license/save':
|
||||||
|
# Sauvegarder la licence sur le serveur (fichier)
|
||||||
|
# La validation RSA est faite côté client
|
||||||
|
content_length = int(self.headers.get('Content-Length', 0))
|
||||||
|
body = self.rfile.read(content_length).decode('utf-8')
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(body)
|
||||||
|
license_content = data.get('license_content', '')
|
||||||
|
except:
|
||||||
|
license_content = body
|
||||||
|
|
||||||
|
if not license_content:
|
||||||
|
response = {"success": False, "error": "Contenu de licence vide"}
|
||||||
|
else:
|
||||||
|
# Sauvegarder le fichier de licence
|
||||||
|
try:
|
||||||
|
local_dir = os.path.join(APP_HOME, 'local')
|
||||||
|
os.makedirs(local_dir, exist_ok=True)
|
||||||
|
|
||||||
|
license_path = os.path.join(local_dir, 'license.lic')
|
||||||
|
|
||||||
|
with open(license_path, 'w') as f:
|
||||||
|
f.write(license_content)
|
||||||
|
|
||||||
|
os.chmod(license_path, 0o600)
|
||||||
|
|
||||||
|
logger.info(f"Licence sauvegardée: {license_path}")
|
||||||
|
response = {
|
||||||
|
"success": True,
|
||||||
|
"message": "Licence sauvegardée sur le serveur",
|
||||||
|
"path": license_path
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur sauvegarde licence: {e}")
|
||||||
|
response = {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Erreur sauvegarde: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
return
|
||||||
|
|
||||||
|
elif path == '/license/delete':
|
||||||
|
# Supprimer la licence
|
||||||
|
license_path = os.path.join(APP_HOME, 'local', 'license.lic')
|
||||||
|
if os.path.exists(license_path):
|
||||||
|
os.remove(license_path)
|
||||||
|
logger.info(f"Licence supprimée: {license_path}")
|
||||||
|
response = {"success": True, "message": "Licence supprimée"}
|
||||||
|
else:
|
||||||
|
response = {"success": False, "error": "Aucune licence à supprimer"}
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ENDPOINT PUSH GIT
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
elif path == '/push' or path.startswith('/services/'):
|
||||||
|
# NOTE: La vérification de licence est maintenant faite côté client (JavaScript)
|
||||||
|
# avec validation RSA. Le serveur fait confiance au client.
|
||||||
|
# Si vous voulez réactiver la vérification serveur, décommentez le bloc ci-dessous:
|
||||||
|
#
|
||||||
|
# license_check = check_limits()
|
||||||
|
# if not license_check.get("allowed"):
|
||||||
|
# response = {
|
||||||
|
# "status": "error",
|
||||||
|
# "error_code": "LICENSE_ERROR",
|
||||||
|
# "message": license_check.get("error", "Licence invalide ou limite atteinte")
|
||||||
|
# }
|
||||||
|
# self.wfile.write(json.dumps(response).encode())
|
||||||
|
# return
|
||||||
|
|
||||||
|
# Traiter le push Git
|
||||||
|
self.handle_git_push(query_params)
|
||||||
|
return
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Traiter comme un push Git (compatibilité)
|
||||||
|
self.handle_git_push(query_params)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"POST error: {str(e)}", exc_info=True)
|
||||||
|
response = {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Error: {str(e)}"
|
||||||
|
}
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
|
||||||
|
def handle_git_push(self, query_params):
|
||||||
|
"""Gérer le push Git et optionnellement le déploiement vers SH Cluster"""
|
||||||
|
try:
|
||||||
|
# Extraire les paramètres
|
||||||
|
git_url = query_params.get('git_url', [''])[0]
|
||||||
|
git_branch = query_params.get('git_branch', ['main'])[0]
|
||||||
|
git_token = query_params.get('git_token', [''])[0]
|
||||||
|
commit_message = query_params.get('commit_message', [''])[0]
|
||||||
|
apps_json = query_params.get('apps', query_params.get('dashboards', ['[]']))[0]
|
||||||
|
shcluster_apps_json = query_params.get('shcluster_apps', ['[]'])[0] # Apps pour le SH Cluster
|
||||||
|
user = query_params.get('user', ['unknown'])[0]
|
||||||
|
|
||||||
|
# Paramètres pour le déploiement SH Cluster
|
||||||
|
deploy_to_shcluster = query_params.get('deploy_to_shcluster', ['false'])[0].lower() == 'true'
|
||||||
|
deployer_host = query_params.get('deployer_host', [SH_DEPLOYER_CONFIG.get('host', '')])[0]
|
||||||
|
deployer_token = query_params.get('deployer_token', [SH_DEPLOYER_CONFIG.get('token', '')])[0]
|
||||||
|
sh_auth_user = query_params.get('sh_auth_user', [''])[0]
|
||||||
|
sh_auth_pass = query_params.get('sh_auth_pass', [''])[0]
|
||||||
|
|
||||||
|
# Paramètres de licence (envoyés par le client)
|
||||||
|
license_type = query_params.get('license_type', [''])[0]
|
||||||
|
license_id = query_params.get('license_id', [''])[0]
|
||||||
|
|
||||||
|
logger.info(f"Parameters: git_url={git_url}, branch={git_branch}, user={user}, deploy_to_shcluster={deploy_to_shcluster}")
|
||||||
|
|
||||||
|
# Parser les apps pour Git
|
||||||
|
try:
|
||||||
|
apps = json.loads(apps_json) if isinstance(apps_json, str) else apps_json
|
||||||
|
except (json.JSONDecodeError, TypeError) as e:
|
||||||
|
logger.error(f"JSON parse error for apps: {e}")
|
||||||
|
apps = []
|
||||||
|
|
||||||
|
# Parser les apps pour SH Cluster
|
||||||
|
try:
|
||||||
|
shcluster_apps = json.loads(shcluster_apps_json) if isinstance(shcluster_apps_json, str) else shcluster_apps_json
|
||||||
|
except (json.JSONDecodeError, TypeError) as e:
|
||||||
|
logger.error(f"JSON parse error for shcluster_apps: {e}")
|
||||||
|
shcluster_apps = apps # Fallback: utiliser toutes les apps
|
||||||
|
|
||||||
|
logger.info(f"Apps for Git: {len(apps)}, Apps for SH Cluster: {len(shcluster_apps)}")
|
||||||
|
|
||||||
|
# NOTE: La vérification des limites est maintenant faite côté client
|
||||||
|
# Le serveur fait confiance aux informations envoyées par le client
|
||||||
|
|
||||||
|
# Valider les paramètres
|
||||||
|
if not git_url or not git_token or not commit_message or not apps:
|
||||||
|
response = {
|
||||||
|
"status": "error",
|
||||||
|
"message": "Missing required parameters"
|
||||||
|
}
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
return
|
||||||
|
|
||||||
|
# Créer un répertoire temporaire
|
||||||
|
temp_dir = tempfile.mkdtemp(prefix='splunk_git_')
|
||||||
|
logger.info(f"Created temp directory: {temp_dir}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Préparer l'URL Git avec le token
|
||||||
|
git_url_with_token = self.prepare_git_url(git_url, git_token)
|
||||||
|
|
||||||
|
logger.info("Cloning repository...")
|
||||||
|
self.clone_repository(temp_dir, git_url_with_token, git_branch)
|
||||||
|
|
||||||
|
# Récupérer les applications
|
||||||
|
logger.info("Fetching applications from Splunk...")
|
||||||
|
app_directories = self.fetch_apps_directories(apps)
|
||||||
|
|
||||||
|
# Créer le dossier apps
|
||||||
|
apps_dir = os.path.join(temp_dir, 'apps')
|
||||||
|
os.makedirs(apps_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Copier les applications
|
||||||
|
logger.info("Copying applications to repository...")
|
||||||
|
apps_copied = 0
|
||||||
|
for app_data in app_directories:
|
||||||
|
app_name = app_data['name']
|
||||||
|
app_path = app_data['path']
|
||||||
|
dest_path = os.path.join(apps_dir, app_name)
|
||||||
|
|
||||||
|
if os.path.exists(app_path):
|
||||||
|
# Supprimer l'ancienne version si elle existe
|
||||||
|
if os.path.exists(dest_path):
|
||||||
|
logger.info(f"Removing old version of {app_name}")
|
||||||
|
shutil.rmtree(dest_path)
|
||||||
|
|
||||||
|
# Copier la nouvelle version
|
||||||
|
shutil.copytree(app_path, dest_path)
|
||||||
|
apps_copied += 1
|
||||||
|
|
||||||
|
# Compter les fichiers copiés
|
||||||
|
file_count = sum(len(files) for _, _, files in os.walk(dest_path))
|
||||||
|
logger.info(f"Copied app: {app_name} ({file_count} files)")
|
||||||
|
else:
|
||||||
|
logger.warning(f"App path not found: {app_path}")
|
||||||
|
|
||||||
|
logger.info(f"Total apps copied: {apps_copied}")
|
||||||
|
|
||||||
|
# Configurer git
|
||||||
|
subprocess.run(['git', 'config', 'user.email', 'splunk@splunk.local'],
|
||||||
|
cwd=temp_dir, capture_output=True)
|
||||||
|
subprocess.run(['git', 'config', 'user.name', 'Splunk Git Pusher'],
|
||||||
|
cwd=temp_dir, capture_output=True)
|
||||||
|
|
||||||
|
# Ajouter TOUS les fichiers (y compris les suppressions)
|
||||||
|
subprocess.run(['git', 'add', '--all'], cwd=temp_dir, capture_output=True)
|
||||||
|
|
||||||
|
# Afficher le statut Git pour debug
|
||||||
|
status_check = subprocess.run(['git', 'status', '--short'],
|
||||||
|
cwd=temp_dir, capture_output=True, text=True)
|
||||||
|
if status_check.stdout.strip():
|
||||||
|
logger.info(f"Git changes detected:\n{status_check.stdout[:500]}")
|
||||||
|
else:
|
||||||
|
logger.info("No Git changes detected")
|
||||||
|
|
||||||
|
# Message de commit avec infos de licence (envoyées par le client)
|
||||||
|
full_message = f"{commit_message}\n\n"
|
||||||
|
full_message += f"Pushed by: {user}\n"
|
||||||
|
full_message += f"License: {license_id or 'N/A'} ({license_type or 'N/A'})\n"
|
||||||
|
full_message += f"Timestamp: {datetime.now().isoformat()}"
|
||||||
|
|
||||||
|
# Vérifier s'il y a des changements à committer
|
||||||
|
status_result = subprocess.run(['git', 'status', '--porcelain'],
|
||||||
|
cwd=temp_dir, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if status_result.stdout.strip():
|
||||||
|
# Il y a des changements, faire le commit
|
||||||
|
result = subprocess.run(['git', 'commit', '-m', full_message],
|
||||||
|
cwd=temp_dir, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.warning(f"Commit warning: {result.stderr}")
|
||||||
|
else:
|
||||||
|
logger.info("Commit created successfully")
|
||||||
|
else:
|
||||||
|
logger.info("No changes detected, skipping commit")
|
||||||
|
|
||||||
|
logger.info("Pushing to Git...")
|
||||||
|
result = subprocess.run(['git', 'push', 'origin', git_branch],
|
||||||
|
cwd=temp_dir, capture_output=True, text=True, timeout=60)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
# Vérifier si c'est juste "Everything up-to-date"
|
||||||
|
if "Everything up-to-date" in result.stderr or "Everything up-to-date" in result.stdout:
|
||||||
|
logger.info("Repository is already up-to-date")
|
||||||
|
else:
|
||||||
|
raise Exception(f"Push failed: {result.stderr}")
|
||||||
|
|
||||||
|
# NOTE: L'incrémentation des stats est maintenant faite côté client (JavaScript)
|
||||||
|
|
||||||
|
logger.info("Git push successful!")
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# DÉPLOIEMENT VERS SH CLUSTER (optionnel)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
deployer_result = None
|
||||||
|
shcluster_apps_deployed = 0
|
||||||
|
|
||||||
|
if deploy_to_shcluster:
|
||||||
|
logger.info("Triggering deployment to SH Cluster...")
|
||||||
|
|
||||||
|
# Extraire les IDs des apps à déployer sur le SH Cluster
|
||||||
|
shcluster_app_ids = [app.get('id') or app.get('name') for app in shcluster_apps]
|
||||||
|
logger.info(f"Apps to deploy to SH Cluster: {shcluster_app_ids}")
|
||||||
|
|
||||||
|
shcluster_apps_deployed = len(shcluster_app_ids)
|
||||||
|
|
||||||
|
# Configurer le deployer
|
||||||
|
deployer_config = SH_DEPLOYER_CONFIG.copy()
|
||||||
|
if deployer_host:
|
||||||
|
deployer_config["host"] = deployer_host
|
||||||
|
if deployer_token:
|
||||||
|
deployer_config["token"] = deployer_token
|
||||||
|
|
||||||
|
# Appeler le SH Deployer pour pull + deploy
|
||||||
|
# Passer la liste des apps à déployer
|
||||||
|
deployer_result = trigger_deployer_pull_and_deploy(
|
||||||
|
git_url=git_url,
|
||||||
|
git_token=git_token,
|
||||||
|
auth_user=sh_auth_user if sh_auth_user else None,
|
||||||
|
auth_pass=sh_auth_pass if sh_auth_pass else None,
|
||||||
|
config=deployer_config,
|
||||||
|
apps_to_deploy=shcluster_app_ids # Liste des apps à déployer
|
||||||
|
)
|
||||||
|
|
||||||
|
if deployer_result.get("success"):
|
||||||
|
logger.info(f"SH Cluster deployment triggered successfully for {shcluster_apps_deployed} apps")
|
||||||
|
else:
|
||||||
|
logger.error(f"SH Cluster deployment failed: {deployer_result.get('error')}")
|
||||||
|
|
||||||
|
# Préparer la réponse
|
||||||
|
response = {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Successfully pushed {len(app_directories)} application(s) to Git",
|
||||||
|
"apps_pushed": len(app_directories),
|
||||||
|
"license_type": license_type or "N/A"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ajouter les infos de déploiement si activé
|
||||||
|
if deploy_to_shcluster:
|
||||||
|
response["shcluster_deployment"] = {
|
||||||
|
"triggered": True,
|
||||||
|
"apps_count": shcluster_apps_deployed,
|
||||||
|
"success": deployer_result.get("success", False) if deployer_result else False,
|
||||||
|
"message": deployer_result.get("data", {}).get("message") if deployer_result and deployer_result.get("success") else deployer_result.get("error") if deployer_result else "Not triggered"
|
||||||
|
}
|
||||||
|
|
||||||
|
if deployer_result and deployer_result.get("success"):
|
||||||
|
response["message"] += f" and triggered SH Cluster deployment ({shcluster_apps_deployed} apps)"
|
||||||
|
else:
|
||||||
|
response["message"] += " (SH Cluster deployment failed)"
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
|
||||||
|
finally:
|
||||||
|
logger.info(f"Cleaning up {temp_dir}")
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Git push error: {str(e)}", exc_info=True)
|
||||||
|
response = {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Error: {str(e)}"
|
||||||
|
}
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
logger.debug(format % args)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def prepare_git_url(git_url, token):
|
||||||
|
"""Préparer l'URL Git avec le token"""
|
||||||
|
if '@' in git_url:
|
||||||
|
protocol = git_url.split('://')[0]
|
||||||
|
rest = git_url.split('://', 1)[1]
|
||||||
|
host_and_path = rest.split('@', 1)[1] if '@' in rest else rest
|
||||||
|
return f"{protocol}://{token}@{host_and_path}"
|
||||||
|
|
||||||
|
if git_url.startswith('https://') or git_url.startswith('http://'):
|
||||||
|
protocol = git_url.split('://')[0]
|
||||||
|
host_and_path = git_url.split('://', 1)[1]
|
||||||
|
return f"{protocol}://{token}@{host_and_path}"
|
||||||
|
|
||||||
|
return git_url
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clone_repository(dest_dir, git_url, branch):
|
||||||
|
"""Cloner le repository"""
|
||||||
|
try:
|
||||||
|
cmd = ['git', 'clone', '--depth', '1', '--branch', branch, git_url, dest_dir]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise Exception(f"Clone failed: {result.stderr}")
|
||||||
|
|
||||||
|
logger.info("Repository cloned successfully")
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise Exception("Git clone operation timed out")
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise Exception("Git is not installed on this system")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fetch_apps_directories(apps):
|
||||||
|
"""Récupérer les dossiers des applications"""
|
||||||
|
logger.info(f"Fetching directories for {len(apps)} applications")
|
||||||
|
|
||||||
|
splunk_home = '/opt/splunk'
|
||||||
|
apps_base_path = os.path.join(splunk_home, 'etc', 'apps')
|
||||||
|
|
||||||
|
app_directories = []
|
||||||
|
|
||||||
|
for app in apps:
|
||||||
|
app_id = app.get('id') or app.get('app_id')
|
||||||
|
app_path = os.path.join(apps_base_path, app_id)
|
||||||
|
|
||||||
|
if os.path.isdir(app_path):
|
||||||
|
app_directories.append({
|
||||||
|
'name': app_id,
|
||||||
|
'path': app_path,
|
||||||
|
'size': sum(os.path.getsize(os.path.join(dirpath, filename))
|
||||||
|
for dirpath, dirnames, filenames in os.walk(app_path)
|
||||||
|
for filename in filenames)
|
||||||
|
})
|
||||||
|
logger.info(f"Found app: {app_id}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"App directory not found: {app_path}")
|
||||||
|
|
||||||
|
return app_directories
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# FONCTIONS SH DEPLOYER
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def call_deployer_agent(endpoint, method="GET", data=None, config=None):
|
||||||
|
"""
|
||||||
|
Appeler l'agent SH Deployer
|
||||||
|
|
||||||
|
Args:
|
||||||
|
endpoint: Endpoint à appeler (ex: /health, /pull, /deploy)
|
||||||
|
method: GET ou POST
|
||||||
|
data: Données à envoyer (dict)
|
||||||
|
config: Configuration (override SH_DEPLOYER_CONFIG)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict avec success, data ou error
|
||||||
|
"""
|
||||||
|
if config is None:
|
||||||
|
config = SH_DEPLOYER_CONFIG
|
||||||
|
|
||||||
|
if not config.get("enabled", True):
|
||||||
|
return {"success": False, "error": "SH Deployer is disabled"}
|
||||||
|
|
||||||
|
host = config.get("host", "10.10.40.14")
|
||||||
|
port = config.get("port", 9998)
|
||||||
|
use_ssl = config.get("use_ssl", True)
|
||||||
|
token = config.get("token", "")
|
||||||
|
timeout = config.get("timeout", 30)
|
||||||
|
|
||||||
|
protocol = "https" if use_ssl else "http"
|
||||||
|
|
||||||
|
# Si c'est un nom de domaine (commence par une lettre et contient un point)
|
||||||
|
# Ne pas ajouter le port (le proxy gère)
|
||||||
|
import re
|
||||||
|
is_domain = bool(re.match(r'^[a-zA-Z]', host)) and '.' in host and not re.match(r'^(\d{1,3}\.){3}\d{1,3}$', host)
|
||||||
|
|
||||||
|
if is_domain:
|
||||||
|
url = f"{protocol}://{host}{endpoint}"
|
||||||
|
else:
|
||||||
|
url = f"{protocol}://{host}:{port}{endpoint}"
|
||||||
|
|
||||||
|
logger.info(f"Calling SH Deployer: {method} {url}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Créer le contexte SSL (ignorer les certificats auto-signés)
|
||||||
|
ssl_context = ssl.create_default_context()
|
||||||
|
ssl_context.check_hostname = False
|
||||||
|
ssl_context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
# Préparer les données
|
||||||
|
if data:
|
||||||
|
json_data = json.dumps(data).encode('utf-8')
|
||||||
|
else:
|
||||||
|
json_data = None
|
||||||
|
|
||||||
|
# Créer la requête
|
||||||
|
req = urllib.request.Request(url, data=json_data, method=method)
|
||||||
|
req.add_header('Content-Type', 'application/json')
|
||||||
|
req.add_header('X-Auth-Token', token)
|
||||||
|
|
||||||
|
# Exécuter la requête
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout, context=ssl_context) as response:
|
||||||
|
response_data = json.loads(response.read().decode('utf-8'))
|
||||||
|
logger.info(f"SH Deployer response: {response_data}")
|
||||||
|
return {"success": True, "data": response_data}
|
||||||
|
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
error_body = e.read().decode('utf-8') if e.fp else str(e)
|
||||||
|
logger.error(f"SH Deployer HTTP error {e.code}: {error_body}")
|
||||||
|
return {"success": False, "error": f"HTTP {e.code}: {error_body}"}
|
||||||
|
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
logger.error(f"SH Deployer connection error: {e.reason}")
|
||||||
|
return {"success": False, "error": f"Connection error: {e.reason}"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"SH Deployer error: {str(e)}")
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def check_deployer_health(config=None):
|
||||||
|
"""Vérifier si l'agent SH Deployer est accessible"""
|
||||||
|
result = call_deployer_agent("/health", config=config)
|
||||||
|
return result.get("success", False)
|
||||||
|
|
||||||
|
|
||||||
|
def trigger_deployer_pull(git_url, git_token, config=None):
|
||||||
|
"""
|
||||||
|
Déclencher un pull sur le SH Deployer
|
||||||
|
|
||||||
|
Args:
|
||||||
|
git_url: URL du repository Git
|
||||||
|
git_token: Token Git pour l'authentification
|
||||||
|
config: Configuration du deployer
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"repo_url": git_url,
|
||||||
|
"git_token": git_token,
|
||||||
|
"apps_subdir": "apps"
|
||||||
|
}
|
||||||
|
|
||||||
|
return call_deployer_agent("/pull", method="POST", data=data, config=config)
|
||||||
|
|
||||||
|
|
||||||
|
def trigger_deployer_deploy(target_uri=None, auth_user=None, auth_pass=None, config=None):
|
||||||
|
"""
|
||||||
|
Déclencher le déploiement du bundle sur le SH Cluster
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_uri: URI du captain du SH Cluster (optionnel)
|
||||||
|
auth_user: Utilisateur Splunk
|
||||||
|
auth_pass: Mot de passe Splunk
|
||||||
|
config: Configuration du deployer
|
||||||
|
"""
|
||||||
|
data = {}
|
||||||
|
if target_uri:
|
||||||
|
data["target_uri"] = target_uri
|
||||||
|
if auth_user:
|
||||||
|
data["auth_user"] = auth_user
|
||||||
|
if auth_pass:
|
||||||
|
data["auth_pass"] = auth_pass
|
||||||
|
|
||||||
|
return call_deployer_agent("/deploy", method="POST", data=data, config=config)
|
||||||
|
|
||||||
|
|
||||||
|
def trigger_deployer_pull_and_deploy(git_url, git_token, target_uri=None, auth_user=None, auth_pass=None, config=None, apps_to_deploy=None):
|
||||||
|
"""
|
||||||
|
Déclencher pull + deploy en une seule opération
|
||||||
|
|
||||||
|
Args:
|
||||||
|
git_url: URL du repo Git
|
||||||
|
git_token: Token d'accès Git
|
||||||
|
target_uri: URI cible pour le déploiement
|
||||||
|
auth_user: Utilisateur Splunk pour l'authentification
|
||||||
|
auth_pass: Mot de passe Splunk
|
||||||
|
config: Configuration du deployer
|
||||||
|
apps_to_deploy: Liste des IDs d'apps à déployer (si None, toutes les apps)
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"repo_url": git_url,
|
||||||
|
"git_token": git_token,
|
||||||
|
"apps_subdir": "apps"
|
||||||
|
}
|
||||||
|
if target_uri:
|
||||||
|
data["target_uri"] = target_uri
|
||||||
|
if auth_user:
|
||||||
|
data["auth_user"] = auth_user
|
||||||
|
if auth_pass:
|
||||||
|
data["auth_pass"] = auth_pass
|
||||||
|
if apps_to_deploy:
|
||||||
|
data["apps_to_deploy"] = apps_to_deploy # Liste des apps à déployer
|
||||||
|
|
||||||
|
return call_deployer_agent("/pull-and-deploy", method="POST", data=data, config=config)
|
||||||
|
|
||||||
|
|
||||||
|
def get_deployer_status(config=None):
|
||||||
|
"""Récupérer le statut du SH Deployer"""
|
||||||
|
return call_deployer_agent("/status", config=config)
|
||||||
|
|
||||||
|
|
||||||
|
def start_server(port=9999, use_ssl=True):
|
||||||
|
"""Démarrer le serveur HTTP/HTTPS"""
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
server = HTTPServer(('0.0.0.0', port), GitPusherRequestHandler)
|
||||||
|
|
||||||
|
ssl_enabled = False
|
||||||
|
|
||||||
|
if use_ssl:
|
||||||
|
# Chemins possibles pour les certificats (ordre de priorité)
|
||||||
|
cert_paths = [
|
||||||
|
# Certificats dédiés pour Git Pusher (recommandé)
|
||||||
|
('/opt/splunk/etc/apps/pusher_app_prem/local/certs/server.crt',
|
||||||
|
'/opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key'),
|
||||||
|
# Certificats splunkweb
|
||||||
|
('/opt/splunk/etc/auth/splunkweb/cert.pem',
|
||||||
|
'/opt/splunk/etc/auth/splunkweb/privkey.pem'),
|
||||||
|
# Autre emplacement splunkweb
|
||||||
|
('/opt/splunk/etc/auth/splunkweb/splunkweb.pem',
|
||||||
|
'/opt/splunk/etc/auth/splunkweb/splunkweb.key'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for cert_file, key_file in cert_paths:
|
||||||
|
logger.info(f"Trying SSL cert: {cert_file}")
|
||||||
|
if os.path.exists(cert_file) and os.path.exists(key_file):
|
||||||
|
try:
|
||||||
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
ssl_context.check_hostname = False
|
||||||
|
ssl_context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
# Charger le certificat et la clé
|
||||||
|
ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file)
|
||||||
|
|
||||||
|
server.socket = ssl_context.wrap_socket(server.socket, server_side=True)
|
||||||
|
ssl_enabled = True
|
||||||
|
logger.info(f"SSL enabled using: {cert_file}")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not load SSL cert {cert_file}: {e}")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
logger.debug(f"Cert not found: {cert_file} or {key_file}")
|
||||||
|
|
||||||
|
if not ssl_enabled:
|
||||||
|
logger.error("=" * 60)
|
||||||
|
logger.error("SSL CERTIFICATES NOT FOUND OR INVALID!")
|
||||||
|
logger.error("HTTPS requests from browser will fail!")
|
||||||
|
logger.error("")
|
||||||
|
logger.error("To fix, run these commands:")
|
||||||
|
logger.error(" mkdir -p /opt/splunk/etc/apps/pusher_app_prem/local/certs")
|
||||||
|
logger.error(" openssl req -x509 -newkey rsa:4096 \\")
|
||||||
|
logger.error(" -keyout /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key \\")
|
||||||
|
logger.error(" -out /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.crt \\")
|
||||||
|
logger.error(" -days 365 -nodes -subj \"/CN=git-pusher\"")
|
||||||
|
logger.error("=" * 60)
|
||||||
|
|
||||||
|
protocol = "HTTPS" if ssl_enabled else "HTTP"
|
||||||
|
logger.info(f"Git Pusher server listening on 0.0.0.0:{port} ({protocol})")
|
||||||
|
|
||||||
|
# Afficher le statut de la licence au démarrage
|
||||||
|
license_status = validate_license()
|
||||||
|
if license_status.get("valid"):
|
||||||
|
logger.info(f"License: {license_status.get('type_name')} - {license_status.get('days_remaining')} days remaining")
|
||||||
|
else:
|
||||||
|
logger.warning(f"License: {license_status.get('error', 'Invalid')}")
|
||||||
|
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='Git Pusher Server')
|
||||||
|
parser.add_argument('--no-ssl', action='store_true', help='Disable SSL/HTTPS')
|
||||||
|
parser.add_argument('--port', type=int, default=9999, help='Port number (default: 9999)')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
port = args.port
|
||||||
|
use_ssl = not args.no_ssl
|
||||||
|
|
||||||
|
logger.info(f"Starting Git Pusher on port {port} (SSL: {use_ssl})")
|
||||||
|
start_server(port, use_ssl)
|
||||||
@ -0,0 +1,191 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Git Pusher - License Endpoints
|
||||||
|
Endpoints REST pour gérer les licences
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
from http.server import BaseHTTPRequestHandler
|
||||||
|
|
||||||
|
# Ajouter le chemin du module
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
from license_validator import LicenseValidator, get_license_status, LICENSE_FILE_PATH
|
||||||
|
|
||||||
|
class LicenseHandler(BaseHTTPRequestHandler):
|
||||||
|
"""Handler pour les requêtes de licence"""
|
||||||
|
|
||||||
|
def do_OPTIONS(self):
|
||||||
|
"""Gérer les requêtes OPTIONS (CORS preflight)"""
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
"""Gérer les requêtes GET"""
|
||||||
|
if self.path == '/custom/git_pusher/license_status':
|
||||||
|
self.handle_license_status()
|
||||||
|
else:
|
||||||
|
self.send_error(404)
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
"""Gérer les requêtes POST"""
|
||||||
|
if self.path == '/custom/git_pusher/install_license':
|
||||||
|
self.handle_install_license()
|
||||||
|
elif self.path == '/custom/git_pusher/request_trial':
|
||||||
|
self.handle_request_trial()
|
||||||
|
else:
|
||||||
|
self.send_error(404)
|
||||||
|
|
||||||
|
def send_headers(self):
|
||||||
|
"""Envoyer les headers CORS"""
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.send_header('Access-Control-Allow-Origin', '*')
|
||||||
|
self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
|
||||||
|
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||||
|
|
||||||
|
def handle_license_status(self):
|
||||||
|
"""Vérifier le statut de la licence"""
|
||||||
|
try:
|
||||||
|
status = get_license_status()
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps(status).encode())
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.send_response(500)
|
||||||
|
self.send_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps({
|
||||||
|
'licensed': False,
|
||||||
|
'errors': [f'Erreur serveur: {str(e)}']
|
||||||
|
}).encode())
|
||||||
|
|
||||||
|
def handle_install_license(self):
|
||||||
|
"""Installer un fichier de licence uploadé"""
|
||||||
|
try:
|
||||||
|
# Lire le body
|
||||||
|
content_length = int(self.headers['Content-Length'])
|
||||||
|
body = self.rfile.read(content_length)
|
||||||
|
data = json.loads(body.decode())
|
||||||
|
|
||||||
|
license_content = data.get('license_file')
|
||||||
|
filename = data.get('filename', 'uploaded.lic')
|
||||||
|
|
||||||
|
if not license_content:
|
||||||
|
raise Exception("Aucun contenu de licence fourni")
|
||||||
|
|
||||||
|
# Créer un fichier temporaire
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.lic', delete=False) as temp_file:
|
||||||
|
temp_file.write(license_content)
|
||||||
|
temp_path = temp_file.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Valider et installer
|
||||||
|
validator = LicenseValidator()
|
||||||
|
result = validator.install_license(temp_path, LICENSE_FILE_PATH)
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps(result).encode())
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Nettoyer le fichier temporaire
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.remove(temp_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.send_response(400)
|
||||||
|
self.send_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps({
|
||||||
|
'success': False,
|
||||||
|
'message': f'Erreur: {str(e)}'
|
||||||
|
}).encode())
|
||||||
|
|
||||||
|
def handle_request_trial(self):
|
||||||
|
"""Créer une licence d'essai"""
|
||||||
|
try:
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import socket
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
# Générer une licence d'essai basique (7 jours)
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
trial_license = {
|
||||||
|
"license": {
|
||||||
|
"version": "1.0",
|
||||||
|
"license_id": "TRIAL-" + hashlib.md5(
|
||||||
|
f"{hostname}{datetime.now()}".encode()
|
||||||
|
).hexdigest()[:8].upper(),
|
||||||
|
"customer": {
|
||||||
|
"name": "Trial User",
|
||||||
|
"hostname": hostname
|
||||||
|
},
|
||||||
|
"validity": {
|
||||||
|
"issued": datetime.now().isoformat(),
|
||||||
|
"expires": (datetime.now() + timedelta(days=7)).isoformat(),
|
||||||
|
"days": 7
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
"max_pushes": 50,
|
||||||
|
"max_apps": None
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"git_push": True,
|
||||||
|
"multi_branch": False,
|
||||||
|
"auto_commit": False,
|
||||||
|
"webhooks": False
|
||||||
|
},
|
||||||
|
"type": "trial"
|
||||||
|
},
|
||||||
|
"signature": "TRIAL-NO-SIGNATURE",
|
||||||
|
"checksum": "TRIAL"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Créer le dossier si nécessaire
|
||||||
|
os.makedirs(os.path.dirname(LICENSE_FILE_PATH), exist_ok=True)
|
||||||
|
|
||||||
|
# Sauvegarder
|
||||||
|
with open(LICENSE_FILE_PATH, 'w') as f:
|
||||||
|
json.dump(trial_license, f, indent=2)
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Licence d\'essai créée (7 jours, 50 pushes max)',
|
||||||
|
'license_info': {
|
||||||
|
'license_id': trial_license['license']['license_id'],
|
||||||
|
'type': 'trial',
|
||||||
|
'expires': trial_license['license']['validity']['expires'],
|
||||||
|
'days_remaining': 7
|
||||||
|
}
|
||||||
|
}).encode())
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.send_response(500)
|
||||||
|
self.send_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps({
|
||||||
|
'success': False,
|
||||||
|
'message': f'Erreur: {str(e)}'
|
||||||
|
}).encode())
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
"""Override pour éviter les logs HTTP par défaut"""
|
||||||
|
pass
|
||||||
@ -0,0 +1,468 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Git Pusher - License Validator (RSA)
|
||||||
|
Validation des licences avec vérification de signature RSA
|
||||||
|
|
||||||
|
Ce fichier est distribué avec l'application client.
|
||||||
|
La clé publique peut être visible - seul le vendeur avec la clé privée peut signer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import socket
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# CLÉ PUBLIQUE RSA
|
||||||
|
# ============================================
|
||||||
|
# Cette clé est générée par le vendeur avec license_generator_rsa.py
|
||||||
|
# Commande: python license_generator_rsa.py export-key
|
||||||
|
#
|
||||||
|
# REMPLACEZ CETTE CLÉ PAR VOTRE CLÉ PUBLIQUE !
|
||||||
|
|
||||||
|
PUBLIC_KEY = '''-----BEGIN PUBLIC KEY-----
|
||||||
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnj2hOg61Q9k9iz4U5F7I
|
||||||
|
RdaJrpLTG+orz0/Kpbz2HSxbAVXkvL5GvYVfxROjy0UgxOZFycZAaGN2am+5CDHA
|
||||||
|
D1dTL9KCPhEaPqw4XTFnf6Ur5VG0+SftugTdTcyxRe614Z+i61/2ahk/vKG9D4kB
|
||||||
|
j4qV4se4lLk993lEaQrOXkXCbZ8royB5MPeOchPxZd7SDzoovEcyUmf2Fa2eYk6U
|
||||||
|
WmbrymCnJRsfxEVZofQQyp1ILS8KuSxaquXvMWm3cXV2Krs/3E5ax0vBPMrZRL+o
|
||||||
|
Vn7/dVnzbOlbifeosTYaad1DLd7NEgst3OFUv+dSH5hcCCc36IHMSvxcJ9l7s2kv
|
||||||
|
KbEFPeh582JNmRoMMNPRbd+/ZVDeJ/oB344+TtB6VeQ2GQyOyoggiLryZujg3WDE
|
||||||
|
shwLkFwiYGa/zEct0qs2/HBS1FOAqrLiPdQYJTx+RhrXhTni/p3H42L+2xJcbnki
|
||||||
|
fAgn8ND5k2yQw2gk4AlLqq2y01m0jsHdjaOfhKzzPLyzt1En/KAnCWJSzB+jur4z
|
||||||
|
fd70R4pLTYfawr2NTvTAhOtiOIjWj380oGmxeKCNJT1P4Dq8Yl+OxKLBy4cnUlgV
|
||||||
|
sbpKJug7Goth4g7bmCVMhC4bf7JB/iTrrS8DhMaWaZX/FeFloM3yYyo/gzmAY19z
|
||||||
|
sUdQuBpxCwIf/J7Q4dYsDkMCAwEAAQ==
|
||||||
|
-----END PUBLIC KEY-----'''
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# CONFIGURATION
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Chemin de l'application
|
||||||
|
APP_HOME = os.environ.get('SPLUNK_HOME', '/opt/splunk') + '/etc/apps/pusher_app_prem'
|
||||||
|
LICENSE_FILE = os.path.join(APP_HOME, 'local', 'license.lic')
|
||||||
|
USAGE_FILE = os.path.join(APP_HOME, 'local', '.usage_stats')
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# FONCTIONS UTILITAIRES
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def get_splunk_hostname():
|
||||||
|
"""Récupérer le hostname du serveur Splunk"""
|
||||||
|
try:
|
||||||
|
# Essayer via servername de Splunk
|
||||||
|
server_conf = os.path.join(
|
||||||
|
os.environ.get('SPLUNK_HOME', '/opt/splunk'),
|
||||||
|
'etc', 'system', 'local', 'server.conf'
|
||||||
|
)
|
||||||
|
if os.path.exists(server_conf):
|
||||||
|
with open(server_conf, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.strip().startswith('serverName'):
|
||||||
|
return line.split('=')[1].strip().lower()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback sur le hostname système
|
||||||
|
return socket.gethostname().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def load_public_key():
|
||||||
|
"""Charger la clé publique RSA"""
|
||||||
|
try:
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
|
public_key = serialization.load_pem_public_key(
|
||||||
|
PUBLIC_KEY.encode('utf-8'),
|
||||||
|
backend=default_backend()
|
||||||
|
)
|
||||||
|
return public_key
|
||||||
|
except ImportError:
|
||||||
|
# Fallback si cryptography n'est pas installé
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur chargement clé publique: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def verify_rsa_signature(data, signature, public_key):
|
||||||
|
"""Vérifier la signature RSA"""
|
||||||
|
try:
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
|
|
||||||
|
public_key.verify(
|
||||||
|
signature,
|
||||||
|
data,
|
||||||
|
padding.PSS(
|
||||||
|
mgf=padding.MGF1(hashes.SHA256()),
|
||||||
|
salt_length=padding.PSS.MAX_LENGTH
|
||||||
|
),
|
||||||
|
hashes.SHA256()
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# PARSING DU FICHIER .LIC
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def parse_license_file(filepath=None):
|
||||||
|
"""Parser un fichier de licence .lic"""
|
||||||
|
if filepath is None:
|
||||||
|
filepath = LICENSE_FILE
|
||||||
|
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
return parse_license_content(content)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lecture licence: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_license_content(content):
|
||||||
|
"""Parser le contenu d'une licence"""
|
||||||
|
try:
|
||||||
|
# Extraire le payload (ignorer les lignes de commentaires)
|
||||||
|
lines = content.strip().split('\n')
|
||||||
|
payload_b64 = None
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith('#'):
|
||||||
|
payload_b64 = line
|
||||||
|
break
|
||||||
|
|
||||||
|
if not payload_b64:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Décoder le payload
|
||||||
|
payload = json.loads(base64.b64decode(payload_b64).decode('utf-8'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"license_b64": payload.get("license"),
|
||||||
|
"signature_b64": payload.get("signature"),
|
||||||
|
"raw_payload": payload_b64
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur parsing licence: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# VALIDATION DE LA LICENCE
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def validate_license(filepath=None):
|
||||||
|
"""
|
||||||
|
Valider une licence
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict avec:
|
||||||
|
- valid: bool
|
||||||
|
- error: str (si invalide)
|
||||||
|
- error_code: str (si invalide)
|
||||||
|
- license_id, type, type_name, expires, days_remaining, limits, features (si valide)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Parser le fichier
|
||||||
|
parsed = parse_license_file(filepath)
|
||||||
|
|
||||||
|
if not parsed:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error": "Fichier de licence non trouvé ou invalide",
|
||||||
|
"error_code": "NO_LICENSE"
|
||||||
|
}
|
||||||
|
|
||||||
|
license_b64 = parsed.get("license_b64")
|
||||||
|
signature_b64 = parsed.get("signature_b64")
|
||||||
|
|
||||||
|
if not license_b64 or not signature_b64:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error": "Format de licence invalide",
|
||||||
|
"error_code": "INVALID_FORMAT"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Décoder la licence et la signature
|
||||||
|
license_json = base64.b64decode(license_b64).decode('utf-8')
|
||||||
|
signature = base64.b64decode(signature_b64)
|
||||||
|
|
||||||
|
# Charger la clé publique
|
||||||
|
public_key = load_public_key()
|
||||||
|
|
||||||
|
if public_key is None:
|
||||||
|
# Mode dégradé sans cryptography - vérification basique
|
||||||
|
print("⚠️ Module cryptography non installé - vérification basique uniquement")
|
||||||
|
else:
|
||||||
|
# Vérifier la signature RSA
|
||||||
|
if not verify_rsa_signature(license_json.encode('utf-8'), signature, public_key):
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error": "Signature de licence invalide",
|
||||||
|
"error_code": "INVALID_SIGNATURE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parser les données de licence
|
||||||
|
license_data = json.loads(license_json)
|
||||||
|
|
||||||
|
# Vérifier le hostname
|
||||||
|
expected_hostname = license_data.get("hostname", "").lower()
|
||||||
|
current_hostname = get_splunk_hostname()
|
||||||
|
|
||||||
|
if expected_hostname and expected_hostname != current_hostname:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error": f"Licence non valide pour ce serveur. Attendu: {expected_hostname}, Actuel: {current_hostname}",
|
||||||
|
"error_code": "HOSTNAME_MISMATCH"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vérifier la date d'expiration
|
||||||
|
expires_str = license_data.get("expires", "")
|
||||||
|
if expires_str:
|
||||||
|
try:
|
||||||
|
expires_date = datetime.strptime(expires_str, "%Y-%m-%d")
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
if now > expires_date:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error": f"Licence expirée le {expires_str}",
|
||||||
|
"error_code": "LICENSE_EXPIRED"
|
||||||
|
}
|
||||||
|
|
||||||
|
days_remaining = (expires_date - now).days
|
||||||
|
except ValueError:
|
||||||
|
days_remaining = 0
|
||||||
|
else:
|
||||||
|
days_remaining = 999 # Pas d'expiration
|
||||||
|
|
||||||
|
# Licence valide !
|
||||||
|
return {
|
||||||
|
"valid": True,
|
||||||
|
"license_id": license_data.get("license_id"),
|
||||||
|
"type": license_data.get("type"),
|
||||||
|
"type_name": license_data.get("type_name"),
|
||||||
|
"customer": license_data.get("customer", {}),
|
||||||
|
"hostname": expected_hostname,
|
||||||
|
"issued": license_data.get("issued"),
|
||||||
|
"expires": expires_str,
|
||||||
|
"days_remaining": days_remaining,
|
||||||
|
"limits": license_data.get("limits", {}),
|
||||||
|
"features": license_data.get("features", [])
|
||||||
|
}
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error": "Données de licence corrompues",
|
||||||
|
"error_code": "CORRUPTED_DATA"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error": f"Erreur de validation: {str(e)}",
|
||||||
|
"error_code": "VALIDATION_ERROR"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# SAUVEGARDE DE LICENCE
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def save_license_file(content):
|
||||||
|
"""Sauvegarder un fichier de licence uploadé"""
|
||||||
|
try:
|
||||||
|
# Créer le dossier local si nécessaire
|
||||||
|
local_dir = os.path.join(APP_HOME, 'local')
|
||||||
|
os.makedirs(local_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Valider le contenu avant de sauvegarder
|
||||||
|
parsed = parse_license_content(content)
|
||||||
|
if not parsed:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Format de licence invalide"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Valider la licence
|
||||||
|
# Sauvegarder temporairement pour valider
|
||||||
|
temp_file = LICENSE_FILE + '.tmp'
|
||||||
|
with open(temp_file, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
validation = validate_license(temp_file)
|
||||||
|
|
||||||
|
if not validation.get("valid"):
|
||||||
|
os.remove(temp_file)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": validation.get("error"),
|
||||||
|
"error_code": validation.get("error_code")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Renommer le fichier temporaire
|
||||||
|
os.rename(temp_file, LICENSE_FILE)
|
||||||
|
os.chmod(LICENSE_FILE, 0o600)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"license": validation
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# GESTION DES LIMITES D'UTILISATION
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def get_usage_stats():
|
||||||
|
"""Récupérer les statistiques d'utilisation"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(USAGE_FILE):
|
||||||
|
with open(USAGE_FILE, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_pushes": 0,
|
||||||
|
"pushes_today": 0,
|
||||||
|
"last_push_date": None,
|
||||||
|
"apps_pushed": []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def save_usage_stats(stats):
|
||||||
|
"""Sauvegarder les statistiques d'utilisation"""
|
||||||
|
try:
|
||||||
|
local_dir = os.path.join(APP_HOME, 'local')
|
||||||
|
os.makedirs(local_dir, exist_ok=True)
|
||||||
|
|
||||||
|
with open(USAGE_FILE, 'w') as f:
|
||||||
|
json.dump(stats, f, indent=2)
|
||||||
|
os.chmod(USAGE_FILE, 0o600)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur sauvegarde stats: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def increment_usage():
|
||||||
|
"""Incrémenter le compteur d'utilisation"""
|
||||||
|
stats = get_usage_stats()
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# Reset compteur quotidien si nouveau jour
|
||||||
|
if stats.get("last_push_date") != today:
|
||||||
|
stats["pushes_today"] = 0
|
||||||
|
stats["last_push_date"] = today
|
||||||
|
|
||||||
|
stats["total_pushes"] = stats.get("total_pushes", 0) + 1
|
||||||
|
stats["pushes_today"] = stats.get("pushes_today", 0) + 1
|
||||||
|
|
||||||
|
save_usage_stats(stats)
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def check_limits():
|
||||||
|
"""Vérifier si les limites de la licence sont respectées"""
|
||||||
|
license_info = validate_license()
|
||||||
|
|
||||||
|
if not license_info.get("valid"):
|
||||||
|
return {
|
||||||
|
"allowed": False,
|
||||||
|
"error": license_info.get("error"),
|
||||||
|
"error_code": license_info.get("error_code")
|
||||||
|
}
|
||||||
|
|
||||||
|
limits = license_info.get("limits", {})
|
||||||
|
max_pushes = limits.get("max_pushes_per_day", -1)
|
||||||
|
|
||||||
|
if max_pushes > 0:
|
||||||
|
stats = get_usage_stats()
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# Reset si nouveau jour
|
||||||
|
if stats.get("last_push_date") != today:
|
||||||
|
stats["pushes_today"] = 0
|
||||||
|
|
||||||
|
if stats.get("pushes_today", 0) >= max_pushes:
|
||||||
|
return {
|
||||||
|
"allowed": False,
|
||||||
|
"error": f"Limite quotidienne atteinte ({max_pushes} pushes/jour)",
|
||||||
|
"error_code": "DAILY_LIMIT_REACHED"
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"allowed": True,
|
||||||
|
"license_type": license_info.get("type_name"),
|
||||||
|
"remaining_today": max_pushes - get_usage_stats().get("pushes_today", 0) if max_pushes > 0 else -1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def has_feature(feature_name):
|
||||||
|
"""Vérifier si une fonctionnalité est disponible dans la licence"""
|
||||||
|
license_info = validate_license()
|
||||||
|
|
||||||
|
if not license_info.get("valid"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
features = license_info.get("features", [])
|
||||||
|
return feature_name in features
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# CLI POUR TESTS
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
if sys.argv[1] == "status":
|
||||||
|
result = validate_license()
|
||||||
|
if result.get("valid"):
|
||||||
|
print("✅ Licence valide")
|
||||||
|
print(f" ID: {result.get('license_id')}")
|
||||||
|
print(f" Type: {result.get('type_name')}")
|
||||||
|
print(f" Hostname: {result.get('hostname')}")
|
||||||
|
print(f" Expire: {result.get('expires')} ({result.get('days_remaining')} jours)")
|
||||||
|
print(f" Features: {', '.join(result.get('features', []))}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Licence invalide: {result.get('error')}")
|
||||||
|
|
||||||
|
elif sys.argv[1] == "hostname":
|
||||||
|
print(f"Hostname: {get_splunk_hostname()}")
|
||||||
|
|
||||||
|
elif sys.argv[1] == "usage":
|
||||||
|
stats = get_usage_stats()
|
||||||
|
print(f"Total pushes: {stats.get('total_pushes', 0)}")
|
||||||
|
print(f"Pushes aujourd'hui: {stats.get('pushes_today', 0)}")
|
||||||
|
|
||||||
|
elif sys.argv[1] == "check":
|
||||||
|
result = check_limits()
|
||||||
|
if result.get("allowed"):
|
||||||
|
print(f"✅ Push autorisé ({result.get('license_type')})")
|
||||||
|
else:
|
||||||
|
print(f"❌ Push refusé: {result.get('error')}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Usage: python license_validator.py [status|hostname|usage|check]")
|
||||||
|
else:
|
||||||
|
result = validate_license()
|
||||||
|
print(json.dumps(result, indent=2))
|
||||||
@ -0,0 +1,354 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================
|
||||||
|
# Git Pusher - Start Script
|
||||||
|
# Version 2.0 avec credentials sécurisés
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
SPLUNK_HOME=${SPLUNK_HOME:-/opt/splunk}
|
||||||
|
APP_NAME="pusher_app_prem"
|
||||||
|
APP_HOME="${SPLUNK_HOME}/etc/apps/${APP_NAME}"
|
||||||
|
BIN_DIR="${APP_HOME}/bin"
|
||||||
|
LOG_DIR="${SPLUNK_HOME}/var/log/splunk"
|
||||||
|
PID_FILE="${BIN_DIR}/git_pusher.pid"
|
||||||
|
CREDENTIALS_MANAGER="${BIN_DIR}/credentials_manager.py"
|
||||||
|
|
||||||
|
# Couleurs pour les logs
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Fonction de logging
|
||||||
|
log_info() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_debug() {
|
||||||
|
echo -e "${BLUE}[DEBUG]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Charger les credentials de manière sécurisée
|
||||||
|
load_credentials() {
|
||||||
|
# Méthode 1: Utiliser le credentials manager (recommandé)
|
||||||
|
if [ -f "$CREDENTIALS_MANAGER" ]; then
|
||||||
|
CREDS=$(python3 "$CREDENTIALS_MANAGER" get 2>/dev/null)
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
export SPLUNK_USERNAME=$(echo "$CREDS" | head -1)
|
||||||
|
export SPLUNK_PASSWORD=$(echo "$CREDS" | tail -1)
|
||||||
|
log_info "Credentials loaded from secure storage"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Méthode 2: Variables d'environnement déjà définies
|
||||||
|
if [ -n "$SPLUNK_USERNAME" ] && [ -n "$SPLUNK_PASSWORD" ]; then
|
||||||
|
log_info "Using credentials from environment variables"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Méthode 3: Fichier .env (moins sécurisé mais pratique)
|
||||||
|
ENV_FILE="${APP_HOME}/local/.env"
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
source "$ENV_FILE"
|
||||||
|
if [ -n "$SPLUNK_USERNAME" ] && [ -n "$SPLUNK_PASSWORD" ]; then
|
||||||
|
log_warn "Using credentials from .env file (consider using 'credentials setup' for better security)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aucune credential trouvée
|
||||||
|
log_error "No credentials found!"
|
||||||
|
log_error "Please run: $0 credentials setup"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vérifier si le serveur est déjà en cours d'exécution
|
||||||
|
check_running() {
|
||||||
|
if [ -f "$PID_FILE" ]; then
|
||||||
|
PID=$(cat "$PID_FILE")
|
||||||
|
if ps -p $PID > /dev/null 2>&1; then
|
||||||
|
return 0 # Running
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 1 # Not running
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configurer les credentials
|
||||||
|
setup_credentials() {
|
||||||
|
if [ -f "$CREDENTIALS_MANAGER" ]; then
|
||||||
|
python3 "$CREDENTIALS_MANAGER" setup
|
||||||
|
else
|
||||||
|
log_error "Credentials manager not found at $CREDENTIALS_MANAGER"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Afficher le statut des credentials
|
||||||
|
credentials_status() {
|
||||||
|
if [ -f "$CREDENTIALS_MANAGER" ]; then
|
||||||
|
python3 "$CREDENTIALS_MANAGER" status
|
||||||
|
else
|
||||||
|
log_error "Credentials manager not found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Supprimer les credentials
|
||||||
|
delete_credentials() {
|
||||||
|
if [ -f "$CREDENTIALS_MANAGER" ]; then
|
||||||
|
python3 "$CREDENTIALS_MANAGER" delete
|
||||||
|
else
|
||||||
|
log_error "Credentials manager not found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Démarrer le serveur
|
||||||
|
start_server() {
|
||||||
|
log_info "Starting Git Pusher server..."
|
||||||
|
|
||||||
|
# Vérifier si déjà en cours
|
||||||
|
if check_running; then
|
||||||
|
log_warn "Git Pusher is already running (PID: $(cat $PID_FILE))"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Charger les credentials
|
||||||
|
if ! load_credentials; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Créer le répertoire de logs
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
|
# Démarrer le serveur Python
|
||||||
|
cd "$BIN_DIR"
|
||||||
|
python3 git_pusher.py > "${LOG_DIR}/git_pusher_startup.log" 2>&1 &
|
||||||
|
|
||||||
|
# Sauvegarder le PID
|
||||||
|
echo $! > "$PID_FILE"
|
||||||
|
|
||||||
|
# Attendre un peu et vérifier
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if check_running; then
|
||||||
|
log_info "Git Pusher started successfully (PID: $(cat $PID_FILE))"
|
||||||
|
log_info "Server listening on port 9999"
|
||||||
|
|
||||||
|
# Vérifier le statut de la licence
|
||||||
|
HOSTNAME=$(hostname)
|
||||||
|
log_info "Hostname: $HOSTNAME"
|
||||||
|
|
||||||
|
if [ -f "${APP_HOME}/local/license.lic" ]; then
|
||||||
|
log_info "License file found"
|
||||||
|
else
|
||||||
|
log_warn "No license file found - activation required"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_error "Failed to start Git Pusher"
|
||||||
|
log_error "Check logs at ${LOG_DIR}/git_pusher.log"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Arrêter le serveur
|
||||||
|
stop_server() {
|
||||||
|
log_info "Stopping Git Pusher server..."
|
||||||
|
|
||||||
|
if [ -f "$PID_FILE" ]; then
|
||||||
|
PID=$(cat "$PID_FILE")
|
||||||
|
|
||||||
|
if ps -p $PID > /dev/null 2>&1; then
|
||||||
|
kill $PID
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Force kill si nécessaire
|
||||||
|
if ps -p $PID > /dev/null 2>&1; then
|
||||||
|
log_warn "Force killing process..."
|
||||||
|
kill -9 $PID
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$PID_FILE"
|
||||||
|
log_info "Git Pusher stopped"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_warn "Process not running, cleaning up PID file"
|
||||||
|
rm -f "$PID_FILE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warn "PID file not found, Git Pusher may not be running"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redémarrer le serveur
|
||||||
|
restart_server() {
|
||||||
|
log_info "Restarting Git Pusher server..."
|
||||||
|
stop_server
|
||||||
|
sleep 1
|
||||||
|
start_server
|
||||||
|
}
|
||||||
|
|
||||||
|
# Afficher le statut
|
||||||
|
show_status() {
|
||||||
|
echo "============================================"
|
||||||
|
echo "Git Pusher Status"
|
||||||
|
echo "============================================"
|
||||||
|
|
||||||
|
if check_running; then
|
||||||
|
PID=$(cat "$PID_FILE")
|
||||||
|
echo -e "Server Status: ${GREEN}RUNNING${NC}"
|
||||||
|
echo "PID: $PID"
|
||||||
|
echo "Port: 9999"
|
||||||
|
else
|
||||||
|
echo -e "Server Status: ${RED}STOPPED${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Paths:"
|
||||||
|
echo " App Home: $APP_HOME"
|
||||||
|
echo " Bin Dir: $BIN_DIR"
|
||||||
|
echo " Log Dir: $LOG_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Statut des credentials
|
||||||
|
echo "Credentials:"
|
||||||
|
if [ -f "${APP_HOME}/local/.credentials" ]; then
|
||||||
|
echo -e " Secure storage: ${GREEN}Configured${NC}"
|
||||||
|
else
|
||||||
|
echo -e " Secure storage: ${YELLOW}Not configured${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Statut de la licence
|
||||||
|
echo "License:"
|
||||||
|
if [ -f "${APP_HOME}/local/license.lic" ]; then
|
||||||
|
echo -e " File: ${GREEN}Present${NC}"
|
||||||
|
# Essayer de lire quelques infos
|
||||||
|
if command -v python3 &> /dev/null; then
|
||||||
|
python3 -c "
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '$BIN_DIR')
|
||||||
|
try:
|
||||||
|
from license_validator import validate_license
|
||||||
|
result = validate_license()
|
||||||
|
if result.get('valid'):
|
||||||
|
print(f\" Type: {result.get('type_name', 'N/A')}\")
|
||||||
|
print(f\" Expires: {result.get('expires', 'N/A')}\")
|
||||||
|
print(f\" Days remaining: {result.get('days_remaining', 'N/A')}\")
|
||||||
|
else:
|
||||||
|
print(f\" Status: Invalid - {result.get('error', 'Unknown error')}\")
|
||||||
|
except Exception as e:
|
||||||
|
print(f' Unable to read license: {e}')
|
||||||
|
" 2>/dev/null || echo " Unable to read license details"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e " File: ${YELLOW}Not found${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Hostname: $(hostname)"
|
||||||
|
echo "============================================"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Afficher les logs
|
||||||
|
show_logs() {
|
||||||
|
LOG_FILE="${LOG_DIR}/git_pusher.log"
|
||||||
|
|
||||||
|
if [ -f "$LOG_FILE" ]; then
|
||||||
|
if [ "$1" == "-f" ]; then
|
||||||
|
tail -f "$LOG_FILE"
|
||||||
|
else
|
||||||
|
tail -n 50 "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warn "Log file not found at $LOG_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Menu d'aide
|
||||||
|
show_help() {
|
||||||
|
echo "Git Pusher - Server Management Script v2.0"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: $0 {command} [options]"
|
||||||
|
echo ""
|
||||||
|
echo "Server Commands:"
|
||||||
|
echo " start Start the Git Pusher server"
|
||||||
|
echo " stop Stop the Git Pusher server"
|
||||||
|
echo " restart Restart the Git Pusher server"
|
||||||
|
echo " status Show the current status"
|
||||||
|
echo " logs [-f] Show recent logs (-f for follow)"
|
||||||
|
echo ""
|
||||||
|
echo "Credentials Commands:"
|
||||||
|
echo " credentials setup Configure Splunk credentials securely"
|
||||||
|
echo " credentials status Show credentials status"
|
||||||
|
echo " credentials delete Delete stored credentials"
|
||||||
|
echo ""
|
||||||
|
echo "Other:"
|
||||||
|
echo " help Show this help message"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 credentials setup # First time setup"
|
||||||
|
echo " $0 start # Start server"
|
||||||
|
echo " $0 logs -f # Follow logs"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
start_server
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop_server
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart_server
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
show_status
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
show_logs "$2"
|
||||||
|
;;
|
||||||
|
credentials)
|
||||||
|
case "$2" in
|
||||||
|
setup)
|
||||||
|
setup_credentials
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
credentials_status
|
||||||
|
;;
|
||||||
|
delete)
|
||||||
|
delete_credentials
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 credentials {setup|status|delete}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
show_help
|
||||||
|
else
|
||||||
|
echo "Unknown command: $1"
|
||||||
|
echo ""
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDCTCCAfGgAwIBAgIUCuKo8SLloS5cjBOR04+X6ayZ40cwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDEyMzIyMTIxOFoXDTI3MDEy
|
||||||
|
MzIyMTIxOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||||
|
AAOCAQ8AMIIBCgKCAQEAs0vF6sFTseKgZC1nZ6CVZdw45yk1Ni0W9Mc24KZ9NKCJ
|
||||||
|
rP0tHy0hs6mME/sq8DV1fh0YtqIvBCxcKEE84/cVXmUfZF9JRXO95734+JGPmo07
|
||||||
|
zpiu7p3r4WyIWmCXX5VB0UkMEXsPQmonqG1Kwtz+R1cfgis2lUk+xsC2zSjER8l4
|
||||||
|
2UODjHvtD25usgxKjpwPrCuZt43miArnVnwfB8OLbAqpwQeYIf18bPt/TrnQsdgd
|
||||||
|
ZZiQdE6UTaJ5xhqztwpYJO9pvZA24Bi3bGNfBciITds5RCGY2wQo8yxbeJsidTuW
|
||||||
|
7Z64DK9t33oVnB2PqlP6hVGD5Agthsv9ehRPxdd3MwIDAQABo1MwUTAdBgNVHQ4E
|
||||||
|
FgQUy0dni+ogqC7YuvfD/Pn0AuebsXQwHwYDVR0jBBgwFoAUy0dni+ogqC7YuvfD
|
||||||
|
/Pn0AuebsXQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAlyxg
|
||||||
|
vR15lsYp4TxJPi1WPzLZl1e6ewTl8GhyE1saxS8LRtyTyr8sa9EFRLQ0OIsqYrUw
|
||||||
|
zZi7FIDoDPZDKpd0/+U94UKlhUuPUyufQwl5vNu0A+SEpwKeznUMaj4Y98tHvVGd
|
||||||
|
1SCndZBWn/v2U4nXqHoTd6Y0xEOga0jUEsUMBckNC236BTo88Zk65/oa9Gncyb27
|
||||||
|
9vGVCbmPyzE70H4KFoVtxkoZrKywn+0ajHhgH5gqZNRPWpe6i8xTbMAeIXkCjmWL
|
||||||
|
LmOA7MkjeQBBEWewu4vMOXsvf+gCtxUj5owsAcOQlZ3g72Sng4MeMjuVx4ZRVxX9
|
||||||
|
fj+vCP9EFI8rX48tjQ==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCzS8XqwVOx4qBk
|
||||||
|
LWdnoJVl3DjnKTU2LRb0xzbgpn00oIms/S0fLSGzqYwT+yrwNXV+HRi2oi8ELFwo
|
||||||
|
QTzj9xVeZR9kX0lFc73nvfj4kY+ajTvOmK7unevhbIhaYJdflUHRSQwRew9Caieo
|
||||||
|
bUrC3P5HVx+CKzaVST7GwLbNKMRHyXjZQ4OMe+0Pbm6yDEqOnA+sK5m3jeaICudW
|
||||||
|
fB8Hw4tsCqnBB5gh/Xxs+39OudCx2B1lmJB0TpRNonnGGrO3Clgk72m9kDbgGLds
|
||||||
|
Y18FyIhN2zlEIZjbBCjzLFt4myJ1O5btnrgMr23fehWcHY+qU/qFUYPkCC2Gy/16
|
||||||
|
FE/F13czAgMBAAECggEAMrEMrvej0xpQ4KHZp3nGY3sk9242JjAPWntsb42CvrtY
|
||||||
|
0XjvJe5bpfEcspWDqVBj/Jj7YL9v7Y0hLRxsu8Mi3oJWoskx7RnxKjES0CxPXpHp
|
||||||
|
w9p1Mu+hPiWyU2MVySdo6WPuro6NXOiod70WswtKNR9TwDi5gPGpdwYLaOvKusSp
|
||||||
|
Rncm0m0H3IBhgVA691X0AUIomAW3Wmh+5If1XHfjrNHTB8cjcNf6koPMkCqHCEZ9
|
||||||
|
wtINxOJior+gGkjMXaDszqzNlicVBXFEFjaXWcp38xAif1uimpqKsRzZEF6RAUzi
|
||||||
|
H7cI3aF2dXG3C9l6Byi7OSgd8X4JUnE0dlCpC7qweQKBgQDgvoavo8G0kYruCUIQ
|
||||||
|
6vcSs1YBByOkl6yZBCZWk10NgRpU1wyu9zmlvEwNVlUfALs5eoLxnhe8Wklq0ckQ
|
||||||
|
r/Rl+r/lj/MZUFn49TgUCsUOIi/G7nWQG0bPo4bCB2QXsAiKdY+KZeC56620uyom
|
||||||
|
1VY+nS3y8O4EP0YHX0qHFfmIZwKBgQDMOywO0DSrZMDyvmbwL0ISzHRcNpRn0jk7
|
||||||
|
pEtzM/VOx+v0O93E+5OygzmXlBKjF0MwMXBidf8IZu4xO8qWqAM4EP2DD0cpoS1Z
|
||||||
|
WiHHkc5NZhjgeG6C4XaCXR++7CuY25VKKe01yz/+j51linDD8OAibKUspkjVufEN
|
||||||
|
R/AT0GFLVQKBgBxMYTEkcXOHD/NA/yyaKVoVcrLWb0p+PqFVwG4OSB03MFWWbmZp
|
||||||
|
gry3pOvY/wbUVL68CljaCysQQ0ZL/AE55pAgrqD9KyL41xtd5R3A7WcGLvXheLQY
|
||||||
|
eyYR9RnhTF0fMTQd8WD/yvgeENU86+XP3vgrWmnIpG+sd+jdusifn7fpAn9QkwfO
|
||||||
|
0FX3SMjW/EegewSWZhOCTgY+77Gk1izuRpGBg16T/QqBrL+Yri0KoGC593OKj/bG
|
||||||
|
4ca8id9vjSdgSOj8NbfO/TgWNICvv9+T3PKHlsA5z0nKWSloRVVA/ew1YmyD1gbA
|
||||||
|
MnAM/pwac4QJyf6jljmUZAZYTAPOOZN+PbglAoGBAJ9cOGDgT+BCOoNc0T1GJDAk
|
||||||
|
xOR8d+tD+j4JH5IVxB51DXjJOZxw9U3XhNH1OcE0x3fRzKJOtlQLxP6fHYVtMVFq
|
||||||
|
VpeekmTtJ9OfMg68ELOlf7ykA3GhMJ3FarM6e8+X+KliGf6ND4HBMb112FlMgIi6
|
||||||
|
yYi7sfSL53Dzp1Q2DxXV
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
#
|
||||||
|
# Splunk app configuration file
|
||||||
|
#
|
||||||
|
|
||||||
|
[install]
|
||||||
|
is_configured = 0
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
is_visible = true
|
||||||
|
label = Pusher Premium
|
||||||
|
|
||||||
|
[launcher]
|
||||||
|
author =
|
||||||
|
description =
|
||||||
|
version = 2.0.0
|
||||||
|
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
<nav search_view="git_pusher_dashboard">
|
||||||
|
<view name="git_pusher_dashboard" default='true' />
|
||||||
|
<view name="git_pusher_config" />
|
||||||
|
</nav>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
Add all the views that your app needs in this directory
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"username": "admin", "password": "VgNoQiAmDzgUO3NQ"}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
d0YpjIlHuVRowV+y4yQh+VuqDhq7QikC3qepdfLE9+o=
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"total_pushes": 21,
|
||||||
|
"pushes_today": 6,
|
||||||
|
"last_push_date": "2026-02-21",
|
||||||
|
"apps_pushed": []
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
[ui]
|
||||||
|
|
||||||
|
[launcher]
|
||||||
|
|
||||||
|
[install]
|
||||||
|
state = enabled
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDxTCCA0ugAwIBAgISBXu55+97HBg/V5v9p1wKdygbMAoGCCqGSM49BAMDMDIx
|
||||||
|
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
|
||||||
|
ODAeFw0yNjAyMTcxMzU5MDRaFw0yNjA1MTgxMzU5MDNaMCkxJzAlBgNVBAMTHm15
|
||||||
|
cHJpdnNwbGRldi5qcC1lbmdpbmVlcmluZy5mcjB2MBAGByqGSM49AgEGBSuBBAAi
|
||||||
|
A2IABOLi1mN0uYVlBILGpDZDyk1wmGPgXklxm7E2Zn/Nswn54qsHJcSW8gSJDHyE
|
||||||
|
GbqFcc3wTomzFHa73aPxF+/8bTX281VkKjABfNPFSu83qmVcDZ35VT4bF1zbDOES
|
||||||
|
280Kq6OCAiswggInMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcD
|
||||||
|
ATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTq3jD3pSEI3y4olRyu1I/aK5sGDDAf
|
||||||
|
BgNVHSMEGDAWgBSPDROi9i5+0VBsMxg4XVmOI3KRyjAyBggrBgEFBQcBAQQmMCQw
|
||||||
|
IgYIKwYBBQUHMAKGFmh0dHA6Ly9lOC5pLmxlbmNyLm9yZy8wKQYDVR0RBCIwIIIe
|
||||||
|
bXlwcml2c3BsZGV2LmpwLWVuZ2luZWVyaW5nLmZyMBMGA1UdIAQMMAowCAYGZ4EM
|
||||||
|
AQIBMC0GA1UdHwQmMCQwIqAgoB6GHGh0dHA6Ly9lOC5jLmxlbmNyLm9yZy8yOC5j
|
||||||
|
cmwwggENBgorBgEEAdZ5AgQCBIH+BIH7APkAfwBxfpXzwjiKbbHjhEk9MeFaqWII
|
||||||
|
di1CAOAFDNBntaZh4gAAAZxsG05YAAgAAAUACa6ZQgQDAEgwRgIhAIichDr9Q2CB
|
||||||
|
AXzXmEbKjiO9qSAUXE7s8IgBXw8BfjluAiEAyjAyAc2I4hCjco4FLp7+y0nmeElH
|
||||||
|
yl4BBAaCEsOFK3AAdgAWgy2r8KklDw/wOqVF/8i/yCPQh0v2BCkn+OcfMxP1+gAA
|
||||||
|
AZxsG1XXAAAEAwBHMEUCIE7uCd4jXwCcTGuqR3ThJm0Zms0U7tAJcuhnR2F0syZJ
|
||||||
|
AiEAj06LtXnMoYEN6wvfgFUVd4zekUUaNJlJ1TK0HcMMlNQwCgYIKoZIzj0EAwMD
|
||||||
|
aAAwZQIwZaVb5NEYfUrGirhaFV9eohbjDY7YLzaJ5dIxH+Jt78o+GMfypoFc6Rc1
|
||||||
|
S2FPNCJsAjEAphAhfBmVN1ASp3JENib1t12TuEVaTNHdNlzzuFE4s3z+z0cp6je4
|
||||||
|
MmgQpu/hgcn3
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP
|
||||||
|
MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
|
||||||
|
Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
|
||||||
|
Fw0yNzAzMTIyMzU5NTlaMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
|
||||||
|
bmNyeXB0MQswCQYDVQQDEwJFODB2MBAGByqGSM49AgEGBSuBBAAiA2IABNFl8l7c
|
||||||
|
S7QMApzSsvru6WyrOq44ofTUOTIzxULUzDMMNMchIJBwXOhiLxxxs0LXeb5GDcHb
|
||||||
|
R6EToMffgSZjO9SNHfY9gjMy9vQr5/WWOrQTZxh7az6NSNnq3u2ubT6HTKOB+DCB
|
||||||
|
9TAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB
|
||||||
|
MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI8NE6L2Ln7RUGwzGDhdWY4j
|
||||||
|
cpHKMB8GA1UdIwQYMBaAFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEB
|
||||||
|
BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzATBgNVHSAE
|
||||||
|
DDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veDEuYy5sZW5j
|
||||||
|
ci5vcmcvMA0GCSqGSIb3DQEBCwUAA4ICAQBnE0hGINKsCYWi0Xx1ygxD5qihEjZ0
|
||||||
|
RI3tTZz1wuATH3ZwYPIp97kWEayanD1j0cDhIYzy4CkDo2jB8D5t0a6zZWzlr98d
|
||||||
|
AQFNh8uKJkIHdLShy+nUyeZxc5bNeMp1Lu0gSzE4McqfmNMvIpeiwWSYO9w82Ob8
|
||||||
|
otvXcO2JUYi3svHIWRm3+707DUbL51XMcY2iZdlCq4Wa9nbuk3WTU4gr6LY8MzVA
|
||||||
|
aDQG2+4U3eJ6qUF10bBnR1uuVyDYs9RhrwucRVnfuDj29CMLTsplM5f5wSV5hUpm
|
||||||
|
Uwp/vV7M4w4aGunt74koX71n4EdagCsL/Yk5+mAQU0+tue0JOfAV/R6t1k+Xk9s2
|
||||||
|
HMQFeoxppfzAVC04FdG9M+AC2JWxmFSt6BCuh3CEey3fE52Qrj9YM75rtvIjsm/1
|
||||||
|
Hl+u//Wqxnu1ZQ4jpa+VpuZiGOlWrqSP9eogdOhCGisnyewWJwRQOqK16wiGyZeR
|
||||||
|
xs/Bekw65vwSIaVkBruPiTfMOo0Zh4gVa8/qJgMbJbyrwwG97z/PRgmLKCDl8z3d
|
||||||
|
tA0Z7qq7fta0Gl24uyuB05dqI5J1LvAzKuWdIjT1tP8qCoxSE/xpix8hX2dt3h+/
|
||||||
|
jujUgFPFZ0EVZ0xSyBNRF3MboGZnYXFUxpNjTWPKpagDHJQmqrAcDmWJnMsFY3jS
|
||||||
|
u1igv3OefnWjSQ==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFCzCCAvOgAwIBAgIUIkJeLpRn6wfN8VbsMrPdFAl/x7swDQYJKoZIhvcNAQEL
|
||||||
|
BQAwFTETMBEGA1UEAwwKZ2l0LXB1c2hlcjAeFw0yNjAyMDExOTM1MzNaFw0yNzAy
|
||||||
|
MDExOTM1MzNaMBUxEzARBgNVBAMMCmdpdC1wdXNoZXIwggIiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4ICDwAwggIKAoICAQDsTH5H+OD27yXIFz4MvNrpmpNgYly6QYUukJO8FI/f
|
||||||
|
unBe4N5mfOTZ/WrBeR5KoBOS3FV8VWIH7ShXIzWBpollmIz9+jlPyftawXyiSnSX
|
||||||
|
rwLBdHc8gjVRZf1H2U0E191v9z0oaXApy8d7E0Pw+i4odoGHOX5Ix89s0DJrG5UP
|
||||||
|
LrLr9OHlyMCC0D2QVp0wqFGkIXXv/cyYwlcGPACvFhE/fWUazC4AEImJhXypOfQZ
|
||||||
|
h57SoldKQWwW7BZdGmnbSqeG2lq7KFUow0sie4KzRPLPXrIGdKQbPAKDKcQ7MYlG
|
||||||
|
9bfgsmM1Rr6klRAmO/e4w1HRHSHUetmFBYDJ5MYNdeddYDfIVwjgwfDvy1i+ojRl
|
||||||
|
/viywW3ONXR9rUcx/nGGc8UJTjJAAaAaTj6UMzn40ltLYmiymhj6mWFNiuGwDxas
|
||||||
|
YTxA/3i62pGlbC8s1ZSXtdCYsEN4+W3N5CSadHdVtAIIEc6OyQ2mRa3v4oeZlyzt
|
||||||
|
1mk9j9oChxi5r74ujUmjNHxpawWG5wmnRy3b9ABxcivESEuXIbjBXrQ+RCmuQ8Dp
|
||||||
|
5Asa7iede7iEPkhGw3Es3uVdk4s3/OlS6o+1F+rG9qZr3aTtJrciVSLofbrSNtFF
|
||||||
|
MERip+VBwIdBofVzS6GUhnLjjdhfu/kdzsqrTDDFan4NX1IyC+zBL90DPQ8SoIOg
|
||||||
|
owIDAQABo1MwUTAdBgNVHQ4EFgQUe6KibB+U4yTq9/pwf6C4JeoPQo0wHwYDVR0j
|
||||||
|
BBgwFoAUe6KibB+U4yTq9/pwf6C4JeoPQo0wDwYDVR0TAQH/BAUwAwEB/zANBgkq
|
||||||
|
hkiG9w0BAQsFAAOCAgEA12Y7N6iLOtRAt+xD3B5eIdJtPnFclhmy8FCd2+SdnF/K
|
||||||
|
zWVdzTsWYKzZ4TZ08aMOh2+H2vDf/ZTG+gryCob28QVy0sRxbpA4WJMI6zBfZSKr
|
||||||
|
Raudh5Gql/CO28gN4k6EGgNAn1S02NkvhZP4FokfXwkY4CIReRECJzws0gCyMZto
|
||||||
|
wLfYEVyvN9NYQf1xoJHaozGghA0FADQrr6pQsl1Ek4bkrvb8/L8gWeT6lgny0ZcI
|
||||||
|
1UEW9lkx7pc0dRDOjUjVzlHYzAf53xEslontihQcOoE/Uj3Wsmi1V/lPm+Pi0Ml0
|
||||||
|
msMUkvMZP6Y8lnvRT4625YKR6L3ibuQxlXGtKHvu7eJdcOnW5IeVmg5vsH2xoVaF
|
||||||
|
4L0zBPpA6CBzsfZPgLJ9odD10nRVx8bh0b555/CUP5WIVHvLkPgp+NptJAoKah7Q
|
||||||
|
PkI5OKZWGjrJGiV7BWx5HIB5TX/w0PCjMA4ce3yVRNs7poUzR4PKgohrp94cIDFa
|
||||||
|
m+K4rfBmeMwVuuJZIuYpye80dcdsiFPrIZuA+hbP7tGlrwBKKMzpB3BEDIFTPb2b
|
||||||
|
hUq1TJCcHffFqxpRia+L/MMI8RpaEwzW9hghQB/LiPG0UqJycNpuo8cnggx+bsop
|
||||||
|
0ZXB68cW6K1UWLo1qLSz7/0+65PrWRkJisbjtmpKRDJlB72yIxlsZmBucxBy3II=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDQfw7eesJcJWzoV0AL
|
||||||
|
EJtevZRBdfNTKjceiNAfFoc/2bKN5pKU3n17/ZuJGGWFGNOhZANiAATi4tZjdLmF
|
||||||
|
ZQSCxqQ2Q8pNcJhj4F5JcZuxNmZ/zbMJ+eKrByXElvIEiQx8hBm6hXHN8E6JsxR2
|
||||||
|
u92j8Rfv/G019vNVZCowAXzTxUrvN6plXA2d+VU+Gxdc2wzhEtvNCqs=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDsTH5H+OD27yXI
|
||||||
|
Fz4MvNrpmpNgYly6QYUukJO8FI/funBe4N5mfOTZ/WrBeR5KoBOS3FV8VWIH7ShX
|
||||||
|
IzWBpollmIz9+jlPyftawXyiSnSXrwLBdHc8gjVRZf1H2U0E191v9z0oaXApy8d7
|
||||||
|
E0Pw+i4odoGHOX5Ix89s0DJrG5UPLrLr9OHlyMCC0D2QVp0wqFGkIXXv/cyYwlcG
|
||||||
|
PACvFhE/fWUazC4AEImJhXypOfQZh57SoldKQWwW7BZdGmnbSqeG2lq7KFUow0si
|
||||||
|
e4KzRPLPXrIGdKQbPAKDKcQ7MYlG9bfgsmM1Rr6klRAmO/e4w1HRHSHUetmFBYDJ
|
||||||
|
5MYNdeddYDfIVwjgwfDvy1i+ojRl/viywW3ONXR9rUcx/nGGc8UJTjJAAaAaTj6U
|
||||||
|
Mzn40ltLYmiymhj6mWFNiuGwDxasYTxA/3i62pGlbC8s1ZSXtdCYsEN4+W3N5CSa
|
||||||
|
dHdVtAIIEc6OyQ2mRa3v4oeZlyzt1mk9j9oChxi5r74ujUmjNHxpawWG5wmnRy3b
|
||||||
|
9ABxcivESEuXIbjBXrQ+RCmuQ8Dp5Asa7iede7iEPkhGw3Es3uVdk4s3/OlS6o+1
|
||||||
|
F+rG9qZr3aTtJrciVSLofbrSNtFFMERip+VBwIdBofVzS6GUhnLjjdhfu/kdzsqr
|
||||||
|
TDDFan4NX1IyC+zBL90DPQ8SoIOgowIDAQABAoICABEB7HDns+FyEwkUyy2Fhkgc
|
||||||
|
DRF54uyw/JH+a/O0kypqM95QVxGPWbVq7P0h55E9dksyuqBpUNX7NtUWvqonP2pl
|
||||||
|
kXhSQz+/7Ox6Uqsnqr6kJRGhfVeIk6fZLGK4fDemBdUiOW+oLx+DAEeWemRkzV+y
|
||||||
|
L954v+MjJoXRcl+NK6xdExmylXPBgEGqFVRHN6ch5kZm9iMg5FH2Yuca+H2hm/oy
|
||||||
|
300PdxwgFJYmWnOfrTcNMNw+PQQmM05vDakD1qym8end23vvCjoV3FmOBDk89DEC
|
||||||
|
wtN+H7WqGxAvuGT+SsAlvWdZz5QtFFmqNPBbjpfozwG7FA1EDlXpsHxXj/22B8Ho
|
||||||
|
XAPQ0ZeEJv4ZDbLs7AM/ofpNaitYcE0po2W7dlMqb1so3hK+eCPpVfos8mxlrkPV
|
||||||
|
sq2nFzVtl7V3MeipGw/MixDZZz6LDX0M1tk60a+4zAbDJaht1dS4SLfhygdQM+/m
|
||||||
|
OEAWyRAT7TDhCB+F474/G8PM1PnKmps5gV0X5jkqjQBCterBRwVcTI+ewNxUz7+Q
|
||||||
|
f4iN6sO8+Ihf94sr0YwaaIFyEetxTWBjevqWXpZ7fmdVbAmD5Z7WN8nvuARyXYdd
|
||||||
|
xOfXYA0mv0gtF1RDFV69lsMD6yPIGYdYIVd50r6P0VPQK+yjDQM3Pn3tVyLBmwqK
|
||||||
|
smKGCvkpZ92UvTsR3L/BAoIBAQD3w2RgKg3qVUeuJgbFDCRS/AkF1mkI4PMY5WLc
|
||||||
|
BHGa9q/9LucYTpZ60Pq/t9cNdDrEY/wdGvS23shWsAN3XGnj3m9FCCyuEZojmOkO
|
||||||
|
kNL51afhJ4h7r0yy4oPuXhQ5V860PuC3xv1QiBpxvKsod+KwiJI7O49dNxCr5dGk
|
||||||
|
RRdAHJEm73O2EmiTFHOZ3oGPoZ5SGtYxurVd3MNptiuKQYLA4+2i/P439TbCrh7E
|
||||||
|
0dxW9bZRLTygA77HLMA5suHKR4q6DS0HlpY9Cba2CxxIh/4xJRSQHOgiamNtYLeH
|
||||||
|
PnrXjyQVblf09j34YMc1K0A4R91CI74BYe5MwCptRSOIer6DAoIBAQD0J4g6F01m
|
||||||
|
26EqLhszq3n4jVroqmdB+oQ7q+yclrpjjPkwYhKZmXVrA+eNavBuMdLNv+50eqg4
|
||||||
|
9RW8H+GBwzhA+CWbZCFJ3Ad3e7jcE6QJVMjNIe6sZ15KF0IvYJA3B1lSsuK1jGga
|
||||||
|
eOFQZG+nIqpH58iowJt0wUTuiehf2xRaEhVkQn+PvIriPAmTlI3xME7ehctxFm8C
|
||||||
|
ZqFODfXsh0bLMx/pWVKDKYNh6WOst3k6/CUt4vN/PlPVvnNLMtbFQWyn1p5LhvvZ
|
||||||
|
xVlFBtobkaGUEjFYoQTwzl3s566f2anMa2Slhz0uopTttkvd3rXyPHm4guNiN/CN
|
||||||
|
ohW534tkPfthAoIBAFsBIfVQfRv9hv6oaQQnmZABky7ZumrQdXpHhzBZUYEh6zKL
|
||||||
|
78Y113/1EqUo2YzPjGZmc0wdgpVI7z0oGZ3WC+7u3N/2SLMHNB6vI6t99oBdwfQp
|
||||||
|
mTAVC48JNHxxgewuHHaIQfI+3PyfgVcVfai3oERHZa7sCZSrjSwWlhJIbmnWFFrA
|
||||||
|
yTevO0oK0QtLdztSmdx+jv5lHgkD9aL2jreRqH1BOyAK3TWglCSd4B9bFhu61OSs
|
||||||
|
QQBlX8W44kJPOjAaZxI/lLKc1UJGNx5WpmTdzrgubocglwNNIIgkZkT+5hAXO6HD
|
||||||
|
jfskF08L/R/CayxA+Tw59Kh9WBJI40yPgKW4sBECggEBAIdk0MeeGn86tnIEpXMO
|
||||||
|
2ZG7GbnCnYZaHTBWE912PKBuEdYB3Nyu3A1fWe3zaqdBG+ybTensBxOm3cm4SD7E
|
||||||
|
epKUyY4VhdxGlyFsS8RHZAUErmILOicDH6eopDxPqUnK2n7g0pXo6eYcOJ5zQ/OE
|
||||||
|
Zrd/Uqg6PzsM3mQFuAZIIE4ejxxNQB3+aWox7wGXNOuWZXZC7eGlliPXtAXr+f+T
|
||||||
|
uO+AR2cI8Jfp0oDegzbJfAH4x8ldfLiIYMc8WQVPiQhUUqP0gU3S6iEGro130kXN
|
||||||
|
ibPqLtE+YdYEKtPwWscsVlwVBfhBOe19nWcBW6sLEQzm+n0WoG/cI5r3UmMEE3Gg
|
||||||
|
aaECggEAJBcLJKp4WEz69qQbvV15HrEgsctIswUoaRsF0r0qnCAJoySG0mX2rGWR
|
||||||
|
Wmf8I2rw5hARLR5BFa3YtBPj0doL2mSxLOuH5tKdqthMai+B7K2J/YTPh8xOKoQd
|
||||||
|
2HcAoh1zxT2oA1Fj7BwzpvLTWwJEfsWzcOM+2ApxzLXkmgF90TBHoeCvlbfzdZ99
|
||||||
|
Ni/yj7tRpK1LoJYCkUHRspWwHB8DIagohG8tCtu4Bb5NdogtfaNeQ/BrgnZj77kf
|
||||||
|
FHu59hzsQQzkplCOnwJuT/Y6iqZuuJgPiESprUxHUz6aesySgcL1tuEq1GmB6O6U
|
||||||
|
eiDRzM4++dQGm7osCy1NRbPGxdUyhw==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"api": {
|
||||||
|
"url": "https://myprivspldev-api.jp-engineering.fr",
|
||||||
|
"port": 9999,
|
||||||
|
"useProxy": true
|
||||||
|
},
|
||||||
|
"deployer": {
|
||||||
|
"enabled": true,
|
||||||
|
"host": "myprivspldev-shdp-api.jp-engineering.fr",
|
||||||
|
"port": 9998,
|
||||||
|
"token": "bc2564e5a885d49ac3811dc946ca5620da24da19b8a8f5c5fdfcd7c07a241688",
|
||||||
|
"useSSL": true
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"checkInterval": 24
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"logLevel": "INFO",
|
||||||
|
"timeout": 30,
|
||||||
|
"gitTimeout": 120
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,572 @@
|
|||||||
|
<dashboard version="1.1" script="license_validation.js, git_pusher.js" hideEdit="true" hideExport="true">
|
||||||
|
<label>Git Pusher - Deploy Applications</label>
|
||||||
|
<description>Push Splunk applications to Git repository and deploy to SH Cluster</description>
|
||||||
|
|
||||||
|
<search id="dsearch">
|
||||||
|
<query>| rest /services/apps/local | search disabled=0 | table title, label, description | rename title as name | sort label</query>
|
||||||
|
<earliest>-1m</earliest>
|
||||||
|
<latest>now</latest>
|
||||||
|
</search>
|
||||||
|
|
||||||
|
<row>
|
||||||
|
<panel>
|
||||||
|
<html>
|
||||||
|
<style>
|
||||||
|
/* ============================================ */
|
||||||
|
/* GIT PUSHER STYLES - VERSION 2.2 */
|
||||||
|
/* ============================================ */
|
||||||
|
|
||||||
|
.git-pusher-container {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header avec badge de licence */
|
||||||
|
.header-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-badge {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container pour le badge de licence */
|
||||||
|
#license-badge-container {
|
||||||
|
min-width: 200px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grille principale */
|
||||||
|
.main-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.main-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
.section {
|
||||||
|
background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 4px;
|
||||||
|
height: 24px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"],
|
||||||
|
.form-group input[type="password"],
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox personnalisé */
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Liste des applications */
|
||||||
|
#dashboard-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dashboard-list::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dashboard-list::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dashboard-list::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dashboard-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a1a1a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header label {
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-count {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-item:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-item input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-item label {
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-badge {
|
||||||
|
background: #e8f0fe;
|
||||||
|
color: #1a73e8;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section SH Deployer */
|
||||||
|
.deployer-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(145deg, #fff8e1 0%, #ffecb3 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #ffd54f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-status {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-config-btn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #f57c00;
|
||||||
|
color: #f57c00;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-config-btn:hover {
|
||||||
|
background: #f57c00;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-checkbox input {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
accent-color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-auth {
|
||||||
|
display: none;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px dashed #ffd54f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-auth.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-auth-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-auth input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ffd54f;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boutons */
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 14px 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Messages */
|
||||||
|
.message {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background: linear-gradient(145deg, #e8f5e9 0%, #c8e6c9 100%);
|
||||||
|
color: #2e7d32;
|
||||||
|
border: 1px solid #a5d6a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: linear-gradient(145deg, #ffebee 0%, #ffcdd2 100%);
|
||||||
|
color: #c62828;
|
||||||
|
border: 1px solid #ef9a9a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading */
|
||||||
|
.loading {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(145deg, #e3f2fd 0%, #bbdefb 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
border: 1px solid #90caf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 3px solid #90caf9;
|
||||||
|
border-top-color: #1976d2;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: #1565c0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="git-pusher-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header-section">
|
||||||
|
<div class="header-title">
|
||||||
|
<h1>🚀 Git Pusher</h1>
|
||||||
|
<span class="version-badge">v2.2</span>
|
||||||
|
</div>
|
||||||
|
<div id="license-badge-container">
|
||||||
|
<!-- Le badge de licence sera inséré ici par JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grille principale -->
|
||||||
|
<div class="main-grid">
|
||||||
|
<!-- Colonne gauche: Applications -->
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">📦 Applications</h2>
|
||||||
|
<div id="dashboard-list">
|
||||||
|
<!-- Liste générée par JavaScript -->
|
||||||
|
<p style="color: #888; text-align: center; padding: 20px;">Loading applications...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonne droite: Configuration -->
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">⚙️ Git Configuration</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="git-url">Repository URL</label>
|
||||||
|
<input type="text" id="git-url" placeholder="https://github.com/user/repo.git" />
|
||||||
|
<div class="form-hint">HTTPS URL of your Git repository</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="git-branch">Branch</label>
|
||||||
|
<input type="text" id="git-branch" value="main" placeholder="main" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="git-token">Access Token / Password</label>
|
||||||
|
<input type="password" id="git-token" placeholder="ghp_xxxx or personal access token" />
|
||||||
|
<div class="form-hint">Personal access token with write permissions</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="commit-message">Commit Message</label>
|
||||||
|
<textarea id="commit-message" placeholder="Describe your changes..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<input type="checkbox" id="save-credentials" />
|
||||||
|
<label for="save-credentials">Remember credentials for next time</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section SH Deployer -->
|
||||||
|
<div class="deployer-section" id="deployer-section">
|
||||||
|
<div class="deployer-header">
|
||||||
|
<div class="deployer-title">
|
||||||
|
🎯 Deploy to Search Head Cluster
|
||||||
|
<span class="deployer-status" id="deployer-status">
|
||||||
|
<span style="color: #888;">● Checking...</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button class="deployer-config-btn" onclick="showDeployerConfigModal()">⚙️ Configure</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="deployer-checkbox">
|
||||||
|
<input type="checkbox" id="deploy-to-shcluster" onchange="toggleDeployerAuth()" />
|
||||||
|
<label for="deploy-to-shcluster">
|
||||||
|
<strong>Enable automatic deployment</strong><br/>
|
||||||
|
<small style="color: #888;">After pushing to Git, pull and apply bundle to SH Cluster</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="deployer-auth" id="deployer-auth">
|
||||||
|
<div class="form-hint" style="margin-bottom: 10px;">
|
||||||
|
Splunk credentials for applying shcluster-bundle (optional if using default)
|
||||||
|
</div>
|
||||||
|
<div class="deployer-auth-grid">
|
||||||
|
<input type="text" id="sh-auth-user" placeholder="Splunk username (optional)" />
|
||||||
|
<input type="password" id="sh-auth-pass" placeholder="Splunk password (optional)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Boutons -->
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-primary" id="push-btn" onclick="pushDashboards()">
|
||||||
|
✈️ Deploy to Git
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="resetForm(true)">
|
||||||
|
🔄 Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
<div class="loading" id="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span class="loading-text">Deploying applications to Git... Please wait</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message success" id="success-message"></div>
|
||||||
|
<div class="message error" id="error-message"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleDeployerAuth() {
|
||||||
|
var checkbox = document.getElementById('deploy-to-shcluster');
|
||||||
|
var authSection = document.getElementById('deployer-auth');
|
||||||
|
if (checkbox) {
|
||||||
|
if (authSection) {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
authSection.classList.add('visible');
|
||||||
|
} else {
|
||||||
|
authSection.classList.remove('visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
</panel>
|
||||||
|
</row>
|
||||||
|
</dashboard>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"pushes_today": 1, "pushes_total": 47, "last_push_date": "2026-02-14", "apps_pushed": []}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
# Application-level permissions
|
||||||
|
|
||||||
|
[]
|
||||||
|
access = read : [ * ], write : [ admin, power ]
|
||||||
|
|
||||||
|
### EVENT TYPES
|
||||||
|
|
||||||
|
[eventtypes]
|
||||||
|
export = system
|
||||||
|
|
||||||
|
|
||||||
|
### PROPS
|
||||||
|
|
||||||
|
[props]
|
||||||
|
export = system
|
||||||
|
|
||||||
|
|
||||||
|
### TRANSFORMS
|
||||||
|
|
||||||
|
[transforms]
|
||||||
|
export = system
|
||||||
|
|
||||||
|
|
||||||
|
### LOOKUPS
|
||||||
|
|
||||||
|
[lookups]
|
||||||
|
export = system
|
||||||
|
|
||||||
|
|
||||||
|
### VIEWSTATES: even normal users should be able to create shared viewstates
|
||||||
|
|
||||||
|
[viewstates]
|
||||||
|
access = read : [ * ], write : [ * ]
|
||||||
|
export = system
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
[app/ui]
|
||||||
|
version = 10.0.2
|
||||||
|
modtime = 1769115948.043388000
|
||||||
|
|
||||||
|
[app/launcher]
|
||||||
|
version = 10.0.2
|
||||||
|
modtime = 1769115948.046389000
|
||||||
|
|
||||||
|
[views/git_pusher_-_push_dashboards_to_git]
|
||||||
|
access = read : [ admin ], write : [ admin ]
|
||||||
|
export = system
|
||||||
|
owner = admin
|
||||||
|
version = 10.0.2
|
||||||
|
modtime = 1769276443.812957000
|
||||||
|
|
||||||
|
[views/git_pusher_-_push_applications_to_git]
|
||||||
|
owner = admin
|
||||||
|
version = 10.0.2
|
||||||
|
modtime = 1769361925.808816000
|
||||||
|
|
||||||
|
[views/git_pusher_-_deploy_applications]
|
||||||
|
access = read : [ * ], write : [ * ]
|
||||||
|
export = none
|
||||||
|
owner = admin
|
||||||
|
version = 10.0.2
|
||||||
|
modtime = 1769976365.099941000
|
||||||
|
|
||||||
|
[app/install/state]
|
||||||
|
version = 10.0.2
|
||||||
|
modtime = 1771702401.801159000
|
||||||
@ -0,0 +1,311 @@
|
|||||||
|
<dashboard version="1.1" script="git_pusher_config.js" hideEdit="true">
|
||||||
|
<label>Git Pusher - Configuration</label>
|
||||||
|
<description>Configuration de l'application Git Pusher</description>
|
||||||
|
|
||||||
|
<row>
|
||||||
|
<panel>
|
||||||
|
<html>
|
||||||
|
<style>
|
||||||
|
.config-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #667eea;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section h2 .icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid input, .config-grid select {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid input:focus, .config-grid select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-hint {
|
||||||
|
grid-column: 2;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 30px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-message {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-message.success {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
border-left: 4px solid #4caf50;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-message.error {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
border-left: 4px solid #f44336;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-status.connected {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-status.disconnected {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-btn {
|
||||||
|
padding: 8px 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #2196F3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-btn:hover {
|
||||||
|
background: #1976D2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-badge {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="config-container">
|
||||||
|
<div class="config-header">
|
||||||
|
<h1>⚙️ Configuration Git Pusher</h1>
|
||||||
|
<p>Configurez les paramètres de l'application</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="config-message" class="config-message"></div>
|
||||||
|
|
||||||
|
<!-- Section API -->
|
||||||
|
<div class="config-section">
|
||||||
|
<h2><span class="icon">🌐</span> Configuration API</h2>
|
||||||
|
<div class="config-grid">
|
||||||
|
<label for="api-url">URL de l'API Git Pusher</label>
|
||||||
|
<input type="text" id="api-url" placeholder="https://myprivspldev-api.example.com" />
|
||||||
|
<span class="config-hint">L'URL du serveur Git Pusher (sans le port si proxy)</span>
|
||||||
|
|
||||||
|
<label for="api-port">Port (si accès direct)</label>
|
||||||
|
<input type="number" id="api-port" placeholder="9999" value="9999" />
|
||||||
|
<span class="config-hint">Port utilisé si accès direct par IP (ignoré si URL de domaine)</span>
|
||||||
|
|
||||||
|
<label for="use-proxy">Utiliser un proxy</label>
|
||||||
|
<input type="checkbox" id="use-proxy" checked="checked" />
|
||||||
|
<span class="config-hint">Coché si vous utilisez un reverse proxy (Nginx, etc.)</span>
|
||||||
|
|
||||||
|
<label>Test de connexion</label>
|
||||||
|
<div>
|
||||||
|
<button class="test-btn" id="test-api-btn">Tester la connexion</button>
|
||||||
|
<span id="api-status" class="config-status disconnected">● Non testé</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section SH Deployer -->
|
||||||
|
<div class="config-section">
|
||||||
|
<h2><span class="icon">🚀</span> Configuration SH Deployer</h2>
|
||||||
|
<div class="config-grid">
|
||||||
|
<label for="deployer-enabled">Activer SH Deployer</label>
|
||||||
|
<input type="checkbox" id="deployer-enabled" />
|
||||||
|
<span class="config-hint">Activer le déploiement automatique vers le Search Head Cluster</span>
|
||||||
|
|
||||||
|
<label for="deployer-host">Adresse du SH Deployer</label>
|
||||||
|
<input type="text" id="deployer-host" placeholder="10.10.40.14" />
|
||||||
|
<span class="config-hint">Adresse IP ou hostname du serveur SH Deployer</span>
|
||||||
|
|
||||||
|
<label for="deployer-port">Port</label>
|
||||||
|
<input type="number" id="deployer-port" placeholder="9998" value="9998" />
|
||||||
|
|
||||||
|
<label for="deployer-token">Token d'authentification</label>
|
||||||
|
<input type="password" id="deployer-token" placeholder="Token secret" />
|
||||||
|
<span class="config-hint">Token configuré dans deployer_agent.py</span>
|
||||||
|
|
||||||
|
<label for="deployer-use-ssl">Utiliser SSL</label>
|
||||||
|
<input type="checkbox" id="deployer-use-ssl" checked="checked" />
|
||||||
|
|
||||||
|
<label>Test de connexion</label>
|
||||||
|
<div>
|
||||||
|
<button class="test-btn" id="test-deployer-btn">Tester la connexion</button>
|
||||||
|
<span id="deployer-status" class="config-status disconnected">● Non testé</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section Licence -->
|
||||||
|
<div class="config-section">
|
||||||
|
<h2><span class="icon">🔐</span> Configuration Licence</h2>
|
||||||
|
<div class="config-grid">
|
||||||
|
<label>Statut de la licence</label>
|
||||||
|
<div id="license-status">Chargement...</div>
|
||||||
|
|
||||||
|
<label for="license-check-interval">Intervalle de vérification (heures)</label>
|
||||||
|
<input type="number" id="license-check-interval" value="24" min="1" max="168" />
|
||||||
|
<span class="config-hint">Fréquence de revalidation de la licence</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section Avancée -->
|
||||||
|
<div class="config-section">
|
||||||
|
<h2><span class="icon">🔧</span> Paramètres avancés</h2>
|
||||||
|
<div class="config-grid">
|
||||||
|
<label for="log-level">Niveau de log</label>
|
||||||
|
<select id="log-level">
|
||||||
|
<option value="INFO">INFO</option>
|
||||||
|
<option value="DEBUG">DEBUG</option>
|
||||||
|
<option value="WARNING">WARNING</option>
|
||||||
|
<option value="ERROR">ERROR</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="timeout">Timeout des requêtes (secondes)</label>
|
||||||
|
<input type="number" id="timeout" value="30" min="5" max="300" />
|
||||||
|
|
||||||
|
<label for="git-timeout">Timeout Git (secondes)</label>
|
||||||
|
<input type="number" id="git-timeout" value="120" min="30" max="600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="config-actions">
|
||||||
|
<button class="btn btn-secondary" id="reset-btn">Réinitialiser</button>
|
||||||
|
<button class="btn btn-primary" id="save-btn">💾 Sauvegarder</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="version-badge">Git Pusher v2.1</div>
|
||||||
|
</html>
|
||||||
|
</panel>
|
||||||
|
</row>
|
||||||
|
</dashboard>
|
||||||
@ -0,0 +1,661 @@
|
|||||||
|
<dashboard version="1.1" script="license_validation.js, git_pusher.js" hideEdit="true" hideExport="true">
|
||||||
|
<label>Git Pusher - Deploy Applications</label>
|
||||||
|
<description>Push Splunk applications to Git repository and deploy to SH Cluster</description>
|
||||||
|
|
||||||
|
<search id="dsearch">
|
||||||
|
<query>| rest /services/apps/local | search disabled=0 | table title, label, description | rename title as name | sort label</query>
|
||||||
|
<earliest>-1m</earliest>
|
||||||
|
<latest>now</latest>
|
||||||
|
</search>
|
||||||
|
|
||||||
|
<row>
|
||||||
|
<panel>
|
||||||
|
<html>
|
||||||
|
<style>
|
||||||
|
/* ============================================ */
|
||||||
|
/* GIT PUSHER STYLES - VERSION 2.2 */
|
||||||
|
/* ============================================ */
|
||||||
|
|
||||||
|
.git-pusher-container {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header avec badge de licence */
|
||||||
|
.header-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-badge {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container pour le badge de licence */
|
||||||
|
#license-badge-container {
|
||||||
|
min-width: 200px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grille principale */
|
||||||
|
.main-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.main-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
.section {
|
||||||
|
background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 4px;
|
||||||
|
height: 24px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"],
|
||||||
|
.form-group input[type="password"],
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox personnalisé */
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Liste des applications */
|
||||||
|
#dashboard-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dashboard-list::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dashboard-list::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dashboard-list::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dashboard-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a1a1a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header label {
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-count {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-item:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-item input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-item label {
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-badge {
|
||||||
|
background: #e8f0fe;
|
||||||
|
color: #1a73e8;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section SH Deployer */
|
||||||
|
.deployer-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(145deg, #fff8e1 0%, #ffecb3 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #ffd54f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-status {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-config-btn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #f57c00;
|
||||||
|
color: #f57c00;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-config-btn:hover {
|
||||||
|
background: #f57c00;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-checkbox input {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
accent-color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section sélection apps SH Cluster */
|
||||||
|
.deployer-apps-section {
|
||||||
|
display: none;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px dashed #ffd54f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-apps-section.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-apps-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-apps-header label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-apps-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shcluster-app-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
background: #fff8e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shcluster-app-item:hover {
|
||||||
|
background: #ffecb3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shcluster-app-item input[type="checkbox"] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
accent-color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shcluster-app-item label {
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shcluster-app-item .app-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-auth {
|
||||||
|
display: none;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px dashed #ffd54f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-auth.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-auth-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployer-auth input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ffd54f;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boutons */
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 14px 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Messages */
|
||||||
|
.message {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background: linear-gradient(145deg, #e8f5e9 0%, #c8e6c9 100%);
|
||||||
|
color: #2e7d32;
|
||||||
|
border: 1px solid #a5d6a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: linear-gradient(145deg, #ffebee 0%, #ffcdd2 100%);
|
||||||
|
color: #c62828;
|
||||||
|
border: 1px solid #ef9a9a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading */
|
||||||
|
.loading {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(145deg, #e3f2fd 0%, #bbdefb 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
border: 1px solid #90caf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 3px solid #90caf9;
|
||||||
|
border-top-color: #1976d2;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: #1565c0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="git-pusher-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header-section">
|
||||||
|
<div class="header-title">
|
||||||
|
<h1>🚀 Git Pusher</h1>
|
||||||
|
<span class="version-badge">v2.2</span>
|
||||||
|
</div>
|
||||||
|
<div id="license-badge-container">
|
||||||
|
<!-- Le badge de licence sera inséré ici par JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grille principale -->
|
||||||
|
<div class="main-grid">
|
||||||
|
<!-- Colonne gauche: Applications -->
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">📦 Applications</h2>
|
||||||
|
<div id="dashboard-list">
|
||||||
|
<!-- Liste générée par JavaScript -->
|
||||||
|
<p style="color: #888; text-align: center; padding: 20px;">Loading applications...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonne droite: Configuration -->
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">⚙️ Git Configuration</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="git-url">Repository URL</label>
|
||||||
|
<input type="text" id="git-url" placeholder="https://github.com/user/repo.git" />
|
||||||
|
<div class="form-hint">HTTPS URL of your Git repository</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="git-branch">Branch</label>
|
||||||
|
<input type="text" id="git-branch" value="main" placeholder="main" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="git-token">Access Token / Password</label>
|
||||||
|
<input type="password" id="git-token" placeholder="ghp_xxxx or personal access token" />
|
||||||
|
<div class="form-hint">Personal access token with write permissions</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="commit-message">Commit Message</label>
|
||||||
|
<textarea id="commit-message" placeholder="Describe your changes..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<input type="checkbox" id="save-credentials" />
|
||||||
|
<label for="save-credentials">Remember credentials for next time</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section SH Deployer -->
|
||||||
|
<div class="deployer-section" id="deployer-section">
|
||||||
|
<div class="deployer-header">
|
||||||
|
<div class="deployer-title">
|
||||||
|
🎯 Deploy to Search Head Cluster
|
||||||
|
<span class="deployer-status" id="deployer-status">
|
||||||
|
<span style="color: #888;">● Checking...</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button class="deployer-config-btn" id="deployer-config-btn">⚙️ Configure</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="deployer-checkbox">
|
||||||
|
<input type="checkbox" id="deploy-to-shcluster" />
|
||||||
|
<label for="deploy-to-shcluster">
|
||||||
|
<strong>Enable automatic deployment</strong><br/>
|
||||||
|
<small style="color: #888;">After pushing to Git, pull and apply bundle to SH Cluster</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sélection des apps pour le SH Cluster -->
|
||||||
|
<div class="deployer-apps-section" id="deployer-apps-section">
|
||||||
|
<div class="deployer-apps-header">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="shcluster-all-apps" checked="checked" />
|
||||||
|
<strong>Deploy all selected apps to SH Cluster</strong>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="deployer-apps-list" id="shcluster-apps-list" style="display: none;">
|
||||||
|
<div class="form-hint" style="margin-bottom: 10px;">
|
||||||
|
Select which apps to deploy to the Search Head Cluster:
|
||||||
|
</div>
|
||||||
|
<div id="shcluster-apps-container">
|
||||||
|
<!-- Apps will be populated by JavaScript -->
|
||||||
|
<p style="color: #888; font-style: italic;">Select apps from the left panel first</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="deployer-auth" id="deployer-auth">
|
||||||
|
<div class="form-hint" style="margin-bottom: 10px;">
|
||||||
|
Splunk credentials for applying shcluster-bundle (optional if using default)
|
||||||
|
</div>
|
||||||
|
<div class="deployer-auth-grid">
|
||||||
|
<input type="text" id="sh-auth-user" placeholder="Splunk username (optional)" />
|
||||||
|
<input type="password" id="sh-auth-pass" placeholder="Splunk password (optional)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Boutons -->
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-primary" id="push-btn" onclick="pushDashboards()">
|
||||||
|
✈️ Deploy to Git
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="resetForm(true)">
|
||||||
|
🔄 Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
<div class="loading" id="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span class="loading-text">Deploying applications to Git... Please wait</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message success" id="success-message"></div>
|
||||||
|
<div class="message error" id="error-message"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleDeployerAuth() {
|
||||||
|
var checkbox = document.getElementById('deploy-to-shcluster');
|
||||||
|
var authSection = document.getElementById('deployer-auth');
|
||||||
|
if (checkbox) {
|
||||||
|
if (authSection) {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
authSection.classList.add('visible');
|
||||||
|
} else {
|
||||||
|
authSection.classList.remove('visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
</panel>
|
||||||
|
</row>
|
||||||
|
</dashboard>
|
||||||
Loading…
Reference in new issue