netapp_exporter/netapp_exporter.py
2025-03-02 15:37:39 +00:00

216 lines
10 KiB
Python

import os
import logging
import requests
from requests.auth import HTTPBasicAuth
from concurrent.futures import ThreadPoolExecutor
from flask import Flask, request, Response
from prometheus_client import Gauge, Counter, generate_latest
from datetime import datetime
# Désactivation des avertissements SSL (à utiliser avec prudence)
requests.packages.urllib3.disable_warnings()
# Configuration du logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
app = Flask(__name__)
# --------------------------
# Métriques pour les volumes
# --------------------------
volume_used = Gauge("netapp_volume_used_bytes", "Espace utilisé sur un volume", ["volume"])
volume_size = Gauge("netapp_volume_size_bytes", "Taille totale du volume", ["volume"])
volume_available = Gauge("netapp_volume_available_bytes", "Espace disponible sur un volume", ["volume"])
volume_usage_percent = Gauge("netapp_volume_usage_percent", "Pourcentage utilisé d'un volume", ["volume"])
# Mètre les informations de métadonnées du volume (info metric : toujours à 1)
volume_info = Gauge("netapp_volume_info", "Information sur le volume (1=present)",
["volume", "style", "type", "snapshot_policy", "svm"])
# Heure de création du volume en timestamp Unix
volume_create_time = Gauge("netapp_volume_create_time", "Heure de création du volume en timestamp Unix", ["volume"])
# -------------------------------
# Métriques pour les agrégats (tiers)
# -------------------------------
tier_used = Gauge("netapp_tier_used_bytes", "Espace utilisé sur un agrégat", ["tier"])
tier_size = Gauge("netapp_tier_size_bytes", "Taille totale de l'agrégat", ["tier"])
tier_available = Gauge("netapp_tier_available_bytes", "Espace disponible sur un agrégat", ["tier"])
tier_usage_percent = Gauge("netapp_tier_usage_percent", "Pourcentage utilisé d'un agrégat", ["tier"])
tier_full_threshold_percent = Gauge("netapp_tier_full_threshold_percent", "Seuil d'alerte plein d'un agrégat", ["tier"])
tier_physical_used = Gauge("netapp_tier_physical_used_bytes", "Espace utilisé physiquement sur un agrégat", ["tier"])
# --------------------------
# Métriques pour les nœuds (nodes)
# --------------------------
node_uptime = Gauge("netapp_node_uptime_seconds", "Uptime du nœud en secondes", ["node"])
node_state = Gauge("netapp_node_state", "État du nœud (1 = up, 0 = down)", ["node"])
node_cpu_count = Gauge("netapp_node_cpu_count", "Nombre de CPU dans le nœud", ["node"])
node_memory_size = Gauge("netapp_node_memory_size_bytes", "Taille de la mémoire du nœud en octets", ["node"])
# --------------------------
# Compteur pour les requêtes échouées
# --------------------------
failed_requests = Counter("netapp_failed_requests", "Nombre de requêtes NetApp échouées", ["endpoint"])
def fetch_json(url, username, password):
"""Effectue une requête GET et renvoie le JSON en cas de succès."""
try:
response = requests.get(url, auth=HTTPBasicAuth(username, password), verify=False, timeout=10)
if response.status_code == 200:
return response.json()
else:
logging.error(f"Erreur {response.status_code} lors de la récupération de {url}")
failed_requests.labels(endpoint=url).inc()
except Exception as e:
logging.error(f"Exception lors de la récupération de {url} : {e}")
failed_requests.labels(endpoint=url).inc()
return None
def collect_volume_metrics(ontap_ip, username, password):
"""Récupère les métriques pour tous les volumes en appelant leur endpoint détaillé."""
logging.info("Récupération des volumes détaillés...")
volumes_list_url = f"https://{ontap_ip}/api/storage/volumes"
data = fetch_json(volumes_list_url, username, password)
if data is None:
logging.error("Impossible de récupérer la liste des volumes")
return
volumes = data.get("records", data)
for vol in volumes:
vol_name = vol.get("name", "Inconnu")
self_href = vol.get("_links", {}).get("self", {}).get("href")
if not self_href:
logging.error(f"Aucun lien de détail pour le volume {vol_name}")
continue
vol_detail_url = f"https://{ontap_ip}{self_href}"
vol_detail = fetch_json(vol_detail_url, username, password)
if vol_detail is None:
logging.error(f"Impossible de récupérer les détails pour le volume {vol_name}")
continue
# Si la section "space" existe, on récupère les métriques de stockage
space = vol_detail.get("space", {})
if space:
used = space.get("used", 0)
size = space.get("size", 0)
available = space.get("available", 0)
percent_used = (used / size * 100) if size > 0 else 0
volume_used.labels(volume=vol_name).set(used)
volume_size.labels(volume=vol_name).set(size)
volume_available.labels(volume=vol_name).set(available)
volume_usage_percent.labels(volume=vol_name).set(percent_used)
logging.info(f"Volume {vol_name} (space): used={used}, size={size}, available={available}, usage={percent_used:.2f}%")
else:
logging.info(f"Volume {vol_name} n'a pas de métriques 'space'")
# Récupération de la date de création et conversion en timestamp Unix
create_time_str = vol_detail.get("create_time")
ts = 0
if create_time_str:
try:
dt = datetime.fromisoformat(create_time_str.replace("Z", "+00:00"))
ts = dt.timestamp()
except Exception as e:
logging.error(f"Erreur lors du parsing de la date pour le volume {vol_name}: {create_time_str}")
volume_create_time.labels(volume=vol_name).set(ts)
# Collecte des informations statiques en tant que métrique "info"
style = vol_detail.get("style", "unknown")
vtype = vol_detail.get("type", "unknown")
snapshot_policy = vol_detail.get("snapshot_policy", {}).get("name", "unknown")
svm_name = vol_detail.get("svm", {}).get("name", "unknown")
volume_info.labels(volume=vol_name, style=style, type=vtype, snapshot_policy=snapshot_policy, svm=svm_name).set(1)
def collect_tier_metrics(ontap_ip, username, password):
"""Récupère les métriques pour tous les agrégats (tiers) via leur endpoint détaillé."""
logging.info("Récupération des agrégats détaillés...")
aggregates_list_url = f"https://{ontap_ip}/api/storage/aggregates"
data = fetch_json(aggregates_list_url, username, password)
if data is None:
logging.error("Impossible de récupérer la liste des agrégats")
return
aggregates = data.get("records", data)
for agg in aggregates:
tier_name = agg.get("name", "Inconnu")
self_href = agg.get("_links", {}).get("self", {}).get("href")
if not self_href:
logging.error(f"Aucun lien de détail pour l'agrégat {tier_name}")
continue
agg_detail_url = f"https://{ontap_ip}{self_href}"
agg_detail = fetch_json(agg_detail_url, username, password)
if agg_detail is None:
logging.error(f"Impossible de récupérer les détails pour l'agrégat {tier_name}")
continue
block_storage = agg_detail.get("space", {}).get("block_storage", {})
used = block_storage.get("used", 0)
size = block_storage.get("size", 0)
available = block_storage.get("available", 0)
percent_used = (used / size * 100) if size > 0 else 0
tier_used.labels(tier=tier_name).set(used)
tier_size.labels(tier=tier_name).set(size)
tier_available.labels(tier=tier_name).set(available)
tier_usage_percent.labels(tier=tier_name).set(percent_used)
# Nouveaux champs
full_threshold = block_storage.get("full_threshold_percent", 0)
physical_used = block_storage.get("physical_used", 0)
tier_full_threshold_percent.labels(tier=tier_name).set(full_threshold)
tier_physical_used.labels(tier=tier_name).set(physical_used)
logging.info(f"Agrégat {tier_name}: used={used}, size={size}, available={available}, usage={percent_used:.2f}%, threshold={full_threshold}%, physical_used={physical_used}")
def collect_node_metrics(ontap_ip, username, password):
"""Récupère les métriques pour tous les nœuds du cluster."""
logging.info("Récupération des nœuds du cluster...")
nodes_url = f"https://{ontap_ip}/api/cluster/nodes"
data = fetch_json(nodes_url, username, password)
if data is None:
logging.error("Impossible de récupérer la liste des nœuds")
return
nodes = data.get("records", data)
for node in nodes:
node_name = node.get("name", "Inconnu")
uptime = node.get("uptime", 0)
state_str = node.get("state", "").lower()
state_val = 1 if state_str == "up" else 0
cpu_count = node.get("controller", {}).get("cpu", {}).get("count", 0)
memory_size = node.get("controller", {}).get("memory_size", 0)
node_uptime.labels(node=node_name).set(uptime)
node_state.labels(node=node_name).set(state_val)
node_cpu_count.labels(node=node_name).set(cpu_count)
node_memory_size.labels(node=node_name).set(memory_size)
logging.info(f"Nœud {node_name}: uptime={uptime}, state={state_str}, cpu_count={cpu_count}, memory_size={memory_size}")
def collect_metrics(ontap_ip, username, password):
"""Collecte toutes les métriques en parallèle pour volumes, agrégats et nœuds."""
with ThreadPoolExecutor(max_workers=3) as executor:
executor.submit(collect_volume_metrics, ontap_ip, username, password)
executor.submit(collect_tier_metrics, ontap_ip, username, password)
executor.submit(collect_node_metrics, ontap_ip, username, password)
@app.route("/metrics")
def metrics():
"""
Endpoint /metrics qui :
- Lit les paramètres de connexion depuis la query string (ou via les variables d'environnement par défaut)
- Lance la collecte des métriques NetApp
- Retourne les métriques au format Prometheus
"""
ontap_ip = request.args.get("ONTAP_IP") or os.getenv("ONTAP_IP", "default_ip")
username = request.args.get("ONTAP_USER") or os.getenv("ONTAP_USER", "admin")
password = request.args.get("ONTAP_PASS") or os.getenv("ONTAP_PASS", "password")
logging.info(f"Collecte des métriques pour ONTAP_IP={ontap_ip}")
collect_metrics(ontap_ip, username, password)
output = generate_latest()
return Response(output, mimetype="text/plain; version=0.0.4; charset=utf-8")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)