Pushed by: admin License: TA9O64YS7EPT (Professional) Timestamp: 2026-02-19T23:47:08.023330masterdev
parent
bf90b22250
commit
5446d7d142
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,598 @@
|
||||
// ============================================
|
||||
// SYSTÈME DE VALIDATION DE LICENCE - VERSION 2.0
|
||||
// Avec support fichier .lic
|
||||
// ============================================
|
||||
|
||||
const LICENSE_API_URL = window.location.protocol + '//' + window.location.hostname + ':9999';
|
||||
|
||||
// État global de la licence
|
||||
let currentLicense = null;
|
||||
let currentHostname = null;
|
||||
|
||||
// ============================================
|
||||
// INITIALISATION
|
||||
// ============================================
|
||||
|
||||
async function initializeLicense() {
|
||||
console.log("Initializing license system v2.0...");
|
||||
|
||||
try {
|
||||
// Récupérer le statut de la licence depuis le serveur
|
||||
const response = await fetch(`${LICENSE_API_URL}/license/status`);
|
||||
const data = await response.json();
|
||||
|
||||
console.log("License status:", data);
|
||||
|
||||
currentHostname = data.hostname;
|
||||
|
||||
if (data.status === 'valid' && data.license) {
|
||||
// Licence valide
|
||||
currentLicense = data.license;
|
||||
displayLicenseInfo(data.license);
|
||||
hideLicenseModal();
|
||||
} else {
|
||||
// Pas de licence ou licence invalide
|
||||
showLicenseModal(data.error, data.error_code, data.hostname);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking license:", error);
|
||||
// En cas d'erreur réseau, afficher le modal
|
||||
showLicenseModal("Impossible de vérifier la licence. Le serveur est-il démarré?", "CONNECTION_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// AFFICHAGE DU BADGE DE LICENCE
|
||||
// ============================================
|
||||
|
||||
function displayLicenseInfo(license) {
|
||||
console.log("Displaying license info:", license);
|
||||
|
||||
const container = document.getElementById('license-badge-container');
|
||||
if (!container) {
|
||||
console.error("license-badge-container not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Supprimer l'ancien badge s'il existe
|
||||
const oldBadge = document.getElementById('license-badge');
|
||||
if (oldBadge) oldBadge.remove();
|
||||
|
||||
// Créer le badge
|
||||
const badge = document.createElement('div');
|
||||
badge.id = 'license-badge';
|
||||
|
||||
// Déterminer le style selon le type et les jours restants
|
||||
let badgeStyle = '';
|
||||
let badgeText = '';
|
||||
let badgeIcon = '✓';
|
||||
|
||||
const daysRemaining = license.days_remaining || 0;
|
||||
const licenseType = license.type_name || license.type || 'Unknown';
|
||||
|
||||
if (daysRemaining <= 0) {
|
||||
// Expirée
|
||||
badgeStyle = 'background: linear-gradient(135deg, #f44336 0%, #da190b 100%);';
|
||||
badgeText = '⚠ Licence expirée';
|
||||
badgeIcon = '⚠';
|
||||
} else if (daysRemaining <= 7) {
|
||||
// Expire bientôt
|
||||
badgeStyle = 'background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);';
|
||||
badgeText = `⚠ ${licenseType} - ${daysRemaining}j restants`;
|
||||
badgeIcon = '⚠';
|
||||
} else if (license.type === 'trial') {
|
||||
// Essai
|
||||
badgeStyle = 'background: linear-gradient(135deg, #9c27b0 0%, #7b1fa2 100%);';
|
||||
badgeText = `⏱ Essai - ${daysRemaining}j restants`;
|
||||
badgeIcon = '⏱';
|
||||
} else {
|
||||
// Licence normale valide
|
||||
badgeStyle = 'background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);';
|
||||
badgeText = `✓ ${licenseType}`;
|
||||
badgeIcon = '✓';
|
||||
}
|
||||
|
||||
badge.style.cssText = `
|
||||
${badgeStyle}
|
||||
color: white;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
badge.innerHTML = `
|
||||
<span style="font-size: 14px;">${badgeIcon}</span>
|
||||
<span>${badgeText}</span>
|
||||
`;
|
||||
|
||||
// Click pour voir les détails
|
||||
badge.onclick = function() {
|
||||
showLicenseDetails(license);
|
||||
};
|
||||
|
||||
// Hover effect
|
||||
badge.addEventListener('mouseenter', function() {
|
||||
this.style.transform = 'translateY(-2px)';
|
||||
this.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.3)';
|
||||
});
|
||||
|
||||
badge.addEventListener('mouseleave', function() {
|
||||
this.style.transform = 'translateY(0)';
|
||||
this.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.2)';
|
||||
});
|
||||
|
||||
container.appendChild(badge);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MODAL DE DÉTAILS DE LICENCE
|
||||
// ============================================
|
||||
|
||||
function showLicenseDetails(license) {
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'license-details-modal';
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
`;
|
||||
|
||||
const features = (license.features || []).map(f => `<span style="background: #e3f2fd; color: #1565c0; padding: 4px 8px; border-radius: 4px; font-size: 11px; margin: 2px;">${f}</span>`).join('');
|
||||
|
||||
const limits = license.limits || {};
|
||||
const maxApps = limits.max_apps === -1 ? 'Illimité' : limits.max_apps;
|
||||
const maxPushes = limits.max_pushes_per_day === -1 ? 'Illimité' : limits.max_pushes_per_day + '/jour';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div style="background: white; border-radius: 16px; padding: 30px; max-width: 500px; width: 90%; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 style="margin: 0; color: #333;">📋 Détails de la licence</h2>
|
||||
<button onclick="this.closest('#license-details-modal').remove()" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999;">×</button>
|
||||
</div>
|
||||
|
||||
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div>
|
||||
<div style="color: #666; font-size: 11px; text-transform: uppercase;">Type</div>
|
||||
<div style="font-weight: 600; color: #333;">${license.type_name || license.type}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="color: #666; font-size: 11px; text-transform: uppercase;">ID</div>
|
||||
<div style="font-weight: 600; color: #333; font-family: monospace;">${license.license_id}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="color: #666; font-size: 11px; text-transform: uppercase;">Expire</div>
|
||||
<div style="font-weight: 600; color: #333;">${license.expires}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="color: #666; font-size: 11px; text-transform: uppercase;">Jours restants</div>
|
||||
<div style="font-weight: 600; color: ${license.days_remaining <= 7 ? '#f44336' : '#4caf50'};">${license.days_remaining}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<div style="color: #666; font-size: 11px; text-transform: uppercase; margin-bottom: 5px;">Client</div>
|
||||
<div style="color: #333;">${license.customer?.name || 'N/A'} (${license.customer?.email || 'N/A'})</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<div style="color: #666; font-size: 11px; text-transform: uppercase; margin-bottom: 5px;">Hostname</div>
|
||||
<div style="color: #333; font-family: monospace;">${license.hostname || currentHostname}</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<div style="color: #666; font-size: 11px; text-transform: uppercase; margin-bottom: 5px;">Limites</div>
|
||||
<div style="color: #333;">Apps: <strong>${maxApps}</strong> | Pushes: <strong>${maxPushes}</strong></div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<div style="color: #666; font-size: 11px; text-transform: uppercase; margin-bottom: 8px;">Fonctionnalités</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 4px;">${features || '<span style="color: #999;">Aucune</span>'}</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button onclick="showLicenseModal(null, null, '${currentHostname}')" style="
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
">🔄 Changer de licence</button>
|
||||
<button onclick="this.closest('#license-details-modal').remove()" style="
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
">Fermer</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MODAL D'UPLOAD DE LICENCE
|
||||
// ============================================
|
||||
|
||||
function showLicenseModal(error = null, errorCode = null, hostname = null) {
|
||||
console.log("Showing license modal", { error, errorCode, hostname });
|
||||
|
||||
// Supprimer l'ancien modal s'il existe
|
||||
hideLicenseModal();
|
||||
const detailsModal = document.getElementById('license-details-modal');
|
||||
if (detailsModal) detailsModal.remove();
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'license-modal';
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
`;
|
||||
|
||||
// Message d'erreur si présent
|
||||
let errorHtml = '';
|
||||
if (error) {
|
||||
let errorStyle = 'background: #ffebee; color: #c62828; border-left: 4px solid #f44336;';
|
||||
if (errorCode === 'NO_LICENSE') {
|
||||
errorStyle = 'background: #fff3e0; color: #e65100; border-left: 4px solid #ff9800;';
|
||||
}
|
||||
errorHtml = `
|
||||
<div style="${errorStyle} padding: 12px; border-radius: 4px; margin-bottom: 20px; font-size: 13px;">
|
||||
${error}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
modal.innerHTML = `
|
||||
<div style="background: white; border-radius: 16px; padding: 40px; max-width: 550px; width: 90%; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<div style="text-align: center; margin-bottom: 25px;">
|
||||
<h1 style="font-size: 28px; margin: 0 0 8px 0; color: #333;">🔐 Git Pusher</h1>
|
||||
<p style="color: #666; margin: 0; font-size: 14px;">Activation de licence requise</p>
|
||||
</div>
|
||||
|
||||
${errorHtml}
|
||||
|
||||
<div style="background: #e8f5e9; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #4caf50;">
|
||||
<p style="margin: 0; color: #2e7d32; font-size: 13px;">
|
||||
<strong>📋 Hostname Splunk:</strong><br>
|
||||
<code style="background: white; padding: 4px 8px; border-radius: 4px; font-size: 14px; display: inline-block; margin-top: 5px;">${hostname || 'Chargement...'}</code>
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0; color: #558b2f; font-size: 11px;">
|
||||
Communiquez ce hostname pour obtenir votre licence.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; font-weight: 600; color: #333; margin-bottom: 10px;">
|
||||
📄 Fichier de licence (.lic)
|
||||
</label>
|
||||
|
||||
<div id="license-dropzone" style="
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: #fafafa;
|
||||
" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleDrop(event)" onclick="document.getElementById('license-file-input').click()">
|
||||
<div style="font-size: 40px; margin-bottom: 10px;">📁</div>
|
||||
<div style="color: #666; font-size: 14px;">
|
||||
Glissez votre fichier <strong>.lic</strong> ici<br>
|
||||
<span style="color: #999; font-size: 12px;">ou cliquez pour sélectionner</span>
|
||||
</div>
|
||||
<input type="file" id="license-file-input" accept=".lic" style="display: none;" onchange="handleFileSelect(event)">
|
||||
</div>
|
||||
|
||||
<div id="selected-file-info" style="display: none; margin-top: 10px; padding: 10px; background: #e3f2fd; border-radius: 6px;">
|
||||
<span id="selected-file-name" style="color: #1565c0; font-weight: 500;"></span>
|
||||
<button onclick="clearSelectedFile()" style="float: right; background: none; border: none; color: #f44336; cursor: pointer;">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="license-message" style="display: none; padding: 12px; border-radius: 8px; margin-bottom: 20px; font-size: 14px;"></div>
|
||||
|
||||
<button id="activate-btn" onclick="uploadLicense()" disabled style="
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0.5;
|
||||
">Activer la licence</button>
|
||||
|
||||
<div style="text-align: center; margin-top: 20px;">
|
||||
<a href="#" onclick="showContactInfo(); return false;" style="color: #667eea; text-decoration: none; font-size: 13px;">
|
||||
Besoin d'une licence ? Contactez-nous
|
||||
</a>
|
||||
</div>
|
||||
|
||||
${currentLicense ? `
|
||||
<div style="text-align: center; margin-top: 15px;">
|
||||
<button onclick="hideLicenseModal()" style="background: none; border: none; color: #999; cursor: pointer; font-size: 13px;">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Récupérer le hostname si pas fourni
|
||||
if (!hostname) {
|
||||
fetch(`${LICENSE_API_URL}/license/hostname`)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
const codeEl = modal.querySelector('code');
|
||||
if (codeEl) codeEl.textContent = d.hostname || 'Inconnu';
|
||||
currentHostname = d.hostname;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function hideLicenseModal() {
|
||||
const modal = document.getElementById('license-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GESTION DU FICHIER
|
||||
// ============================================
|
||||
|
||||
let selectedLicenseContent = null;
|
||||
|
||||
function handleDragOver(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.style.borderColor = '#667eea';
|
||||
event.currentTarget.style.background = '#f0f4ff';
|
||||
}
|
||||
|
||||
function handleDragLeave(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.style.borderColor = '#ccc';
|
||||
event.currentTarget.style.background = '#fafafa';
|
||||
}
|
||||
|
||||
function handleDrop(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.style.borderColor = '#ccc';
|
||||
event.currentTarget.style.background = '#fafafa';
|
||||
|
||||
const files = event.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
processFile(files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(event) {
|
||||
const files = event.target.files;
|
||||
if (files.length > 0) {
|
||||
processFile(files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function processFile(file) {
|
||||
console.log("Processing file:", file.name);
|
||||
|
||||
if (!file.name.endsWith('.lic')) {
|
||||
showLicenseMessage('Veuillez sélectionner un fichier .lic', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
selectedLicenseContent = e.target.result;
|
||||
|
||||
// Afficher le nom du fichier
|
||||
document.getElementById('selected-file-info').style.display = 'block';
|
||||
document.getElementById('selected-file-name').textContent = '📄 ' + file.name;
|
||||
|
||||
// Activer le bouton
|
||||
const btn = document.getElementById('activate-btn');
|
||||
btn.disabled = false;
|
||||
btn.style.opacity = '1';
|
||||
btn.style.cursor = 'pointer';
|
||||
|
||||
showLicenseMessage('Fichier prêt à être activé', 'info');
|
||||
};
|
||||
reader.onerror = function() {
|
||||
showLicenseMessage('Erreur de lecture du fichier', 'error');
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function clearSelectedFile() {
|
||||
selectedLicenseContent = null;
|
||||
document.getElementById('selected-file-info').style.display = 'none';
|
||||
document.getElementById('license-file-input').value = '';
|
||||
|
||||
const btn = document.getElementById('activate-btn');
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = '0.5';
|
||||
|
||||
const msgEl = document.getElementById('license-message');
|
||||
msgEl.style.display = 'none';
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UPLOAD ET ACTIVATION
|
||||
// ============================================
|
||||
|
||||
async function uploadLicense() {
|
||||
if (!selectedLicenseContent) {
|
||||
showLicenseMessage('Veuillez sélectionner un fichier de licence', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showLicenseMessage('⏳ Validation en cours...', 'info');
|
||||
|
||||
const btn = document.getElementById('activate-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Validation...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${LICENSE_API_URL}/license/upload`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
license_content: selectedLicenseContent
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log("Upload result:", result);
|
||||
|
||||
if (result.success) {
|
||||
showLicenseMessage('✓ Licence activée avec succès!', 'success');
|
||||
currentLicense = result.license_info;
|
||||
|
||||
setTimeout(() => {
|
||||
hideLicenseModal();
|
||||
displayLicenseInfo(result.license_info);
|
||||
}, 1500);
|
||||
} else {
|
||||
showLicenseMessage(result.error || 'Erreur d\'activation', 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Activer la licence';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Upload error:", error);
|
||||
showLicenseMessage('Erreur de connexion au serveur', 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Activer la licence';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UTILITAIRES
|
||||
// ============================================
|
||||
|
||||
function showLicenseMessage(message, type) {
|
||||
const msgEl = document.getElementById('license-message');
|
||||
if (!msgEl) return;
|
||||
|
||||
msgEl.style.display = 'block';
|
||||
msgEl.textContent = message;
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
msgEl.style.background = '#e8f5e9';
|
||||
msgEl.style.color = '#2e7d32';
|
||||
msgEl.style.border = '1px solid #a5d6a7';
|
||||
break;
|
||||
case 'error':
|
||||
msgEl.style.background = '#ffebee';
|
||||
msgEl.style.color = '#c62828';
|
||||
msgEl.style.border = '1px solid #ef9a9a';
|
||||
break;
|
||||
case 'info':
|
||||
msgEl.style.background = '#e3f2fd';
|
||||
msgEl.style.color = '#1565c0';
|
||||
msgEl.style.border = '1px solid #90caf9';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function showContactInfo() {
|
||||
alert(`Pour obtenir une licence Git Pusher:
|
||||
|
||||
1. Copiez votre hostname Splunk affiché ci-dessus
|
||||
2. Contactez-nous avec:
|
||||
- Votre hostname
|
||||
- Votre email
|
||||
- Le type de licence souhaité
|
||||
|
||||
Email: support@gitpusher.com
|
||||
Site: https://gitpusher.com`);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// VÉRIFICATION AVANT PUSH
|
||||
// ============================================
|
||||
|
||||
async function checkLicenseBeforePush() {
|
||||
try {
|
||||
const response = await fetch(`${LICENSE_API_URL}/license/status`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status !== 'valid') {
|
||||
alert('⚠️ ' + (data.error || 'Licence invalide ou absente'));
|
||||
showLicenseModal(data.error, data.error_code, data.hostname);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérifier les limites
|
||||
const limits = data.license?.limits || {};
|
||||
const usage = data.usage || {};
|
||||
|
||||
if (limits.max_pushes_per_day > 0 && usage.pushes_today >= limits.max_pushes_per_day) {
|
||||
alert(`⚠️ Limite quotidienne atteinte (${limits.max_pushes_per_day} pushes/jour)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("License check error:", error);
|
||||
alert('⚠️ Impossible de vérifier la licence');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EXPORT POUR UTILISATION EXTERNE
|
||||
// ============================================
|
||||
|
||||
window.LicenseManager = {
|
||||
init: initializeLicense,
|
||||
check: checkLicenseBeforePush,
|
||||
showModal: showLicenseModal,
|
||||
getLicense: () => currentLicense,
|
||||
getHostname: () => currentHostname
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1 +1 @@
|
||||
770390
|
||||
3764537
|
||||
|
||||
@ -0,0 +1,758 @@
|
||||
#!/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 SH DEPLOYER
|
||||
# ============================================
|
||||
|
||||
# Configuration du SH Deployer (peut être surchargée par les paramètres)
|
||||
SH_DEPLOYER_CONFIG = {
|
||||
"enabled": True,
|
||||
"host": "10.10.40.14",
|
||||
"port": 9998,
|
||||
"use_ssl": True,
|
||||
"token": "deployer_agent_secret_token_change_me_in_production",
|
||||
"timeout": 30
|
||||
}
|
||||
|
||||
# Configuration du logging
|
||||
log_dir = '/opt/splunk/var/log/splunk'
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
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')
|
||||
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 == '/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}")
|
||||
|
||||
# ============================================
|
||||
# ENDPOINTS LICENCE
|
||||
# ============================================
|
||||
|
||||
if path == '/license/upload':
|
||||
# Uploader une nouvelle licence
|
||||
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:
|
||||
response = save_license_file(license_content)
|
||||
|
||||
self.wfile.write(json.dumps(response).encode())
|
||||
return
|
||||
|
||||
elif path == '/license/delete':
|
||||
# Supprimer la licence
|
||||
license_path = "/opt/splunk/etc/apps/pusher_app_prem/local/license.lic"
|
||||
if os.path.exists(license_path):
|
||||
os.remove(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/'):
|
||||
# Vérifier la licence avant le push
|
||||
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:
|
||||
# Ancien comportement pour compatibilité
|
||||
# Vérifier la licence
|
||||
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
|
||||
|
||||
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]
|
||||
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]
|
||||
|
||||
logger.info(f"Parameters: git_url={git_url}, branch={git_branch}, user={user}, deploy_to_shcluster={deploy_to_shcluster}")
|
||||
|
||||
# Parser les apps
|
||||
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: {e}")
|
||||
apps = []
|
||||
|
||||
logger.info(f"Parsed apps: {len(apps)} items")
|
||||
|
||||
# Vérifier les limites d'apps
|
||||
license_info = validate_license()
|
||||
max_apps = license_info.get("limits", {}).get("max_apps", -1)
|
||||
if max_apps > 0 and len(apps) > max_apps:
|
||||
response = {
|
||||
"status": "error",
|
||||
"error_code": "APP_LIMIT",
|
||||
"message": f"Votre licence permet {max_apps} apps max. Vous en avez sélectionné {len(apps)}."
|
||||
}
|
||||
self.wfile.write(json.dumps(response).encode())
|
||||
return
|
||||
|
||||
# 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...")
|
||||
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):
|
||||
if os.path.exists(dest_path):
|
||||
shutil.rmtree(dest_path)
|
||||
shutil.copytree(app_path, dest_path)
|
||||
logger.info(f"Copied app: {app_name}")
|
||||
else:
|
||||
logger.warning(f"App path not found: {app_path}")
|
||||
|
||||
# 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)
|
||||
|
||||
# Commit et push
|
||||
subprocess.run(['git', 'add', '-A'], cwd=temp_dir, capture_output=True)
|
||||
|
||||
# Ajouter les infos de licence au commit
|
||||
license_info = validate_license()
|
||||
full_message = f"{commit_message}\n\n"
|
||||
full_message += f"Pushed by: {user}\n"
|
||||
full_message += f"License: {license_info.get('license_id', 'N/A')} ({license_info.get('type_name', 'N/A')})\n"
|
||||
full_message += f"Timestamp: {datetime.now().isoformat()}"
|
||||
|
||||
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}")
|
||||
|
||||
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:
|
||||
raise Exception(f"Push failed: {result.stderr}")
|
||||
|
||||
# Incrémenter les stats d'utilisation
|
||||
increment_usage()
|
||||
|
||||
logger.info("Git push successful!")
|
||||
|
||||
# ============================================
|
||||
# DÉPLOIEMENT VERS SH CLUSTER (optionnel)
|
||||
# ============================================
|
||||
|
||||
deployer_result = None
|
||||
|
||||
if deploy_to_shcluster:
|
||||
logger.info("Triggering deployment to SH Cluster...")
|
||||
|
||||
# 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
|
||||
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
|
||||
)
|
||||
|
||||
if deployer_result.get("success"):
|
||||
logger.info("SH Cluster deployment triggered successfully")
|
||||
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_info.get("type_name", "N/A")
|
||||
}
|
||||
|
||||
# Ajouter les infos de déploiement si activé
|
||||
if deploy_to_shcluster:
|
||||
response["shcluster_deployment"] = {
|
||||
"triggered": True,
|
||||
"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"] += " and triggered SH Cluster deployment"
|
||||
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"
|
||||
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):
|
||||
"""
|
||||
Déclencher pull + deploy en une seule opération
|
||||
"""
|
||||
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
|
||||
|
||||
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,485 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Git Pusher - License Validator (Server-side)
|
||||
Ce fichier doit être déployé sur le serveur Splunk dans l'application
|
||||
|
||||
Emplacement: /opt/splunk/etc/apps/pusher_app_prem/bin/license_validator.py
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
from datetime import datetime
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
import logging
|
||||
|
||||
# Configuration du logging
|
||||
log_dir = '/opt/splunk/var/log/splunk'
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(os.path.join(log_dir, 'license_validator.log')),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger('license_validator')
|
||||
|
||||
# ============================================
|
||||
# CONFIGURATION - DOIT CORRESPONDRE AU GÉNÉRATEUR !
|
||||
# ============================================
|
||||
|
||||
# IMPORTANT: Cette clé DOIT être identique à celle du générateur
|
||||
SECRET_KEY = "git_pusher_super_secret_key_2024_change_me_in_production"
|
||||
|
||||
# Chemin vers le fichier de licence
|
||||
LICENSE_FILE_PATH = "/opt/splunk/etc/apps/pusher_app_prem/local/license.lic"
|
||||
|
||||
# Fichier de cache pour les stats d'utilisation
|
||||
USAGE_STATS_PATH = "/opt/splunk/etc/apps/pusher_app_prem/local/usage_stats.json"
|
||||
|
||||
|
||||
def get_splunk_hostname():
|
||||
"""Récupérer le hostname du serveur Splunk"""
|
||||
# Essayer d'abord via l'API Splunk
|
||||
try:
|
||||
import urllib.request
|
||||
import ssl
|
||||
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
splunk_username = os.environ.get('SPLUNK_USERNAME', 'admin')
|
||||
splunk_password = os.environ.get('SPLUNK_PASSWORD', '2312Jocpam!?')
|
||||
credentials = base64.b64encode(f"{splunk_username}:{splunk_password}".encode()).decode()
|
||||
|
||||
req = urllib.request.Request("https://127.0.0.1:8089/services/server/info?output_mode=json")
|
||||
req.add_header('Authorization', f'Basic {credentials}')
|
||||
|
||||
with urllib.request.urlopen(req, timeout=5, context=ssl_context) as response:
|
||||
data = json.loads(response.read().decode('utf-8'))
|
||||
hostname = data.get('entry', [{}])[0].get('content', {}).get('host', '')
|
||||
if hostname:
|
||||
return hostname.lower().strip()
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get hostname via Splunk API: {e}")
|
||||
|
||||
# Fallback sur le hostname système
|
||||
return socket.gethostname().lower().strip()
|
||||
|
||||
|
||||
def create_signature(data_str):
|
||||
"""Créer une signature HMAC-SHA256"""
|
||||
signature = hmac.new(
|
||||
SECRET_KEY.encode('utf-8'),
|
||||
data_str.encode('utf-8'),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
return signature
|
||||
|
||||
|
||||
def verify_signature(data_str, signature):
|
||||
"""Vérifier une signature"""
|
||||
expected = create_signature(data_str)
|
||||
return hmac.compare_digest(expected, signature)
|
||||
|
||||
|
||||
def read_license_file(filepath=None):
|
||||
"""Lire et parser un fichier de licence"""
|
||||
if filepath is None:
|
||||
filepath = LICENSE_FILE_PATH
|
||||
|
||||
try:
|
||||
if not os.path.exists(filepath):
|
||||
return None
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Extraire la partie base64 (ignorer les commentaires)
|
||||
lines = content.strip().split('\n')
|
||||
base64_lines = [l for l in lines if not l.startswith('#') and l.strip()]
|
||||
base64_content = ''.join(base64_lines)
|
||||
|
||||
# Décoder
|
||||
decoded = base64.b64decode(base64_content).decode('utf-8')
|
||||
license_file = json.loads(decoded)
|
||||
|
||||
return license_file
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading license file: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_license_content(content):
|
||||
"""Parser le contenu d'un fichier de licence (pour l'upload)"""
|
||||
try:
|
||||
# Extraire la partie base64 (ignorer les commentaires)
|
||||
lines = content.strip().split('\n')
|
||||
base64_lines = [l for l in lines if not l.startswith('#') and l.strip()]
|
||||
base64_content = ''.join(base64_lines)
|
||||
|
||||
# Décoder
|
||||
decoded = base64.b64decode(base64_content).decode('utf-8')
|
||||
license_file = json.loads(decoded)
|
||||
|
||||
return license_file
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing license content: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def validate_license(license_file=None, current_hostname=None):
|
||||
"""
|
||||
Valider une licence
|
||||
|
||||
Args:
|
||||
license_file: Contenu du fichier de licence (dict) - si None, lit le fichier par défaut
|
||||
current_hostname: Hostname actuel - si None, détecte automatiquement
|
||||
|
||||
Returns:
|
||||
dict avec {valid: bool, error: str, license_info: dict}
|
||||
"""
|
||||
try:
|
||||
# Lire la licence si non fournie
|
||||
if license_file is None:
|
||||
license_file = read_license_file()
|
||||
if license_file is None:
|
||||
return {
|
||||
"valid": False,
|
||||
"error": "Aucun fichier de licence trouvé",
|
||||
"error_code": "NO_LICENSE"
|
||||
}
|
||||
|
||||
# Récupérer le hostname si non fourni
|
||||
if current_hostname is None:
|
||||
current_hostname = get_splunk_hostname()
|
||||
|
||||
license_data = license_file.get("license", {})
|
||||
signature = license_file.get("signature", "")
|
||||
|
||||
# Recréer le JSON pour vérifier la signature
|
||||
license_json = json.dumps(license_data, separators=(',', ':'), sort_keys=True)
|
||||
|
||||
# Vérifier la signature
|
||||
if not verify_signature(license_json, signature):
|
||||
return {
|
||||
"valid": False,
|
||||
"error": "Signature invalide - fichier corrompu ou modifié",
|
||||
"error_code": "INVALID_SIGNATURE"
|
||||
}
|
||||
|
||||
# Vérifier le hostname
|
||||
expected_hostname = license_data.get("binding", {}).get("hostname", "").lower()
|
||||
current_hostname_clean = current_hostname.lower().strip()
|
||||
|
||||
if current_hostname_clean != expected_hostname:
|
||||
return {
|
||||
"valid": False,
|
||||
"error": f"Cette licence est pour '{expected_hostname}', pas '{current_hostname_clean}'",
|
||||
"error_code": "HOSTNAME_MISMATCH",
|
||||
"expected_hostname": expected_hostname,
|
||||
"current_hostname": current_hostname_clean
|
||||
}
|
||||
|
||||
# Vérifier l'expiration
|
||||
expires_timestamp = license_data.get("dates", {}).get("expires_timestamp", 0)
|
||||
if datetime.now().timestamp() > expires_timestamp:
|
||||
expires_date = license_data.get("dates", {}).get("expires", "unknown")
|
||||
return {
|
||||
"valid": False,
|
||||
"error": f"Licence expirée le {expires_date}",
|
||||
"error_code": "EXPIRED"
|
||||
}
|
||||
|
||||
# Calculer les jours restants
|
||||
days_remaining = (expires_timestamp - datetime.now().timestamp()) / (24 * 3600)
|
||||
|
||||
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", {}),
|
||||
"expires": license_data.get("dates", {}).get("expires"),
|
||||
"days_remaining": int(days_remaining),
|
||||
"limits": license_data.get("limits", {}),
|
||||
"features": license_data.get("features", []),
|
||||
"hostname": expected_hostname
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Validation error: {e}")
|
||||
return {
|
||||
"valid": False,
|
||||
"error": f"Erreur de validation: {str(e)}",
|
||||
"error_code": "VALIDATION_ERROR"
|
||||
}
|
||||
|
||||
|
||||
def save_license_file(content):
|
||||
"""
|
||||
Sauvegarder un nouveau fichier de licence
|
||||
|
||||
Args:
|
||||
content: Contenu du fichier .lic
|
||||
|
||||
Returns:
|
||||
dict avec {success: bool, error: str}
|
||||
"""
|
||||
try:
|
||||
# Créer le dossier local si nécessaire
|
||||
local_dir = os.path.dirname(LICENSE_FILE_PATH)
|
||||
os.makedirs(local_dir, exist_ok=True)
|
||||
|
||||
# Parser d'abord pour valider
|
||||
license_file = parse_license_content(content)
|
||||
if license_file is None:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Format de fichier de licence invalide"
|
||||
}
|
||||
|
||||
# Valider la licence
|
||||
validation = validate_license(license_file)
|
||||
if not validation.get("valid"):
|
||||
return {
|
||||
"success": False,
|
||||
"error": validation.get("error", "Licence invalide")
|
||||
}
|
||||
|
||||
# Sauvegarder
|
||||
with open(LICENSE_FILE_PATH, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
logger.info(f"License saved: {validation.get('license_id')}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"license_info": validation
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving license: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def get_usage_stats():
|
||||
"""Récupérer les statistiques d'utilisation"""
|
||||
try:
|
||||
if os.path.exists(USAGE_STATS_PATH):
|
||||
with open(USAGE_STATS_PATH, 'r') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"pushes_today": 0,
|
||||
"pushes_total": 0,
|
||||
"last_push_date": None,
|
||||
"apps_pushed": []
|
||||
}
|
||||
|
||||
|
||||
def increment_usage():
|
||||
"""Incrémenter le compteur d'utilisation"""
|
||||
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
|
||||
stats["last_push_date"] = today
|
||||
|
||||
stats["pushes_today"] += 1
|
||||
stats["pushes_total"] += 1
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.dirname(USAGE_STATS_PATH), exist_ok=True)
|
||||
with open(USAGE_STATS_PATH, 'w') as f:
|
||||
json.dump(stats, f)
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving usage stats: {e}")
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def check_limits():
|
||||
"""
|
||||
Vérifier si les limites de la licence sont respectées
|
||||
|
||||
Returns:
|
||||
dict avec {allowed: bool, error: str}
|
||||
"""
|
||||
validation = validate_license()
|
||||
|
||||
if not validation.get("valid"):
|
||||
return {
|
||||
"allowed": False,
|
||||
"error": validation.get("error")
|
||||
}
|
||||
|
||||
limits = validation.get("limits", {})
|
||||
stats = get_usage_stats()
|
||||
|
||||
# Vérifier pushes par jour
|
||||
max_pushes = limits.get("max_pushes_per_day", -1)
|
||||
if max_pushes > 0 and stats.get("pushes_today", 0) >= max_pushes:
|
||||
return {
|
||||
"allowed": False,
|
||||
"error": f"Limite quotidienne atteinte ({max_pushes} pushes/jour)"
|
||||
}
|
||||
|
||||
return {
|
||||
"allowed": True,
|
||||
"license_info": validation,
|
||||
"usage": stats
|
||||
}
|
||||
|
||||
|
||||
# ============================================
|
||||
# API REST POUR L'INTERFACE WEB
|
||||
# ============================================
|
||||
|
||||
class LicenseAPIHandler(BaseHTTPRequestHandler):
|
||||
"""Handler pour les requêtes de l'API licence"""
|
||||
|
||||
def send_cors_headers(self):
|
||||
"""Envoyer les headers CORS complets"""
|
||||
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')
|
||||
self.send_header('Access-Control-Allow-Credentials', 'true')
|
||||
self.send_header('Access-Control-Max-Age', '86400')
|
||||
|
||||
def do_OPTIONS(self):
|
||||
logger.info(f"OPTIONS request from {self.headers.get('Origin', 'unknown')}")
|
||||
self.send_response(200)
|
||||
self.send_cors_headers()
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
def do_GET(self):
|
||||
"""GET /license - Récupérer les infos de licence"""
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.send_cors_headers()
|
||||
self.end_headers()
|
||||
|
||||
try:
|
||||
if '/license/status' in self.path or self.path == '/license':
|
||||
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
|
||||
}
|
||||
|
||||
elif '/license/hostname' in self.path:
|
||||
response = {
|
||||
"hostname": get_splunk_hostname()
|
||||
}
|
||||
|
||||
else:
|
||||
response = {"error": "Unknown endpoint"}
|
||||
|
||||
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):
|
||||
"""POST /license/upload - Uploader une nouvelle licence"""
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.send_cors_headers()
|
||||
self.end_headers()
|
||||
|
||||
try:
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
body = self.rfile.read(content_length).decode('utf-8')
|
||||
|
||||
if '/license/upload' in self.path:
|
||||
# Le body contient le contenu du fichier .lic
|
||||
data = json.loads(body)
|
||||
license_content = data.get('license_content', '')
|
||||
|
||||
if not license_content:
|
||||
response = {"success": False, "error": "Contenu de licence vide"}
|
||||
else:
|
||||
response = save_license_file(license_content)
|
||||
|
||||
elif '/license/delete' in self.path:
|
||||
# Supprimer la licence
|
||||
if os.path.exists(LICENSE_FILE_PATH):
|
||||
os.remove(LICENSE_FILE_PATH)
|
||||
response = {"success": True, "message": "Licence supprimée"}
|
||||
else:
|
||||
response = {"success": False, "error": "Aucune licence à supprimer"}
|
||||
|
||||
else:
|
||||
response = {"error": "Unknown endpoint"}
|
||||
|
||||
self.wfile.write(json.dumps(response).encode())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"POST error: {e}")
|
||||
self.wfile.write(json.dumps({"error": str(e)}).encode())
|
||||
|
||||
def log_message(self, format, *args):
|
||||
logger.debug(format % args)
|
||||
|
||||
|
||||
def start_license_server(port=9998):
|
||||
"""Démarrer le serveur API licence (optionnel, peut être intégré au serveur principal)"""
|
||||
server = HTTPServer(('127.0.0.1', port), LicenseAPIHandler)
|
||||
logger.info(f"License API server listening on 127.0.0.1:{port}")
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
# ============================================
|
||||
# CLI POUR TESTS
|
||||
# ============================================
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1] == "status":
|
||||
result = validate_license()
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
elif sys.argv[1] == "hostname":
|
||||
print(f"Hostname: {get_splunk_hostname()}")
|
||||
|
||||
elif sys.argv[1] == "server":
|
||||
start_license_server()
|
||||
|
||||
else:
|
||||
print("Usage:")
|
||||
print(" python license_validator.py status # Vérifier la licence")
|
||||
print(" python license_validator.py hostname # Afficher le hostname")
|
||||
print(" python license_validator.py server # Démarrer l'API")
|
||||
else:
|
||||
result = validate_license()
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
@ -0,0 +1,6 @@
|
||||
{
|
||||
"total_pushes": 15,
|
||||
"pushes_today": 14,
|
||||
"last_push_date": "2026-02-19",
|
||||
"apps_pushed": []
|
||||
}
|
||||
@ -1 +1 @@
|
||||
{"pushes_today": 2, "pushes_total": 46, "last_push_date": "2026-02-13", "apps_pushed": []}
|
||||
{"pushes_today": 1, "pushes_total": 47, "last_push_date": "2026-02-14", "apps_pushed": []}
|
||||
Loading…
Reference in new issue