From 450adfdb7387cb374e84d56276711c0f4c9edfd8 Mon Sep 17 00:00:00 2001 From: Fab Date: Fri, 30 Oct 2026 11:48:16 +0100 Subject: [PATCH] recup des versions des dockers (oui, je me suis fait aider :( ) --- dockers/grafana/docker-inventaire/Dockerfile | 11 + .../grafana/docker-inventaire/inventaire.py | 1732 +++++++++++++++++ 2 files changed, 1743 insertions(+) create mode 100644 dockers/grafana/docker-inventaire/Dockerfile create mode 100644 dockers/grafana/docker-inventaire/inventaire.py diff --git a/dockers/grafana/docker-inventaire/Dockerfile b/dockers/grafana/docker-inventaire/Dockerfile new file mode 100644 index 0000000..1e62ba1 --- /dev/null +++ b/dockers/grafana/docker-inventaire/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.13-alpine + +RUN apk add --no-cache git \ + && pip install --no-cache-dir prometheus-client requests + +WORKDIR /app + +COPY inventaire.py . + +CMD ["python", "-u", "inventaire.py"] + diff --git a/dockers/grafana/docker-inventaire/inventaire.py b/dockers/grafana/docker-inventaire/inventaire.py new file mode 100644 index 0000000..5364255 --- /dev/null +++ b/dockers/grafana/docker-inventaire/inventaire.py @@ -0,0 +1,1732 @@ +#!/usr/bin/env python3 + +import os +import re +import tarfile +import tempfile +import threading +import time +import subprocess + +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import quote + +import requests + +from prometheus_client import ( + CollectorRegistry, + Gauge, + generate_latest, +) + + +# ============================================================ +# Configuration +# ============================================================ + +GIT_URL = "https://git.kaz.bzh/KAZ/KazV2.git" +GIT_REF = "master" + +PORT = 9911 + +INVENTORY_INTERVAL = 3600 +REGISTRY_CACHE_TTL = 6 * 3600 + +DOCKER_HUB_REGISTRY_API = "https://registry-1.docker.io" + +session = requests.Session() + +session.headers.update( + { + "User-Agent": "KAZ-docker-inventaire/1.0", + } +) + +registry_cache = {} + +# Cache des tokens : +# +# (registry, repository) -> { +# "token": "...", +# "expires_at": timestamp +# } +# +registry_tokens = {} + +print_lock = threading.Lock() + + +# ============================================================ +# Logs +# ============================================================ + +_original_print = print + + +def log(*args, **kwargs): + timestamp = time.strftime( + "%Y-%m-%d %H:%M:%S +0000", + time.gmtime(), + ) + + with print_lock: + _original_print( + f"[{timestamp}]", + *args, + **kwargs, + ) + + +print = log + + +# ============================================================ +# Prometheus +# ============================================================ + +registry = CollectorRegistry() + + +# ------------------------------------------------------------ +# Version déclarée dans Git +# ------------------------------------------------------------ + +git_metric = Gauge( + "docker_image_git_version_info", + "Docker image version declared in Git", + [ + "project", + "service", + "image", + "version", + ], + registry=registry, +) + + +# ------------------------------------------------------------ +# Dernière version stable disponible dans le registry +# ------------------------------------------------------------ + +source_metric = Gauge( + "docker_image_source_version_info", + "Latest stable Docker image version available from source registry", + [ + "image", + "version", + "registry", + ], + registry=registry, +) + + +# ------------------------------------------------------------ +# Métrique complète destinée à Grafana +# ------------------------------------------------------------ + +version_metric = Gauge( + "docker_image_version_info", + "Docker image installed version and latest stable available version", + [ + "project", + "service", + "image", + "installed_version", + "available_version", + "registry", + ], + registry=registry, +) + + +# ------------------------------------------------------------ +# Indicateur de mise à jour disponible +# ------------------------------------------------------------ + +update_metric = Gauge( + "docker_image_version_update_available", + "Docker image has a newer stable version available", + [ + "project", + "service", + "image", + "installed_version", + "available_version", + "registry", + ], + registry=registry, +) + + +# ============================================================ +# Version parsing +# ============================================================ + +def parse_version(tag): + """ + Extrait une version numérique située au début du tag. + """ + + if not tag: + return None + + tag = tag.strip() + + match = re.match( + r"^v?(\d+(?:[._-]\d+)*)(?![A-Za-z0-9])", + tag, + ) + + if not match: + return None + + value = match.group(1) + + numbers = [ + int(x) + for x in re.split( + r"[._-]", + value, + ) + ] + + if numbers[0] > 999: + return None + + return tuple(numbers) + + +def get_tag_suffix(tag): + """ + Retourne ce qui se trouve après la version numérique. + """ + + if not tag: + return "" + + match = re.match( + r"^v?(\d+(?:[._-]\d+)*)(?![A-Za-z0-9])(.*)$", + tag, + ) + + if not match: + return "" + + suffix = match.group(2) + + suffix = suffix.lstrip( + "._-" + ) + + return suffix + + +def get_build_variant(tag): + """ + Identifie les variantes de distribution/build. + """ + + suffix = get_tag_suffix(tag) + + if not suffix: + return "" + + known_variants = ( + "alpine", + "slim", + "bookworm", + "bullseye", + "buster", + "stretch", + "trixie", + "jammy", + "noble", + "focal", + "debian", + "ubuntu", + "ubi", + "distroless", + ) + + if suffix in known_variants: + return suffix + + match = re.match( + r"^(alpine|bookworm|bullseye|buster|trixie)" + r"\d+(?:\.\d+)*$", + suffix, + ) + + if match: + return match.group(1) + + match = re.match( + r"^(debian|ubuntu|alpine|bookworm|bullseye|trixie)" + r"(?:[-_]\d+(?:\.\d+)*)?$", + suffix, + ) + + if match: + return match.group(1) + + return "" + + +# ============================================================ +# Stable tag filtering +# ============================================================ + +def is_stable_tag(tag): + """ + Détermine si un tag ressemble à une release stable. + """ + + if not tag: + return False + + lower = tag.lower() + + if lower in ( + "latest", + "production", + "stable", + "edge", + "nightly", + "snapshot", + "dev", + "development", + "testing", + "test", + ): + return False + + if re.search( + r"(^|[-_.])" + r"(pr|pull|preview|dev|development|nightly|" + r"snapshot|master|main|branch|commit|sha)" + r"([-. _]|$)", + lower, + ): + return False + + if re.search( + r"(^|[-_.])" + r"(rc|alpha|beta|pre|preview|canary|" + r"unstable|experimental)" + r"(?:[-_.]|\d|$)", + lower, + ): + return False + + if re.search( + r"(^|[-_.])" + r"(amd64|x86_64|arm64|aarch64|armv7|armv6|" + r"386|ppc64le|s390x|riscv64|" + r"windows|win32|linux|darwin|" + r"freebsd|solaris)" + r"([-. _]|$)", + lower, + ): + return False + + return True + + +# ============================================================ +# Docker image +# ============================================================ + +def split_image(image): + """ + Sépare registry / repository / tag. + """ + + image = image.strip() + + registry_name = "docker.io" + + parts = image.split("/") + + if len(parts) > 1 and ( + "." in parts[0] + or ":" in parts[0] + or parts[0] == "localhost" + ): + registry_name = parts[0] + parts = parts[1:] + + if len(parts) == 1: + repository = ( + "library/" + parts[0] + ) + else: + repository = "/".join(parts) + + tag = None + + last = repository.rsplit( + "/", + 1, + )[-1] + + if ":" in last: + repository, tag = repository.rsplit( + ":", + 1, + ) + + if "@" in repository: + repository, digest = repository.split( + "@", + 1, + ) + + tag = digest + + return ( + registry_name, + repository, + tag, + ) + + +# ============================================================ +# Registry token cache +# ============================================================ + +def get_cached_token( + registry_name, + repository, +): + """ + Retourne un token encore valide depuis le cache. + + On garde une marge de sécurité de 60 secondes avant + l'expiration réelle du token. + """ + + cache_key = ( + registry_name, + repository, + ) + + cached = registry_tokens.get( + cache_key + ) + + if not cached: + return None + + expires_at = cached.get( + "expires_at", + 0, + ) + + if time.time() >= expires_at - 60: + + registry_tokens.pop( + cache_key, + None, + ) + + print( + f" Registry : token expiré pour " + f"{registry_name}/{repository}" + ) + + return None + + return cached.get( + "token" + ) + + +def invalidate_registry_token( + registry_name, + repository, +): + """ + Supprime explicitement un token du cache. + """ + + cache_key = ( + registry_name, + repository, + ) + + if cache_key in registry_tokens: + + registry_tokens.pop( + cache_key, + None, + ) + + print( + f" Registry : token invalidé pour " + f"{registry_name}/{repository}" + ) + + +def store_registry_token( + registry_name, + repository, + token, + expires_in, +): + """ + Stocke un token avec sa date d'expiration. + + expires_in est généralement fourni par le serveur + d'authentification. + + Par sécurité, on limite également la durée maximale + du cache à 24 heures. + """ + + try: + expires_in = int( + expires_in + ) + except ( + TypeError, + ValueError, + ): + expires_in = 300 + + expires_in = max( + 60, + min( + expires_in, + 24 * 3600, + ), + ) + + cache_key = ( + registry_name, + repository, + ) + + registry_tokens[ + cache_key + ] = { + "token": token, + "expires_at": ( + time.time() + + expires_in + ), + } + + +# ============================================================ +# Docker Hub authentication +# ============================================================ + +def get_dockerhub_token( + repository, + force_refresh=False, +): + """ + Obtient un token Docker Hub. + + Le token est conservé jusqu'à son expiration. + + force_refresh=True permet de forcer une nouvelle + authentification, notamment après un HTTP 401 du Registry. + """ + + if not force_refresh: + + token = get_cached_token( + "docker.io", + repository, + ) + + if token: + return token + + else: + + invalidate_registry_token( + "docker.io", + repository, + ) + + url = ( + "https://auth.docker.io/token" + "?service=registry.docker.io" + f"&scope=repository:{quote(repository, safe='')}:pull" + ) + + print( + f" Registry : nouveau token Docker Hub pour " + f"{repository}" + ) + + response = session.get( + url, + timeout=30, + ) + + response.raise_for_status() + + data = response.json() + + token = data.get( + "token" + ) + + if not token: + raise RuntimeError( + "Docker Hub : token absent de la réponse" + ) + + expires_in = data.get( + "expires_in", + 300, + ) + + store_registry_token( + "docker.io", + repository, + token, + expires_in, + ) + + return token + + +# ============================================================ +# GHCR authentication +# ============================================================ + +def get_ghcr_token( + repository, + force_refresh=False, +): + """ + Obtient un token GHCR. + + Même mécanisme de cache que Docker Hub. + """ + + if not force_refresh: + + token = get_cached_token( + "ghcr.io", + repository, + ) + + if token: + return token + + else: + + invalidate_registry_token( + "ghcr.io", + repository, + ) + + url = ( + "https://ghcr.io/token" + f"?scope=repository:{quote(repository, safe='')}:pull" + ) + + print( + f" Registry : nouveau token GHCR pour " + f"{repository}" + ) + + response = session.get( + url, + timeout=30, + ) + + response.raise_for_status() + + data = response.json() + + token = data.get( + "token" + ) + + if not token: + raise RuntimeError( + "GHCR : token absent de la réponse" + ) + + expires_in = data.get( + "expires_in", + 300, + ) + + store_registry_token( + "ghcr.io", + repository, + token, + expires_in, + ) + + return token + + +# ============================================================ +# Registry tags +# ============================================================ + +def get_registry_tags( + registry_name, + repository, +): + """ + Récupère TOUS les tags du registry. + + Docker Hub : + registry-1.docker.io + + GHCR : + ghcr.io + + La pagination Link peut être absolue ou relative. + + En cas de HTTP 401 sur un registry authentifié : + 1. le token est supprimé ; + 2. un nouveau token est demandé ; + 3. la requête est rejouée une seule fois. + """ + + cache_key = ( + registry_name, + repository, + ) + + cached = registry_cache.get( + cache_key + ) + + if cached: + + timestamp, tags = cached + + if ( + time.time() - timestamp + < REGISTRY_CACHE_TTL + ): + + print( + f" Registry : cache pour " + f"{registry_name}/{repository}" + ) + + return tags + + print( + f" Registry : récupération des tags de " + f"{registry_name}/{repository}" + ) + + if registry_name == "docker.io": + + base_url = ( + DOCKER_HUB_REGISTRY_API + ) + + token = get_dockerhub_token( + repository + ) + + headers = { + "Authorization": ( + f"Bearer {token}" + ), + } + + authenticated = True + + elif registry_name == "ghcr.io": + + base_url = "https://ghcr.io" + + token = get_ghcr_token( + repository + ) + + headers = { + "Authorization": ( + f"Bearer {token}" + ), + } + + authenticated = True + + else: + + base_url = ( + f"https://{registry_name}" + ) + + headers = {} + + authenticated = False + + url = ( + f"{base_url}/v2/" + f"{repository}/tags/list?n=100" + ) + + tags = [] + + # -------------------------------------------------------- + # Une seule tentative normale + un éventuel renouvellement + # du token après 401. + # -------------------------------------------------------- + + token_refresh_attempted = False + + while url: + + response = session.get( + url, + headers=headers, + timeout=60, + ) + + # ---------------------------------------------------- + # Token refusé. + # + # On ne boucle surtout pas indéfiniment : + # un seul renouvellement est tenté. + # ---------------------------------------------------- + + if ( + response.status_code == 401 + and authenticated + and not token_refresh_attempted + ): + + token_refresh_attempted = True + + print( + f" Registry : HTTP 401 pour " + f"{registry_name}/{repository}" + ) + + print( + f" Registry : renouvellement du token pour " + f"{registry_name}/{repository}" + ) + + if registry_name == "docker.io": + + token = get_dockerhub_token( + repository, + force_refresh=True, + ) + + else: + + token = get_ghcr_token( + repository, + force_refresh=True, + ) + + headers = { + "Authorization": ( + f"Bearer {token}" + ), + } + + # On rejoue exactement la même requête. + continue + + response.raise_for_status() + + data = response.json() + + tags.extend( + data.get( + "tags", + [], + ) + ) + + next_url = None + + link = response.headers.get( + "Link" + ) + + if link: + + match = re.search( + r"<([^>]+)>;\s*rel=\"next\"", + link, + ) + + if match: + + next_url = match.group(1) + + if next_url.startswith("/"): + next_url = ( + base_url + + next_url + ) + + url = next_url + + tags = list( + dict.fromkeys(tags) + ) + + registry_cache[ + cache_key + ] = ( + time.time(), + tags, + ) + + print( + f" Registry : {len(tags)} tags pour " + f"{registry_name}/{repository}" + ) + + return tags + + +# ============================================================ +# Bitnami +# ============================================================ + +def source_version_bitnami( + repository, +): + """ + Bitnami ne fournit pas toujours une liste de tags + exploitable via Registry. + + On cherche les releases dans les PR GitHub. + """ + + image_name = repository.rsplit( + "/", + 1, + )[-1] + + print( + f" GitHub Bitnami : recherche des versions " + f"de {repository}" + ) + + url = ( + "https://api.github.com/search/issues" + ) + + query = ( + f"repo:bitnami/containers " + f'"[bitnami/{image_name}] Release" ' + f"is:pr" + ) + + headers = { + "Accept": ( + "application/vnd.github+json" + ), + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": ( + "KAZ-docker-inventaire/1.0" + ), + } + + response = session.get( + url, + params={ + "q": query, + "per_page": 100, + }, + headers=headers, + timeout=60, + ) + + response.raise_for_status() + + data = response.json() + + versions = [] + + for item in data.get( + "items", + [], + ): + + title = item.get( + "title", + "", + ) + + match = re.search( + rf"\[bitnami/{re.escape(image_name)}\]" + r"\s+Release\s+([0-9][^ ]*)", + title, + ) + + if not match: + continue + + full_version = match.group(1) + + version = parse_version( + full_version + ) + + if not version: + continue + + if not is_stable_tag( + full_version + ): + continue + + numeric = ".".join( + str(x) + for x in version + ) + + versions.append( + ( + version, + numeric, + ) + ) + + if not versions: + return None + + versions.sort( + key=lambda item: item[0] + ) + + return versions[-1][1] + + +# ============================================================ +# Source version +# ============================================================ + +def source_version_for( + image, +): + """ + Retourne la dernière version stable disponible. + """ + + ( + registry_name, + repository, + current_tag, + ) = split_image(image) + + if not current_tag: + return None + + if current_tag in ( + "latest", + "production", + ): + return None + + current_version = parse_version( + current_tag + ) + + if not current_version: + return None + + current_variant = get_build_variant( + current_tag + ) + + # -------------------------------------------------------- + # Bitnami + # -------------------------------------------------------- + + if ( + registry_name == "docker.io" + and repository.startswith( + "bitnami/" + ) + ): + + try: + + version = ( + source_version_bitnami( + repository + ) + ) + + if version: + + print( + f" {repository} -> {version}" + ) + + return version + + except Exception as exc: + + print( + f" Bitnami erreur: {exc}" + ) + + return None + + # -------------------------------------------------------- + # Registry + # -------------------------------------------------------- + + try: + + tags = get_registry_tags( + registry_name, + repository, + ) + + except Exception as exc: + + print( + f" Registry erreur: {exc}" + ) + + return None + + candidates = [] + + for tag in tags: + + if not tag: + continue + + if tag in ( + "latest", + "production", + ): + continue + + if not is_stable_tag( + tag + ): + continue + + version = parse_version( + tag + ) + + if not version: + continue + + variant = get_build_variant( + tag + ) + + if current_variant: + + if variant != current_variant: + continue + + exact = ( + 1 + if tag.startswith("v") + else 0 + ) + + no_suffix = ( + 1 + if get_tag_suffix(tag) == "" + else 0 + ) + + candidates.append( + ( + version, + no_suffix, + exact, + tag, + ) + ) + + if not candidates: + + print( + f" Source : aucune version compatible pour " + f"{image}" + ) + + return None + + candidates.sort( + key=lambda item: ( + item[0], + item[1], + item[2], + ) + ) + + source_version = candidates[-1][3] + + print( + f" Source : {image} -> " + f"{source_version}" + ) + + return source_version + + +# ============================================================ +# Git archive +# ============================================================ + +def git_archive(): + + print( + f"Git : récupération de {GIT_URL}" + ) + + tempdir = tempfile.mkdtemp( + prefix="docker-inventaire-" + ) + + archive_path = os.path.join( + tempdir, + "dockers.tar", + ) + + command = [ + "git", + "archive", + "--remote=" + GIT_URL, + GIT_REF, + "dockers", + ] + + with open( + archive_path, + "wb", + ) as archive: + + process = subprocess.run( + command, + stdout=archive, + stderr=subprocess.PIPE, + ) + + if process.returncode != 0: + + raise RuntimeError( + "git archive a échoué : " + + process.stderr.decode( + errors="replace" + ) + ) + + extract_dir = os.path.join( + tempdir, + "src", + ) + + os.makedirs( + extract_dir, + exist_ok=True, + ) + + with tarfile.open( + archive_path, + "r", + ) as tar: + + for member in tar.getmembers(): + + if ( + member.issym() + or member.islnk() + ): + continue + + tar.extract( + member, + extract_dir, + filter="data", + ) + + print( + "Git : archive extraite" + ) + + return extract_dir + + +# ============================================================ +# Compose parser +# ============================================================ + +def parse_compose_file( + path, +): + """ + Parse le compose comme du texte. + """ + + result = [] + + try: + + with open( + path, + "r", + encoding="utf-8", + errors="replace", + ) as file: + + lines = file.readlines() + + except Exception: + + return result + + current_service = None + service_indent = None + + for line in lines: + + if not line.strip(): + continue + + if line.lstrip().startswith("#"): + continue + + indent = ( + len(line) + - len(line.lstrip()) + ) + + stripped = line.strip() + + match_service = re.match( + r"^([A-Za-z0-9_.-]+):\s*$", + stripped, + ) + + if ( + match_service + and indent >= 2 + ): + + current_service = ( + match_service.group(1) + ) + + service_indent = indent + + continue + + if ( + current_service + and service_indent is not None + and indent > service_indent + ): + + match_image = re.match( + r"^image:\s*['\"]?([^'\"\s]+)", + stripped, + ) + + if match_image: + + image = match_image.group( + 1 + ) + + if ( + image.endswith( + ":latest" + ) + or image.endswith( + ":production" + ) + ): + continue + + if ":" not in image: + continue + + ( + registry_name, + repository, + tag, + ) = split_image(image) + + if not tag: + continue + + if not parse_version(tag): + continue + + result.append( + ( + current_service, + image, + ) + ) + + return result + + +# ============================================================ +# Inventaire +# ============================================================ + +def inventaire(): + + print( + "=" * 50 + ) + + print( + "Début inventaire" + ) + + # -------------------------------------------------------- + # Nettoyage des anciennes métriques + # -------------------------------------------------------- + + git_metric._metrics.clear() + source_metric._metrics.clear() + version_metric._metrics.clear() + update_metric._metrics.clear() + + # -------------------------------------------------------- + # Git + # -------------------------------------------------------- + + try: + + source_dir = git_archive() + + except Exception as exc: + + print( + f"Git erreur : {exc}" + ) + + return + + compose_files = [] + + for root, dirs, files in os.walk( + os.path.join( + source_dir, + "dockers", + ) + ): + + for filename in files: + + if filename in ( + "docker-compose.yml", + "docker-compose.yaml", + "docker-compose.yml.dist", + "docker-compose.yaml.dist", + "docker-compose.tmpl.yml.dist", + "docker-compose.tmpl.yaml.dist", + ): + + compose_files.append( + os.path.join( + root, + filename, + ) + ) + + print( + f"Git : {len(compose_files)} fichiers compose" + ) + + # -------------------------------------------------------- + # Images + # -------------------------------------------------------- + + images = {} + + for compose_file in compose_files: + + relative = os.path.relpath( + compose_file, + os.path.join( + source_dir, + "dockers", + ), + ) + + project = relative.split( + os.sep, + 1, + )[0] + + entries = parse_compose_file( + compose_file + ) + + for service, image in entries: + + ( + registry_name, + repository, + tag, + ) = split_image(image) + + if not tag: + continue + + git_metric.labels( + project=project, + service=service, + image=image, + version=tag, + ).set(1) + + images.setdefault( + image, + [], + ).append( + { + "project": project, + "service": service, + "registry": registry_name, + "installed_version": tag, + } + ) + + # -------------------------------------------------------- + # Suppression des doublons + # -------------------------------------------------------- + + for image in images: + + unique_entries = [] + seen = set() + + for entry in images[image]: + + key = ( + entry["project"], + entry["service"], + entry["registry"], + entry["installed_version"], + ) + + if key in seen: + continue + + seen.add(key) + + unique_entries.append( + entry + ) + + images[image] = unique_entries + + print( + f"Git : {len(images)} images distinctes" + ) + + # -------------------------------------------------------- + # Source + # -------------------------------------------------------- + + print( + "Recherche des versions Source..." + ) + + for image in sorted( + images + ): + + source_version = None + + try: + + ( + registry_name, + repository, + current_tag, + ) = split_image(image) + + source_version = ( + source_version_for( + image + ) + ) + + if source_version: + + source_metric.labels( + image=image, + version=source_version, + registry=registry_name, + ).set(1) + + except Exception as exc: + + print( + f" Erreur Source {image}: " + f"{exc}" + ) + + # ---------------------------------------------------- + # Métrique complète pour Grafana + # ---------------------------------------------------- + + available_version = ( + source_version + if source_version + else "" + ) + + for entry in images[image]: + + installed_version = entry[ + "installed_version" + ] + + version_metric.labels( + project=entry["project"], + service=entry["service"], + image=image, + installed_version=installed_version, + available_version=available_version, + registry=entry["registry"], + ).set(1) + + update_metric.labels( + project=entry["project"], + service=entry["service"], + image=image, + installed_version=installed_version, + available_version=available_version, + registry=entry["registry"], + ).set( + 1 if ( + available_version + and installed_version != available_version + ) else 0 + ) + + print( + "Fin inventaire" + ) + + print( + "=" * 50 + ) + + +# ============================================================ +# HTTP Prometheus +# ============================================================ + +class MetricsHandler( + BaseHTTPRequestHandler +): + + def do_GET(self): + + if self.path != "/metrics": + + self.send_response( + 404 + ) + + self.end_headers() + + return + + output = generate_latest( + registry + ) + + self.send_response( + 200 + ) + + self.send_header( + "Content-Type", + "text/plain; version=0.0.4", + ) + + self.send_header( + "Content-Length", + str(len(output)), + ) + + self.end_headers() + + self.wfile.write( + output + ) + + def log_message( + self, + format, + *args, + ): + return + + +# ============================================================ +# Main +# ============================================================ + +def main(): + + print( + "Démarrage docker-inventaire " + f"sur le port {PORT}" + ) + + server = HTTPServer( + ( + "0.0.0.0", + PORT, + ), + MetricsHandler, + ) + + def run_inventory(): + + while True: + + try: + + inventaire() + + except Exception as exc: + + print( + f"ERREUR inventaire : {exc}" + ) + + time.sleep( + INVENTORY_INTERVAL + ) + + thread = threading.Thread( + target=run_inventory, + daemon=True, + ) + + thread.start() + + server.serve_forever() + + +if __name__ == "__main__": + main()