#!/usr/bin/env python # -*- coding: utf-8 -*- """ Git Pusher - Main Server Serveur HTTP pour pousser les applications Splunk vers Git Avec système de licence par fichier .lic """ 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 # 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 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() } 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""" 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] logger.info(f"Parameters: git_url={git_url}, branch={git_branch}, user={user}") # 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...") 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("Push successful!") 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") } 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') or app.get('name') # Vérifier que app_id n'est pas None if not app_id: logger.warning(f"Skipping app with no ID: {app}") continue 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 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}") # 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__': port = 9999 logger.info(f"Starting Git Pusher on port {port}") start_server(port)