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