Merge branch 'master' of http://10.10.30.12:3000/admingit/Splunk_Deploiement
commit
9a0b2acca7
@ -0,0 +1,565 @@
|
||||
// ============================================
|
||||
// CHARGER LES DASHBOARDS DYNAMIQUEMENT
|
||||
// ============================================
|
||||
|
||||
// Charger les applications
|
||||
function loadAvailableApps() {
|
||||
console.log("loadAvailableApps called");
|
||||
|
||||
const apiUrl = '/en-US/splunkd/__raw/services/apps/local?output_mode=json&count=0';
|
||||
|
||||
fetch(apiUrl)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log("Apps Response: Found " + (data.entry ? data.entry.length : 0) + " apps");
|
||||
|
||||
if (data.entry && data.entry.length > 0) {
|
||||
const apps = data.entry
|
||||
.filter(item => {
|
||||
// Filtrer les apps système
|
||||
const isHidden = item.content && item.content.is_visible === 0;
|
||||
const appName = item.name;
|
||||
// Exclure les apps système
|
||||
const systemApps = ['launcher', 'splunk_monitoring_console', 'introspection'];
|
||||
return !isHidden && !systemApps.includes(appName);
|
||||
})
|
||||
.map(item => ({
|
||||
id: item.name,
|
||||
name: item.content.label || item.name,
|
||||
description: item.content.description || ''
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
console.log("Filtered apps: " + apps.length);
|
||||
populateAppsList(apps);
|
||||
} else {
|
||||
console.warn("No apps found");
|
||||
showAppsEmpty();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("API Error:", error);
|
||||
showAppsEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
function populateAppsList(apps) {
|
||||
console.log("populateAppsList called with", apps.length, "apps");
|
||||
|
||||
const container = document.getElementById('dashboard-list');
|
||||
|
||||
if (!container) {
|
||||
console.error("dashboard-list container not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apps || apps.length === 0) {
|
||||
showAppsEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="app-header">';
|
||||
html += '<span>Select Applications</span>';
|
||||
html += '<input type="checkbox" id="select-all">';
|
||||
html += '</div>';
|
||||
|
||||
apps.forEach((app, index) => {
|
||||
const checkboxId = 'app-' + index;
|
||||
html += '<div class="app-item">';
|
||||
html += '<input type="checkbox" id="' + checkboxId + '" value="' + app.id + '" data-app="true" data-name="' + app.name + '">';
|
||||
html += '<label for="' + checkboxId + '"><strong>' + app.name + '</strong> <span class="app-badge">' + app.id + '</span>';
|
||||
if (app.description) {
|
||||
html += '<div style="font-size: 11px; color: #999; margin-top: 3px;">' + app.description + '</div>';
|
||||
}
|
||||
html += '</label>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
console.log('Successfully populated ' + apps.length + ' apps');
|
||||
|
||||
// Ajouter les event listeners
|
||||
addCheckboxListeners();
|
||||
}
|
||||
|
||||
function addCheckboxListeners() {
|
||||
console.log("addCheckboxListeners called");
|
||||
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
toggleSelectAll(this);
|
||||
});
|
||||
}
|
||||
|
||||
const appCheckboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app]');
|
||||
appCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
console.log("App checkbox changed");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showAppsEmpty() {
|
||||
const container = document.getElementById('dashboard-list');
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="dashboard-empty">No apps found</div>';
|
||||
}
|
||||
console.log("Displayed empty state");
|
||||
}
|
||||
|
||||
// Attendre que le DOM soit chargé
|
||||
function initScript() {
|
||||
console.log("initScript called");
|
||||
|
||||
// INITIALISER LA LICENCE EN PREMIER
|
||||
initializeLicense();
|
||||
|
||||
// Charger les applications
|
||||
loadAvailableApps();
|
||||
|
||||
// Charger les credentials sauvegardés
|
||||
loadSavedCredentials();
|
||||
|
||||
// Attacher les event listeners au bouton
|
||||
const pushBtn = document.getElementById('push-btn');
|
||||
if (pushBtn) {
|
||||
console.log("Push button found, attaching listener");
|
||||
pushBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
console.log("Push button clicked!");
|
||||
pushDashboards();
|
||||
});
|
||||
} else {
|
||||
console.warn("Push button not found");
|
||||
}
|
||||
|
||||
// Attacher l'event listener pour sauvegarder les credentials
|
||||
const saveCheckbox = document.getElementById('save-credentials');
|
||||
if (saveCheckbox) {
|
||||
saveCheckbox.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
saveCredentials();
|
||||
} else {
|
||||
clearSavedCredentials();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function loadSavedCredentials() {
|
||||
console.log("Loading saved credentials...");
|
||||
|
||||
try {
|
||||
const savedUrl = getCookie('git_pusher_url');
|
||||
const savedToken = getCookie('git_pusher_token');
|
||||
const savedBranch = getCookie('git_pusher_branch');
|
||||
|
||||
if (savedUrl) {
|
||||
document.getElementById('git-url').value = decodeURIComponent(savedUrl);
|
||||
console.log("Loaded saved URL from cookie");
|
||||
}
|
||||
|
||||
if (savedToken) {
|
||||
document.getElementById('git-token').value = decodeURIComponent(savedToken);
|
||||
console.log("Loaded saved token from cookie");
|
||||
}
|
||||
|
||||
if (savedBranch) {
|
||||
document.getElementById('git-branch').value = decodeURIComponent(savedBranch);
|
||||
}
|
||||
|
||||
if (savedUrl && savedToken) {
|
||||
document.getElementById('save-credentials').checked = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Could not load saved credentials:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function saveCredentials() {
|
||||
console.log("Saving credentials...");
|
||||
|
||||
try {
|
||||
const gitUrl = document.getElementById('git-url').value;
|
||||
const gitToken = document.getElementById('git-token').value;
|
||||
const gitBranch = document.getElementById('git-branch').value;
|
||||
|
||||
if (gitUrl && gitToken) {
|
||||
setCookie('git_pusher_url', gitUrl, 30);
|
||||
setCookie('git_pusher_token', gitToken, 30);
|
||||
setCookie('git_pusher_branch', gitBranch, 30);
|
||||
console.log("Credentials saved to cookies");
|
||||
showSuccess("Credentials saved locally");
|
||||
} else {
|
||||
showError("Please fill in URL and Token before saving");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error saving credentials:", e);
|
||||
showError("Could not save credentials");
|
||||
}
|
||||
}
|
||||
|
||||
function clearSavedCredentials() {
|
||||
console.log("Clearing saved credentials...");
|
||||
|
||||
try {
|
||||
deleteCookie('git_pusher_url');
|
||||
deleteCookie('git_pusher_token');
|
||||
deleteCookie('git_pusher_branch');
|
||||
console.log("Credentials cleared from cookies");
|
||||
showSuccess("Credentials cleared");
|
||||
} catch (e) {
|
||||
console.error("Error clearing credentials:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fonctions utilitaires pour les cookies
|
||||
function setCookie(name, value, days) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
const expires = "expires=" + d.toUTCString();
|
||||
document.cookie = name + "=" + encodeURIComponent(value) + ";" + expires + ";path=/";
|
||||
console.log("Cookie set: " + name);
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
const nameEQ = name + "=";
|
||||
const ca = document.cookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i].trim();
|
||||
if (c.indexOf(nameEQ) === 0) {
|
||||
return c.substring(nameEQ.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function deleteCookie(name) {
|
||||
setCookie(name, "", -1);
|
||||
console.log("Cookie deleted: " + name);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log("DOM Ready - Initializing script...");
|
||||
setTimeout(function() {
|
||||
initScript();
|
||||
}, 1000);
|
||||
});
|
||||
} else {
|
||||
console.log("DOM already ready - Initializing script...");
|
||||
setTimeout(function() {
|
||||
initScript();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function getFormKeyValue() {
|
||||
try {
|
||||
// Chercher dans les meta tags du DOM
|
||||
const metaTag = document.querySelector('meta[name="splunk_form_key"]');
|
||||
if (metaTag) {
|
||||
return metaTag.getAttribute('content');
|
||||
}
|
||||
|
||||
// Chercher dans les cookies
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
const [name, value] = cookie.trim().split('=');
|
||||
if (name === 'splunk_form_key') {
|
||||
return decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
|
||||
console.warn("Could not find form key, proceeding without it");
|
||||
return '';
|
||||
} catch (e) {
|
||||
console.error("Error getting form key:", e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function loadAvailableApps() {
|
||||
console.log("loadAvailableApps called");
|
||||
|
||||
const apiUrl = '/en-US/splunkd/__raw/services/apps/local?output_mode=json&count=0';
|
||||
|
||||
fetch(apiUrl)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log("Apps Response: Found " + (data.entry ? data.entry.length : 0) + " apps");
|
||||
|
||||
if (data.entry && data.entry.length > 0) {
|
||||
const apps = data.entry
|
||||
.filter(item => {
|
||||
// Filtrer les apps système
|
||||
const isHidden = item.content && item.content.is_visible === 0;
|
||||
const appName = item.name;
|
||||
// Exclure les apps système
|
||||
const systemApps = ['launcher', 'splunk_monitoring_console', 'introspection'];
|
||||
return !isHidden && !systemApps.includes(appName);
|
||||
})
|
||||
.map(item => ({
|
||||
id: item.name,
|
||||
name: item.content.label || item.name,
|
||||
description: item.content.description || ''
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
console.log("Filtered apps: " + apps.length);
|
||||
populateAppsList(apps);
|
||||
} else {
|
||||
console.warn("No apps found");
|
||||
showAppsEmpty();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("API Error:", error);
|
||||
showAppsEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
function populateAppsList(apps) {
|
||||
console.log("populateAppsList called with", apps.length, "apps");
|
||||
|
||||
const container = document.getElementById('dashboard-list');
|
||||
|
||||
if (!container) {
|
||||
console.error("dashboard-list container not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apps || apps.length === 0) {
|
||||
showAppsEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="select-all-group">';
|
||||
html += '<input type="checkbox" id="select-all" onchange="toggleSelectAll(this)">';
|
||||
html += '<label for="select-all">Select All (' + apps.length + ' apps)</label>';
|
||||
html += '</div>';
|
||||
|
||||
apps.forEach((app, index) => {
|
||||
const checkboxId = 'app-' + index;
|
||||
html += '<div class="dashboard-item">';
|
||||
html += '<input type="checkbox" id="' + checkboxId + '" value="' + app.id + '" data-app="' + app.id + '" data-name="' + app.name + '">';
|
||||
html += '<label for="' + checkboxId + '"><strong>' + app.name + '</strong> <span class="app-badge">' + app.id + '</span>';
|
||||
if (app.description) {
|
||||
html += '<div style="font-size: 11px; color: #666; margin-top: 3px;">' + app.description + '</div>';
|
||||
}
|
||||
html += '</label>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
console.log('Successfully populated ' + apps.length + ' apps');
|
||||
}
|
||||
|
||||
function showAppsEmpty() {
|
||||
const container = document.getElementById('dashboard-list');
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="dashboard-empty">No apps found</div>';
|
||||
}
|
||||
console.log("Displayed empty state");
|
||||
}
|
||||
|
||||
function toggleSelectAll(checkbox) {
|
||||
const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app]');
|
||||
checkboxes.forEach(cb => cb.checked = checkbox.checked);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// POUSSER LES DASHBOARDS VERS GIT
|
||||
// ============================================
|
||||
|
||||
function pushDashboards() {
|
||||
|
||||
// Vérifier la licence
|
||||
if (!checkLicenseBeforePush()) {
|
||||
return;
|
||||
}
|
||||
console.log("pushDashboards called");
|
||||
|
||||
const gitUrl = document.getElementById('git-url').value;
|
||||
const gitBranch = document.getElementById('git-branch').value;
|
||||
const gitToken = document.getElementById('git-token').value;
|
||||
const commitMessage = document.getElementById('commit-message').value;
|
||||
|
||||
console.log("Git URL:", gitUrl);
|
||||
console.log("Git Branch:", gitBranch);
|
||||
console.log("Commit Message:", commitMessage);
|
||||
|
||||
const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"]:not(#select-all):checked');
|
||||
const selectedApps = Array.from(checkboxes).map(cb => ({
|
||||
id: cb.value,
|
||||
name: cb.getAttribute('data-name')
|
||||
}));
|
||||
|
||||
console.log("Selected apps:", selectedApps);
|
||||
|
||||
// Validation
|
||||
if (!gitUrl.trim()) {
|
||||
console.warn("Validation failed: No Git URL");
|
||||
showError('Please enter a Git repository URL');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gitToken.trim()) {
|
||||
console.warn("Validation failed: No Git token");
|
||||
showError('Please enter your Git token or password');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!commitMessage.trim()) {
|
||||
console.warn("Validation failed: No commit message");
|
||||
showError('Please enter a commit message');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedApps.length === 0) {
|
||||
console.warn("Validation failed: No apps selected");
|
||||
showError('Please select at least one application');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Validation passed, showing loading state...");
|
||||
|
||||
// Afficher le loading
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
document.getElementById('success-msg').style.display = 'none';
|
||||
document.getElementById('error-msg').style.display = 'none';
|
||||
document.getElementById('push-btn').disabled = true;
|
||||
|
||||
// Sauvegarder les credentials si la case est cochée
|
||||
if (document.getElementById('save-credentials').checked) {
|
||||
try {
|
||||
setCookie('git_pusher_url', gitUrl, 30);
|
||||
setCookie('git_pusher_token', gitToken, 30);
|
||||
setCookie('git_pusher_branch', gitBranch, 30);
|
||||
console.log("Credentials auto-saved to cookies");
|
||||
} catch (e) {
|
||||
console.warn("Could not auto-save credentials:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Préparer les données - passer les apps au lieu des dashboards
|
||||
const payload = {
|
||||
git_url: gitUrl,
|
||||
git_branch: gitBranch,
|
||||
git_token: gitToken,
|
||||
apps: selectedApps,
|
||||
commit_message: commitMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
user: getCurrentUser()
|
||||
};
|
||||
|
||||
console.log("Payload prepared:", payload);
|
||||
|
||||
// Appeler le script Python via serveur
|
||||
callPushScript(payload);
|
||||
}
|
||||
|
||||
function callPushScript(payload) {
|
||||
console.log("callPushScript called");
|
||||
console.log("Payload:", payload);
|
||||
|
||||
// Construire l'URL vers le serveur Python sur le port 9999 en HTTP
|
||||
const hostname = window.location.hostname;
|
||||
const baseUrl = `http://${hostname}:9999`;
|
||||
|
||||
const url = new URL('/push', baseUrl);
|
||||
|
||||
// Ajouter les paramètres en query string
|
||||
url.searchParams.append('git_url', payload.git_url);
|
||||
url.searchParams.append('git_branch', payload.git_branch);
|
||||
url.searchParams.append('git_token', payload.git_token);
|
||||
url.searchParams.append('commit_message', payload.commit_message);
|
||||
|
||||
// Encoder correctement les apps en JSON
|
||||
const appsJson = JSON.stringify(payload.apps);
|
||||
console.log("Apps JSON:", appsJson);
|
||||
url.searchParams.append('apps', appsJson);
|
||||
|
||||
url.searchParams.append('user', payload.user);
|
||||
|
||||
console.log("Calling:", url.toString());
|
||||
|
||||
fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
mode: 'no-cors'
|
||||
})
|
||||
.then(response => {
|
||||
// Avec no-cors, on ne peut pas lire response.json()
|
||||
// Donc on suppose que si la requête arrive au serveur, c'est bon
|
||||
console.log("Request sent successfully");
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('push-btn').disabled = false;
|
||||
showSuccess('Push request sent! Check server logs for details.');
|
||||
resetForm();
|
||||
return;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fetch error:', error);
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('push-btn').disabled = false;
|
||||
showError('Network error: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentUser() {
|
||||
try {
|
||||
// Essayer plusieurs méthodes
|
||||
if (Splunk && Splunk.util && typeof Splunk.util.getCurrentUser === 'function') {
|
||||
return Splunk.util.getCurrentUser();
|
||||
}
|
||||
|
||||
// Fallback: chercher dans le DOM ou retourner 'unknown'
|
||||
return 'unknown_user';
|
||||
} catch (e) {
|
||||
console.warn("Could not get current user:", e);
|
||||
return 'unknown_user';
|
||||
}
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
const successMsg = document.getElementById('success-msg');
|
||||
document.getElementById('success-text').textContent = message;
|
||||
successMsg.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
successMsg.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const errorMsg = document.getElementById('error-msg');
|
||||
document.getElementById('error-text').textContent = 'X ' + message;
|
||||
errorMsg.style.display = 'block';
|
||||
}
|
||||
|
||||
function resetForm(showConfirm = false) {
|
||||
document.getElementById('git-url').value = '';
|
||||
document.getElementById('git-branch').value = 'main';
|
||||
document.getElementById('git-token').value = '';
|
||||
document.getElementById('commit-message').value = '';
|
||||
document.querySelectorAll('#dashboard-list input[type="checkbox"]').forEach(cb => cb.checked = false);
|
||||
|
||||
// Demander si l'utilisateur veut aussi effacer les credentials sauvegardés
|
||||
// SEULEMENT si showConfirm = true (c'est-à-dire si l'utilisateur a cliqué sur Reset)
|
||||
if (showConfirm && document.getElementById('save-credentials').checked) {
|
||||
const confirmClear = confirm('Do you want to clear saved credentials?');
|
||||
if (confirmClear) {
|
||||
clearSavedCredentials();
|
||||
document.getElementById('save-credentials').checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,368 @@
|
||||
// ============================================
|
||||
// SYSTÈME DE VALIDATION DE LICENCE
|
||||
// ============================================
|
||||
|
||||
const LICENSE_STORAGE_KEY = 'git_pusher_license';
|
||||
|
||||
function initializeLicense() {
|
||||
console.log("Initializing license system...");
|
||||
|
||||
// Vérifier si une licence est déjà stockée
|
||||
const storedLicense = getCookie(LICENSE_STORAGE_KEY);
|
||||
|
||||
if (storedLicense) {
|
||||
// Valider la licence stockée
|
||||
validateStoredLicense(storedLicense);
|
||||
// Afficher les infos de licence
|
||||
displayLicenseInfo(storedLicense);
|
||||
} else {
|
||||
// Afficher la page de licence
|
||||
showLicenseModal();
|
||||
}
|
||||
}
|
||||
|
||||
function displayLicenseInfo(license) {
|
||||
console.log("Displaying license info...");
|
||||
|
||||
// Chercher le container du badge
|
||||
const container = document.getElementById('license-badge-container');
|
||||
if (!container) {
|
||||
console.error("license-badge-container not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Créer le badge
|
||||
const badge = document.createElement('div');
|
||||
badge.id = 'license-badge';
|
||||
badge.style.cssText = `
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
text-align: center;
|
||||
min-width: 200px;
|
||||
`;
|
||||
|
||||
let badgeText = '✓ Licence Activée';
|
||||
|
||||
// Si c'est une licence d'essai
|
||||
if (license.startsWith('TRIAL-')) {
|
||||
const daysRemaining = getTrialDaysRemaining(license);
|
||||
|
||||
if (daysRemaining <= 0) {
|
||||
badgeText = '⏱️ Essai expiré';
|
||||
badge.style.background = 'linear-gradient(135deg, #f44336 0%, #da190b 100%)';
|
||||
} else if (daysRemaining <= 2) {
|
||||
badgeText = `⚠️ ${daysRemaining} jour${daysRemaining > 1 ? 's' : ''} restant${daysRemaining > 1 ? 's' : ''}`;
|
||||
badge.style.background = 'linear-gradient(135deg, #ff9800 0%, #f57c00 100%)';
|
||||
} else {
|
||||
badgeText = `⏱️ Essai: ${daysRemaining} jours`;
|
||||
}
|
||||
}
|
||||
|
||||
badge.textContent = badgeText;
|
||||
badge.onclick = function() {
|
||||
alert('Licence: ' + license.substring(0, 50) + '...\n\nClique sur le logo pour gérer ta licence.');
|
||||
};
|
||||
|
||||
container.appendChild(badge);
|
||||
|
||||
// Ajouter un hover effect
|
||||
badge.addEventListener('mouseenter', function() {
|
||||
this.style.transform = 'translateY(-3px)';
|
||||
this.style.boxShadow = '0 6px 25px rgba(102, 126, 234, 0.5)';
|
||||
});
|
||||
|
||||
badge.addEventListener('mouseleave', function() {
|
||||
this.style.transform = 'translateY(0)';
|
||||
this.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.3)';
|
||||
});
|
||||
}
|
||||
|
||||
function getTrialDaysRemaining(trialLicense) {
|
||||
// Extraire le timestamp du license (format: TRIAL-timestamp)
|
||||
const parts = trialLicense.split('-');
|
||||
if (parts.length !== 2) return 0;
|
||||
|
||||
const timestamp = parseInt(parts[1]);
|
||||
if (isNaN(timestamp)) return 0;
|
||||
|
||||
// Créer la date de création
|
||||
const createdDate = new Date(timestamp);
|
||||
|
||||
// Ajouter 7 jours
|
||||
const expirationDate = new Date(createdDate.getTime() + (7 * 24 * 60 * 60 * 1000));
|
||||
|
||||
// Calculer les jours restants
|
||||
const now = new Date();
|
||||
const daysRemaining = Math.ceil((expirationDate - now) / (1000 * 60 * 60 * 24));
|
||||
|
||||
console.log("Trial created:", createdDate);
|
||||
console.log("Trial expires:", expirationDate);
|
||||
console.log("Days remaining:", daysRemaining);
|
||||
|
||||
return Math.max(0, daysRemaining);
|
||||
}
|
||||
|
||||
function showLicenseModal() {
|
||||
console.log("Showing license modal");
|
||||
|
||||
// Créer le modal HTML
|
||||
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;
|
||||
`;
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = `
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
`;
|
||||
|
||||
content.innerHTML = `
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1 style="font-size: 32px; margin: 0 0 10px 0; color: #333;">🔐 Git Pusher</h1>
|
||||
<p style="color: #666; margin: 0; font-size: 14px;">Activation de licence requise</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f5f7ff; padding: 15px; border-radius: 8px; margin-bottom: 25px; border-left: 4px solid #667eea;">
|
||||
<p style="margin: 0; color: #667eea; font-weight: 500; font-size: 13px;">
|
||||
📋 <strong>Hostname détecté:</strong> <span id="detected-hostname">Chargement...</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; font-weight: 600; color: #333; margin-bottom: 8px;">
|
||||
Entrez votre clé de licence
|
||||
</label>
|
||||
<textarea id="license-input" placeholder="Collez votre clé de licence ici..." style="
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
box-sizing: border-box;
|
||||
"></textarea>
|
||||
<small style="color: #999; display: block; margin-top: 8px;">
|
||||
Vous n'avez pas de licence? <a href="#" onclick="showGeneratorInfo(); return false;" style="color: #667eea; text-decoration: none;">Cliquez ici</a>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div id="license-message" style="display: none; padding: 12px; border-radius: 8px; margin-bottom: 20px; font-size: 14px;"></div>
|
||||
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button onclick="validateLicenseInput()" style="
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
">Activer la licence</button>
|
||||
<button onclick="skipLicense()" style="
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: #f5f7ff;
|
||||
color: #667eea;
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
">Essai gratuit (7 jours)</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.appendChild(content);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Afficher le hostname
|
||||
getHostname().then(hostname => {
|
||||
document.getElementById('detected-hostname').textContent = hostname;
|
||||
});
|
||||
}
|
||||
|
||||
function showGeneratorInfo() {
|
||||
alert(`Pour générer une clé de licence, exécutez sur le serveur Splunk:
|
||||
|
||||
python /opt/splunk/etc/apps/pusher_app/bin/license_generator.py
|
||||
|
||||
Cela générera une clé basée sur votre hostname.`);
|
||||
}
|
||||
|
||||
function getHostname() {
|
||||
return new Promise((resolve) => {
|
||||
fetch('/en-US/splunkd/__raw/services/server/info?output_mode=json')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
const hostname = d.entry?.[0]?.content?.host || 'unknown';
|
||||
resolve(hostname);
|
||||
})
|
||||
.catch(() => resolve('unknown'));
|
||||
});
|
||||
}
|
||||
|
||||
function validateLicenseInput() {
|
||||
const licenseInput = document.getElementById('license-input').value.trim();
|
||||
|
||||
if (!licenseInput) {
|
||||
showLicenseMessage('Veuillez entrer une clé de licence', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Afficher le message de chargement
|
||||
showLicenseMessage('Validation en cours...', 'info');
|
||||
|
||||
// Simuler la validation (en production, faire un appel à un serveur)
|
||||
// Pour l'instant, on accepte juste n'importe quelle licence
|
||||
if (licenseInput.length > 20) {
|
||||
// Stocker la licence
|
||||
setCookie(LICENSE_STORAGE_KEY, licenseInput, 365);
|
||||
|
||||
showLicenseMessage('✓ Licence activée avec succès!', 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
closeLicenseModal();
|
||||
// Afficher les infos de licence
|
||||
displayLicenseInfo(licenseInput);
|
||||
}, 1500);
|
||||
} else {
|
||||
showLicenseMessage('Format de licence invalide', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function skipLicense() {
|
||||
// Créer une licence d'essai avec timestamp (format: TRIAL-timestamp)
|
||||
const trialLicense = 'TRIAL-' + Date.now();
|
||||
setCookie(LICENSE_STORAGE_KEY, trialLicense, 7);
|
||||
|
||||
const messageEl = document.getElementById('license-message');
|
||||
messageEl.style.display = 'block';
|
||||
messageEl.style.background = '#fff3cd';
|
||||
messageEl.style.color = '#856404';
|
||||
messageEl.style.border = '1px solid #ffeaa7';
|
||||
messageEl.textContent = '⏱️ Mode essai activé pour 7 jours';
|
||||
|
||||
setTimeout(() => {
|
||||
closeLicenseModal();
|
||||
// Afficher les infos de licence
|
||||
displayLicenseInfo(trialLicense);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function showLicenseMessage(message, type) {
|
||||
const messageEl = document.getElementById('license-message');
|
||||
messageEl.style.display = 'block';
|
||||
messageEl.textContent = message;
|
||||
|
||||
if (type === 'success') {
|
||||
messageEl.style.background = '#d4edda';
|
||||
messageEl.style.color = '#155724';
|
||||
messageEl.style.border = '1px solid #c3e6cb';
|
||||
} else if (type === 'error') {
|
||||
messageEl.style.background = '#f8d7da';
|
||||
messageEl.style.color = '#721c24';
|
||||
messageEl.style.border = '1px solid #f5c6cb';
|
||||
} else if (type === 'info') {
|
||||
messageEl.style.background = '#d1ecf1';
|
||||
messageEl.style.color = '#0c5460';
|
||||
messageEl.style.border = '1px solid #bee5eb';
|
||||
}
|
||||
}
|
||||
|
||||
function validateStoredLicense(license) {
|
||||
console.log("Validating stored license...");
|
||||
|
||||
// Pour l'instant, accepter simplement la licence stockée
|
||||
// En production, faire une validation serveur
|
||||
if (license && license.length > 5) {
|
||||
console.log("License is valid");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si invalide, afficher le modal à nouveau
|
||||
showLicenseModal();
|
||||
return false;
|
||||
}
|
||||
|
||||
function closeLicenseModal() {
|
||||
const modal = document.getElementById('license-modal');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function checkLicenseBeforePush() {
|
||||
const license = getCookie(LICENSE_STORAGE_KEY);
|
||||
|
||||
if (!license) {
|
||||
alert('Veuillez d\'abord activer une licence');
|
||||
showLicenseModal();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérifier si c'est une licence d'essai expirée
|
||||
if (license.startsWith('TRIAL-')) {
|
||||
// À implémenter : vérifier la date
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FONCTIONS UTILITAIRES DE COOKIE
|
||||
// ============================================
|
||||
|
||||
function setCookie(name, value, days) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
const expires = "expires=" + d.toUTCString();
|
||||
document.cookie = name + "=" + encodeURIComponent(value) + ";" + expires + ";path=/";
|
||||
console.log("Cookie set: " + name);
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
const nameEQ = name + "=";
|
||||
const ca = document.cookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i].trim();
|
||||
if (c.indexOf(nameEQ) === 0) {
|
||||
return decodeURIComponent(c.substring(nameEQ.length));
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function deleteCookie(name) {
|
||||
setCookie(name, "", -1);
|
||||
console.log("Cookie deleted: " + name);
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
This is where you put any scripts you want to add to this app.
|
||||
@ -0,0 +1 @@
|
||||
919562
|
||||
@ -0,0 +1,340 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import tempfile
|
||||
import shutil
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
# 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 do_OPTIONS(self):
|
||||
"""Traiter les requêtes OPTIONS (CORS preflight)"""
|
||||
self.send_response(200)
|
||||
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')
|
||||
self.end_headers()
|
||||
|
||||
def do_POST(self):
|
||||
"""Traiter les requêtes POST"""
|
||||
# Envoyer les headers CORS EN PREMIER
|
||||
self.send_response(200)
|
||||
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')
|
||||
self.end_headers()
|
||||
|
||||
try:
|
||||
logger.info(f"POST request to {self.path}")
|
||||
|
||||
# Parser l'URL et les paramètres
|
||||
parsed_url = urlparse(self.path)
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
|
||||
logger.info(f"Query params keys: {list(query_params.keys())}")
|
||||
|
||||
# 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]
|
||||
|
||||
# Accepter soit 'apps' soit 'dashboards'
|
||||
apps_json = query_params.get('apps', query_params.get('dashboards', ['[]']))[0]
|
||||
user = query_params.get('user', ['unknown'])[0]
|
||||
|
||||
logger.info(f"Parameters received: git_url={git_url}, branch={git_branch}, user={user}")
|
||||
logger.info(f"Raw apps_json: '{apps_json}'")
|
||||
|
||||
# Parser les apps
|
||||
try:
|
||||
# parse_qs décode déjà, mais au cas où
|
||||
if isinstance(apps_json, str):
|
||||
apps = json.loads(apps_json)
|
||||
else:
|
||||
apps = apps_json
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
logger.error(f"JSON parse error: {e} - trying to parse: {apps_json}")
|
||||
apps = []
|
||||
|
||||
logger.info(f"Parsed apps: {len(apps)} items - {apps}")
|
||||
|
||||
# Valider
|
||||
if not git_url or not git_token or not commit_message or not apps:
|
||||
logger.warning(f"Validation failed: git_url={bool(git_url)}, git_token={bool(git_token)}, commit_message={bool(commit_message)}, apps={len(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(f"Git URL prepared (token inserted)")
|
||||
logger.debug(f"Git URL with token: {git_url_with_token}")
|
||||
logger.info("Cloning repository...")
|
||||
self.clone_repository(temp_dir, git_url_with_token, git_branch)
|
||||
|
||||
# Récupérer TOUTES les applications (dossiers complets)
|
||||
logger.info("Fetching applications from Splunk...")
|
||||
dashboard_contents = 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 dashboard_contents:
|
||||
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):
|
||||
logger.info(f"Copying app {app_name} from {app_path}")
|
||||
# Supprimer le dossier s'il existe déjà
|
||||
if os.path.exists(dest_path):
|
||||
logger.info(f"Removing existing app directory: {dest_path}")
|
||||
shutil.rmtree(dest_path)
|
||||
# Copier le dossier
|
||||
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
|
||||
logger.info("Configuring 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
|
||||
logger.info("Adding files...")
|
||||
subprocess.run(['git', 'add', '-A'], cwd=temp_dir, capture_output=True)
|
||||
|
||||
full_message = f"{commit_message}\n\nPushed by: {user}\nTimestamp: {datetime.now().isoformat()}"
|
||||
logger.info("Committing...")
|
||||
result = subprocess.run(['git', 'commit', '-m', full_message],
|
||||
cwd=temp_dir, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.warning(f"Commit may have failed or had no changes: {result.stderr}")
|
||||
|
||||
logger.info("Pushing...")
|
||||
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}")
|
||||
|
||||
logger.info("Push successful!")
|
||||
response = {
|
||||
'status': 'success',
|
||||
'message': f'Successfully pushed {len(dashboard_contents)} dashboards from {len(apps)} application(s) to Git',
|
||||
'dashboards_pushed': len(dashboard_contents)
|
||||
}
|
||||
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"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):
|
||||
"""Éviter les logs HTTP par défaut"""
|
||||
logger.debug(format % args)
|
||||
|
||||
@staticmethod
|
||||
def prepare_git_url(git_url, token):
|
||||
"""Préparer l'URL Git avec le token inséré"""
|
||||
logger.info(f"Preparing git URL with token")
|
||||
|
||||
# Si l'URL contient déjà un token (format: https://user:token@host/repo)
|
||||
# on le remplace
|
||||
if '@' in git_url:
|
||||
# Extraire la partie sans le token
|
||||
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}"
|
||||
|
||||
# Si l'URL est juste https://host/repo (sans credentials)
|
||||
if git_url.startswith('https://') or git_url.startswith('http://'):
|
||||
protocol = git_url.split('://')[0]
|
||||
host_and_path = git_url.split('://', 1)[1]
|
||||
# Insérer le token au format user:token@host ou juste token@host
|
||||
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 complets 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)
|
||||
|
||||
logger.info(f"Checking app directory: {app_path}")
|
||||
|
||||
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} at {app_path}")
|
||||
else:
|
||||
logger.warning(f"App directory not found: {app_path}")
|
||||
|
||||
logger.info(f"Successfully found {len(app_directories)} application directories")
|
||||
return app_directories
|
||||
"""Récupérer TOUS les dashboards de chaque application"""
|
||||
logger.info(f"Fetching dashboards from {len(apps)} applications")
|
||||
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import ssl
|
||||
import base64
|
||||
|
||||
# Ignorer les certificats SSL auto-signés
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
dashboard_contents = []
|
||||
|
||||
# Lire le fichier de configuration Splunk pour obtenir les credentials
|
||||
# Ou utiliser des credentials par défaut
|
||||
splunk_username = os.environ.get('SPLUNK_USERNAME', 'admin')
|
||||
splunk_password = os.environ.get('SPLUNK_PASSWORD', 'changeme')
|
||||
|
||||
# Créer l'authentification Basic
|
||||
credentials = base64.b64encode(f"{splunk_username}:{splunk_password}".encode()).decode()
|
||||
|
||||
for app in apps:
|
||||
app_id = app.get('id') or app.get('app_id')
|
||||
logger.info(f"Fetching all dashboards from app: {app_id}")
|
||||
|
||||
try:
|
||||
# Récupérer la liste de TOUS les dashboards de cette app
|
||||
api_url = f"https://127.0.0.1:8089/servicesNS/-/{app_id}/data/ui/views?output_mode=json&count=0"
|
||||
|
||||
logger.debug(f"API URL: {api_url}")
|
||||
|
||||
req = urllib.request.Request(api_url)
|
||||
req.add_header('Authorization', f'Basic {credentials}')
|
||||
|
||||
with urllib.request.urlopen(req, timeout=15, context=ssl_context) as response:
|
||||
api_data = json.loads(response.read().decode('utf-8'))
|
||||
|
||||
if 'entry' in api_data and len(api_data['entry']) > 0:
|
||||
for entry in api_data['entry']:
|
||||
try:
|
||||
dashboard_id = entry.get('name')
|
||||
content = entry.get('content', {})
|
||||
|
||||
# eai:data contient le XML complet du dashboard
|
||||
dashboard_xml = content.get('eai:data', '')
|
||||
|
||||
if dashboard_xml:
|
||||
dashboard_contents.append({
|
||||
'id': f"{app_id}_{dashboard_id}",
|
||||
'app': app_id,
|
||||
'content': dashboard_xml,
|
||||
'name': dashboard_id
|
||||
})
|
||||
logger.debug(f"Fetched: {dashboard_id} from {app_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing dashboard entry: {str(e)}")
|
||||
|
||||
logger.info(f"Found {len([d for d in dashboard_contents if d['app'] == app_id])} dashboards in {app_id}")
|
||||
else:
|
||||
logger.warning(f"No dashboards found in app {app_id}")
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
logger.error(f"HTTP {e.code} when fetching app {app_id}: {e.reason}")
|
||||
except urllib.error.URLError as e:
|
||||
logger.error(f"Cannot reach Splunk API for app {app_id}: {e.reason}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching dashboards from {app_id}: {str(e)}")
|
||||
|
||||
logger.info(f"Successfully fetched {len(dashboard_contents)} dashboards total")
|
||||
return dashboard_contents
|
||||
|
||||
|
||||
def start_server(port=9999):
|
||||
"""Démarrer le serveur HTTP"""
|
||||
server = HTTPServer(('0.0.0.0', port), GitPusherRequestHandler)
|
||||
logger.info(f"Git Pusher server listening on 0.0.0.0:{port} (HTTP)")
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Démarrer le serveur en background
|
||||
port = 9999
|
||||
logger.info(f"Starting Git Pusher on port {port}")
|
||||
start_server(port)
|
||||
@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Git Pusher - License Generator
|
||||
Génère des clés de licence basées sur le hostname Splunk
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import base64
|
||||
import socket
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
|
||||
# Secret key pour générer les licences (À CHANGER !)
|
||||
SECRET_KEY = "git_pusher_license_secret_2024"
|
||||
|
||||
def get_hostname():
|
||||
"""Récupérer le hostname du serveur"""
|
||||
return socket.gethostname()
|
||||
|
||||
def generate_license(hostname, days_valid=365, max_pushes=None):
|
||||
"""
|
||||
Générer une clé de licence
|
||||
|
||||
Args:
|
||||
hostname: nom d'hôte Splunk
|
||||
days_valid: nombre de jours de validité
|
||||
max_pushes: nombre maximum de pushes (None = illimité)
|
||||
|
||||
Returns:
|
||||
license_key: clé de licence formatée
|
||||
"""
|
||||
|
||||
# Créer la date d'expiration
|
||||
expiration_date = datetime.now() + timedelta(days=days_valid)
|
||||
expiration_str = expiration_date.strftime("%Y-%m-%d")
|
||||
|
||||
# Créer le payload
|
||||
payload = {
|
||||
"hostname": hostname,
|
||||
"expiration": expiration_str,
|
||||
"max_pushes": max_pushes,
|
||||
"issued": datetime.now().strftime("%Y-%m-%d")
|
||||
}
|
||||
|
||||
# Convertir en JSON et encoder en base64
|
||||
payload_json = json.dumps(payload, separators=(',', ':'))
|
||||
payload_b64 = base64.b64encode(payload_json.encode()).decode()
|
||||
|
||||
# Créer la signature HMAC
|
||||
signature = hmac.new(
|
||||
SECRET_KEY.encode(),
|
||||
payload_b64.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()[:16] # Prendre les 16 premiers caractères
|
||||
|
||||
# Formater la clé de licence
|
||||
license_key = f"{signature}-{payload_b64}"
|
||||
|
||||
return license_key, payload
|
||||
|
||||
def validate_license(license_key, hostname):
|
||||
"""
|
||||
Valider une clé de licence
|
||||
|
||||
Args:
|
||||
license_key: clé à valider
|
||||
hostname: hostname Splunk actuel
|
||||
|
||||
Returns:
|
||||
dict: {valid: bool, error: str, expiration: str, max_pushes: int}
|
||||
"""
|
||||
|
||||
try:
|
||||
# Séparer signature et payload
|
||||
parts = license_key.split('-', 1)
|
||||
if len(parts) != 2:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': 'Format de clé invalide'
|
||||
}
|
||||
|
||||
signature, payload_b64 = parts
|
||||
|
||||
# Vérifier la signature
|
||||
expected_signature = hmac.new(
|
||||
SECRET_KEY.encode(),
|
||||
payload_b64.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()[:16]
|
||||
|
||||
if signature != expected_signature:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': 'Signature invalide - clé corrompue ou falsifiée'
|
||||
}
|
||||
|
||||
# Décoder le payload
|
||||
try:
|
||||
payload_json = base64.b64decode(payload_b64).decode()
|
||||
payload = json.loads(payload_json)
|
||||
except Exception as e:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Erreur de décodage: {str(e)}'
|
||||
}
|
||||
|
||||
# Vérifier le hostname
|
||||
if payload.get('hostname') != hostname:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Cette licence est pour {payload.get("hostname")}, pas {hostname}'
|
||||
}
|
||||
|
||||
# Vérifier l'expiration
|
||||
expiration = datetime.strptime(payload.get('expiration'), '%Y-%m-%d')
|
||||
if datetime.now() > expiration:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Licence expirée le {payload.get("expiration")}'
|
||||
}
|
||||
|
||||
return {
|
||||
'valid': True,
|
||||
'expiration': payload.get('expiration'),
|
||||
'max_pushes': payload.get('max_pushes'),
|
||||
'days_remaining': (expiration - datetime.now()).days
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Erreur de validation: {str(e)}'
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
hostname = get_hostname()
|
||||
print("=" * 60)
|
||||
print("Git Pusher - License Generator")
|
||||
print("=" * 60)
|
||||
print(f"\nHostname détecté: {hostname}")
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'validate':
|
||||
# Mode validation
|
||||
license_key = sys.argv[2] if len(sys.argv) > 2 else input("Entrez la clé de licence: ")
|
||||
result = validate_license(license_key, hostname)
|
||||
|
||||
print("\nRésultat de validation:")
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
# Mode génération
|
||||
days = int(sys.argv[1]) if len(sys.argv) > 1 else 365
|
||||
max_pushes = int(sys.argv[2]) if len(sys.argv) > 2 else None
|
||||
|
||||
license_key, payload = generate_license(hostname, days, max_pushes)
|
||||
|
||||
print(f"\n📋 Payload:")
|
||||
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||
|
||||
print(f"\n🔑 Clé de licence générée:")
|
||||
print(license_key)
|
||||
|
||||
print(f"\n✓ Valide pour: {days} jours")
|
||||
if max_pushes:
|
||||
print(f"✓ Pushes limités à: {max_pushes}")
|
||||
|
||||
# Tester la validation
|
||||
print(f"\n✔️ Test de validation:")
|
||||
result = validate_license(license_key, hostname)
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
export SPLUNK_USERNAME=admin
|
||||
export SPLUNK_PASSWORD='2312Jocpam!?'
|
||||
python3 /opt/splunk/etc/apps/pusher_app/bin/git_pusher.py > /opt/splunk/var/log/splunk/git_pusher_startup.log 2>&1 &
|
||||
echo $! > /opt/splunk/etc/apps/pusher_app/bin/git_pusher.pid
|
||||
@ -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 = 1.0.0
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
<nav search_view="search">
|
||||
<view name="search" default='true' />
|
||||
<view name="analytics_workspace" />
|
||||
<view name="datasets" />
|
||||
<view name="reports" />
|
||||
<view name="alerts" />
|
||||
<view name="dashboards" />
|
||||
</nav>
|
||||
@ -0,0 +1 @@
|
||||
Add all the views that your app needs in this directory
|
||||
@ -0,0 +1,3 @@
|
||||
[ui]
|
||||
|
||||
[launcher]
|
||||
@ -0,0 +1,493 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<dashboard version="1.1" script="license_validation.js, git_pusher.js">
|
||||
<label>🚀 Git Pusher - Deploy Applications</label>
|
||||
<description>Modern interface to push Splunk applications to Git repository</description>
|
||||
|
||||
<!-- BADGE DE LICENCE -->
|
||||
<row>
|
||||
<panel>
|
||||
<html>
|
||||
<div id="license-badge-container" style="position: absolute; top: 20px; right: 20px; z-index: 1000;"></div>
|
||||
</html>
|
||||
</panel>
|
||||
</row>
|
||||
|
||||
<search id="dsearch">
|
||||
<query>| rest /services/apps/local | search disabled=0 | fields name, label, description | sort label</query>
|
||||
<earliest>-4h@h</earliest>
|
||||
<latest>now</latest>
|
||||
</search>
|
||||
|
||||
<row>
|
||||
<panel>
|
||||
<html>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
|
||||
margin-bottom: 30px;
|
||||
border-radius: 0 0 20px 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 15px 50px rgba(102, 126, 234, 0.2);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card-title::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 24px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="password"],
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.form-group input[type="text"]:focus,
|
||||
.form-group input[type="password"]:focus,
|
||||
.form-group textarea:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background: #f5f7ff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: #667eea;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
font-size: 13px;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.apps-list {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.apps-list::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.apps-list::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.apps-list::-webkit-scrollbar-thumb {
|
||||
background: #667eea;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
padding: 15px 16px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.app-count {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.app-item:hover {
|
||||
background: #f0f4ff;
|
||||
}
|
||||
|
||||
.app-item input[type="checkbox"] {
|
||||
margin-right: 12px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: #667eea;
|
||||
}
|
||||
|
||||
.app-item label {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.app-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 28px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
box-shadow: 0 6px 25px rgba(102, 126, 234, 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f5f7ff;
|
||||
color: #667eea;
|
||||
border: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
||||
border-radius: 12px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.loading.active {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #e0e0e0;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 16px 20px;
|
||||
border-radius: 12px;
|
||||
margin: 15px 0;
|
||||
display: none;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
|
||||
color: white;
|
||||
border-left: 4px solid white;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: linear-gradient(135deg, #f44336 0%, #da190b 100%);
|
||||
color: white;
|
||||
border-left: 4px solid white;
|
||||
}
|
||||
|
||||
.info-banner {
|
||||
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 20px;
|
||||
color: #667eea;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, #e0e0e0, transparent);
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 40px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="header">
|
||||
<h1>🚀 Git Pusher</h1>
|
||||
<p>Deploy your Splunk applications to Git with confidence</p>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="info-banner">
|
||||
✨ Configure your Git settings below and select the applications you want to deploy to your repository
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Configuration Panel -->
|
||||
<div class="card">
|
||||
<div class="card-title">⚙️ Configuration</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="git-url">Git Repository URL</label>
|
||||
<input type="text" id="git-url" placeholder="https://github.com/username/repo.git" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="git-branch">Target Branch</label>
|
||||
<input type="text" id="git-branch" placeholder="main" value="main" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="git-token">Git Token / Password</label>
|
||||
<input type="password" id="git-token" placeholder="Enter your Git token or password" />
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="save-credentials" />
|
||||
<label for="save-credentials">Save credentials locally</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="commit-message">Commit Message</label>
|
||||
<textarea id="commit-message" placeholder="Describe your changes..."></textarea>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Applications Panel -->
|
||||
<div class="card">
|
||||
<div class="card-title">📦 Applications</div>
|
||||
|
||||
<div class="apps-list" id="dashboard-list">
|
||||
<div style="padding: 30px; text-align: center; color: #999;">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading applications...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<div class="loading-text">Deploying applications to Git... Please wait</div>
|
||||
</div>
|
||||
|
||||
<div class="message success-message" id="success-msg">
|
||||
✅ <span id="success-text">Applications successfully deployed to Git!</span>
|
||||
</div>
|
||||
|
||||
<div class="message error-message" id="error-msg">
|
||||
❌ <span id="error-text">Error occurred while deploying applications</span>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Made with ❤️ for Splunk • Git Pusher v2.0
|
||||
</div>
|
||||
</div>
|
||||
</html>
|
||||
</panel>
|
||||
</row>
|
||||
|
||||
</dashboard>
|
||||
@ -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,26 @@
|
||||
[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 = 1769373002.573917000
|
||||
Loading…
Reference in new issue