From 3b7384019f7b3db8d5f72b73f153fa44cc21c733 Mon Sep 17 00:00:00 2001 From: AbidiWael Date: Fri, 7 Mar 2025 23:40:01 +0100 Subject: [PATCH 1/4] fix: updating dockerfile and the backend endpoints --- backend/Dockerfile | 3 + backend/app.py | 150 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 122 insertions(+), 31 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 1a91fe2..21b9213 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -4,6 +4,9 @@ FROM python:3.11 # Définir le répertoire de travail dans le conteneur WORKDIR /app +# Installer Docker CLI +RUN apt-get update && apt-get install -y docker.io + # Copier les fichiers nécessaires COPY requirements.txt . diff --git a/backend/app.py b/backend/app.py index c9712c3..29f571c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2,71 +2,93 @@ import os import redis import hashlib import itertools +import logging +import subprocess from flask import Flask, request, jsonify +from flask_cors import CORS # Import pour activer CORS + +# Configuration du logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) app = Flask(__name__) +CORS(app) # Activation de CORS pour toutes les routes # Configuration de Redis -redis_host = os.getenv("REDIS_HOST", "redis") # "redis" correspond au nom du service Redis dans docker-compose +redis_host = os.getenv("REDIS_HOST", "redis") # "redis" correspond au service Redis dans Docker redis_port = int(os.getenv("REDIS_PORT", 6379)) redis_client = redis.StrictRedis(host=redis_host, port=redis_port, db=0, decode_responses=True) +# Clé pour le compteur de requêtes reçues +REQUEST_RECEIVED_KEY = "request_received_count" + +# Initialisation du compteur si inexistant +if not redis_client.exists(REQUEST_RECEIVED_KEY): + redis_client.set(REQUEST_RECEIVED_KEY, 0) + + +@app.before_request +def count_requests(): + """ + Middleware exécuté avant chaque requête pour incrémenter le compteur global de requêtes reçues. + """ + redis_client.incr(REQUEST_RECEIVED_KEY) + logger.info(f"📩 Nouvelle requête reçue ! Nombre total : {redis_client.get(REQUEST_RECEIVED_KEY)}") + + +@app.route("/health", methods=["GET"]) +def health(): + """ Vérifie si l'application fonctionne correctement. """ + logger.info("✅ Vérification du statut de l'application (health check)") + return jsonify({"status": "ok"}), 200 + @app.route("/bruteforce", methods=["POST"]) def bruteforce(): """ Endpoint pour bruteforcer un hash MD5. - Reçoit un hash MD5 via un payload JSON et retourne la chaîne brute correspondante. """ - # Vérifier si le payload contient un hash data = request.json if not data or "hash" not in data: + logger.warning("⚠️ Requête invalide reçue sur /bruteforce") return jsonify({ "status": "fail", "data": None, - "errors": { - "message": "Invalid payload. Please provide a valid hash." - } + "errors": {"message": "Invalid payload. Please provide a valid hash."} }), 400 target_hash = data["hash"] + logger.info(f"🔍 Bruteforce demandé pour le hash : {target_hash}") # Vérifier si le hash existe déjà dans Redis if redis_client.exists(target_hash): original = redis_client.get(target_hash) + logger.info(f"✅ Hash trouvé en cache : {original}") return jsonify({ "status": "success", - "data": { - "hash": target_hash, - "original": original - }, + "data": {"hash": target_hash, "original": original}, "errors": None }), 200 - # Logique de bruteforce + # Logique de brute-force charset = "abcdefghijklmnopqrstuvwxyz0123456789" - for length in range(1, 8): # Limite la longueur des combinaisons pour les performances + for length in range(1, 8): for guess in itertools.product(charset, repeat=length): guess_str = ''.join(guess) if hashlib.md5(guess_str.encode()).hexdigest() == target_hash: - # Ajouter le résultat dans Redis redis_client.set(target_hash, guess_str) + logger.info(f"✅ Hash {target_hash} trouvé : {guess_str}") return jsonify({ "status": "success", - "data": { - "hash": target_hash, - "original": guess_str - }, + "data": {"hash": target_hash, "original": guess_str}, "errors": None }), 200 - # Si aucune correspondance n'est trouvée + logger.warning(f"❌ Aucun résultat trouvé pour le hash : {target_hash}") return jsonify({ "status": "fail", "data": None, - "errors": { - "message": "No match found for the provided hash." - } + "errors": {"message": "No match found."} }), 404 @@ -75,13 +97,13 @@ def get_resolved_hashes(): """ Endpoint pour récupérer tous les hash résolus. """ - # Récupérer toutes les clés et valeurs de Redis keys = redis_client.keys() - resolved_hashes = {key: redis_client.get(key) for key in keys} + resolved_hashes = {key: redis_client.get(key) for key in keys if key != REQUEST_RECEIVED_KEY} + logger.info(f"📜 Liste des hash résolus récupérée ({len(resolved_hashes)} entrées)") return jsonify({ "status": "success", - "data": resolved_hashes, # Retourne un dictionnaire {hash: original} + "data": resolved_hashes, "errors": None }), 200 @@ -93,23 +115,89 @@ def get_resolved_hash(hash_value): """ if redis_client.exists(hash_value): original = redis_client.get(hash_value) + logger.info(f"🔎 Recherche du hash {hash_value} : {original}") return jsonify({ "status": "success", - "data": { - "hash": hash_value, - "original": original - }, + "data": {"hash": hash_value, "original": original}, "errors": None }), 200 else: + logger.warning(f"⚠️ Aucune entrée trouvée pour le hash : {hash_value}") return jsonify({ "status": "fail", "data": None, - "errors": { - "message": f"No resolved entry found for hash: {hash_value}" - } + "errors": {"message": f"No resolved entry found for hash: {hash_value}"} }), 404 +@app.route("/request_count", methods=["GET"]) +def get_request_count(): + """ + Endpoint pour récupérer le nombre total de requêtes reçues. + """ + count = redis_client.get(REQUEST_RECEIVED_KEY) + logger.info(f"📊 Nombre total de requêtes reçues : {count}") + return jsonify({ + "status": "success", + "data": {"request_received_count": int(count) if count else 0}, + "errors": None + }), 200 + + +@app.route("/clear_cache", methods=["DELETE"]) +def clear_cache(): + """ + Endpoint pour effacer toutes les entrées stockées dans Redis (sauf le compteur de requêtes). + """ + keys = redis_client.keys() + for key in keys: + if key != REQUEST_RECEIVED_KEY: + redis_client.delete(key) + + logger.info("🗑️ Cache nettoyé, toutes les entrées supprimées sauf le compteur de requêtes.") + return jsonify({"status": "success", "message": "Cache cleared."}), 200 + + +@app.route("/replicas", methods=["GET"]) +def get_replicas(): + """ + Endpoint pour récupérer le nombre actuel de réplicas du backend dans Docker Swarm. + """ + SERVICE_NAME = "md5_stack_backend" + + try: + result = subprocess.run(["docker", "service", "ls"], capture_output=True, text=True) + for line in result.stdout.split("\n"): + if SERVICE_NAME in line: + replicas = line.split()[3].split("/")[0] # Extraction du nombre de réplicas actuels + logger.info(f"🔢 Nombre de réplicas actuels : {replicas}") + return jsonify({"status": "success", "data": {"replicas": int(replicas)}, "errors": None}), 200 + except Exception as e: + logger.error(f"❌ Erreur récupération réplicas : {e}") + return jsonify({"status": "fail", "data": None, "errors": {"message": str(e)}}), 500 + + return jsonify({"status": "fail", "data": None, "errors": {"message": "Service not found"}}), 404 + + +@app.route("/cpu_usage", methods=["GET"]) +def get_cpu_usage(): + """ + Endpoint pour récupérer l'utilisation CPU actuelle du service backend dans Docker Swarm. + """ + SERVICE_NAME = "md5_stack_backend" + + try: + result = subprocess.run(["docker", "stats", "--no-stream", "--format", "{{.Name}} {{.CPUPerc}}"], + capture_output=True, text=True) + for line in result.stdout.split("\n"): + if SERVICE_NAME in line: + cpu_usage = line.split()[1].replace("%", "") # Extraction de l'utilisation CPU + logger.info(f"⚡ Utilisation CPU actuelle : {cpu_usage}%") + return jsonify({"status": "success", "data": {"cpu_usage": float(cpu_usage)}, "errors": None}), 200 + except Exception as e: + logger.error(f"❌ Erreur récupération CPU : {e}") + return jsonify({"status": "fail", "data": None, "errors": {"message": str(e)}}), 500 + + if __name__ == "__main__": app.run(host="0.0.0.0", port=5000) -- GitLab From 8cd7c0b3e83f4a209677b1cbadbe8bbe3f1c13da Mon Sep 17 00:00:00 2001 From: AbidiWael Date: Fri, 7 Mar 2025 23:43:08 +0100 Subject: [PATCH 2/4] updating requirements --- backend/requirements.txt | Bin 378 -> 416 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index e425e0519941a8bb7a47e956ab57f8133c1f9a8a..5d52d94c6c3b4e58915e291a51dab9134dc20897 100644 GIT binary patch delta 46 vcmeyxw19a-6{A`jLk>eCLoq`(gDyidLq0 Date: Fri, 7 Mar 2025 23:47:23 +0100 Subject: [PATCH 3/4] feat:adding stack file for services creation --- infra/stack.yml | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 infra/stack.yml diff --git a/infra/stack.yml b/infra/stack.yml new file mode 100644 index 0000000..85ebf53 --- /dev/null +++ b/infra/stack.yml @@ -0,0 +1,46 @@ +version: "3.8" + +services: + redis: + image: redis:alpine + container_name: redis-server + networks: + - app_network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 3 + deploy: + restart_policy: + condition: any + + backend: + image: md5_backend:latest + container_name: flask-backend + deploy: + replicas: 1 + restart_policy: + condition: any # Relancer en cas de crash + ports: + - "5000:5000" + networks: + - app_network + environment: + - REDIS_HOST=redis + - REDIS_PORT=6379 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + - redis + privileged: true + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/health"] + interval: 10s + timeout: 5s + retries: 3 + +networks: + app_network: + driver: overlay + name: app_network \ No newline at end of file -- GitLab From fd8e1e8df82ee6a9a3b93a2d3ee3c6c71664bbcd Mon Sep 17 00:00:00 2001 From: AbidiWael Date: Fri, 7 Mar 2025 23:51:01 +0100 Subject: [PATCH 4/4] feat: adding autoscaling logic --- infra/autoscaler.py | 50 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 infra/autoscaler.py diff --git a/infra/autoscaler.py b/infra/autoscaler.py new file mode 100644 index 0000000..6009981 --- /dev/null +++ b/infra/autoscaler.py @@ -0,0 +1,50 @@ +import subprocess +import time +import requests + +SERVICE_NAME = "md5_stack_backend" +THRESHOLD_UP = 50 # Si plus de 50 requêtes en 10s, ajouter un réplica +THRESHOLD_DOWN = 10 # Si moins de 10 requêtes en 10s, réduire un réplica +MIN_REPLICAS = 1 +MAX_REPLICAS = 10 +BACKEND_URL = "http://localhost:5000/request_count" + +def get_request_count(): + """Récupère le nombre total de requêtes reçues par le backend (non seulement traitées)""" + try: + response = requests.get(BACKEND_URL) + if response.status_code == 200: + return response.json()["data"]["request_received_count"] # On récupère request_received_count + except Exception as e: + print(f"❌ Erreur récupération requêtes : {e}") + return 0 + +def get_current_replicas(): + """Récupère le nombre actuel de réplicas""" + result = subprocess.run(["docker", "service", "ls"], capture_output=True, text=True) + for line in result.stdout.split("\n"): + if SERVICE_NAME in line: + return int(line.split()[3].split("/")[0]) # Extrait le nombre actuel de réplicas + return MIN_REPLICAS + +def scale_service(replicas): + """Met à jour le nombre de réplicas du service""" + print(f"⚡ Scaling {SERVICE_NAME} to {replicas} replicas...") + subprocess.run(["docker", "service", "scale", f"{SERVICE_NAME}={replicas}"]) + +while True: + request_count = get_request_count() # Utilisation de request_received_count ici + current_replicas = get_current_replicas() + + print(f"🔍 Requêtes reçues : {request_count} | Réplicas actuels: {current_replicas}") + + if request_count > THRESHOLD_UP and current_replicas < MAX_REPLICAS: + print(f"🔥 Beaucoup de requêtes ({request_count}), augmentation des réplicas...") + scale_service(current_replicas + 1) + elif request_count < THRESHOLD_DOWN and current_replicas > MIN_REPLICAS: + print(f"❄️ Peu de requêtes ({request_count}), réduction des réplicas...") + scale_service(current_replicas - 1) + else: + print("✅ Charge stable, pas de scaling nécessaire.") + + time.sleep(10) # Vérifie toutes les 10 secondes -- GitLab