first (big) draft
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
tenant_detail.py — Détail des coûts par famille de service (S3, EC2, ECS,
|
||||
Fargate, etc.) pour UN tenant spécifique, converti en EUR par défaut.
|
||||
|
||||
Ce script réutilise providers.py (accès AWS/Azure) et default_period() de
|
||||
cost_report.py : il doit rester dans le même dossier que ces deux modules.
|
||||
|
||||
Utilisation :
|
||||
python tenant_detail.py --tenant analytics-platform
|
||||
python tenant_detail.py --tenant client-c-prod --start 2026-06-01 --end 2026-07-01
|
||||
python tenant_detail.py --tenant analytics-platform --top 15
|
||||
python tenant_detail.py --tenant analytics-platform --json
|
||||
python tenant_detail.py --tenant analytics-platform --no-convert # devise d'origine
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from providers import get_aws_cost, get_azure_cost, convert_result
|
||||
from cost_report import default_period
|
||||
|
||||
|
||||
def find_tenant(config: dict, tenant_name: str):
|
||||
"""Cherche le tenant par son 'name' dans les sections aws/azure de la config.
|
||||
Retourne (provider, entry) ou (None, None) si introuvable."""
|
||||
for entry in config.get("aws", []):
|
||||
if entry["name"] == tenant_name:
|
||||
return "aws", entry
|
||||
for entry in config.get("azure", []):
|
||||
if entry["name"] == tenant_name:
|
||||
return "azure", entry
|
||||
return None, None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Détail des coûts par famille de service pour un tenant donné"
|
||||
)
|
||||
parser.add_argument("--tenant", required=True,
|
||||
help="Nom du tenant tel que défini dans config.json")
|
||||
parser.add_argument("--config", default="config.json", help="Chemin vers le fichier de config")
|
||||
parser.add_argument("--start", help="Date de début (YYYY-MM-DD), défaut: 1er du mois en cours")
|
||||
parser.add_argument("--end", help="Date de fin exclusive (YYYY-MM-DD), défaut: demain")
|
||||
parser.add_argument("--currency", default="EUR", help="Devise cible (défaut: EUR)")
|
||||
parser.add_argument("--no-convert", action="store_true",
|
||||
help="Désactive la conversion, garde la devise d'origine")
|
||||
parser.add_argument("--top", type=int, default=None,
|
||||
help="N'afficher que les N services les plus coûteux")
|
||||
parser.add_argument("--json", action="store_true", help="Sortie en JSON brut")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
with open(args.config) as f:
|
||||
config = json.load(f)
|
||||
except FileNotFoundError:
|
||||
print(f"Fichier de config introuvable : {args.config}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
provider, entry = find_tenant(config, args.tenant)
|
||||
if entry is None:
|
||||
available = [e["name"] for e in config.get("aws", [])] + \
|
||||
[e["name"] for e in config.get("azure", [])]
|
||||
print(f"Tenant '{args.tenant}' introuvable dans {args.config}", file=sys.stderr)
|
||||
print(f"Tenants disponibles : {', '.join(available) or '(aucun)'}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
default_start, default_end = default_period()
|
||||
start = args.start or default_start
|
||||
end = args.end or default_end
|
||||
|
||||
try:
|
||||
if provider == "aws":
|
||||
data = get_aws_cost(entry, start, end)
|
||||
else:
|
||||
data = get_azure_cost(
|
||||
entry["tenant_id"], entry["client_id"], entry["client_secret"],
|
||||
entry["subscription_id"], start, end,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la récupération des coûts pour '{args.tenant}' : {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
target_currency = None if args.no_convert else args.currency
|
||||
if target_currency:
|
||||
data = convert_result(data, target_currency)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(
|
||||
{"tenant": args.tenant, "provider": provider.upper(), "start": start, "end": end, **data},
|
||||
indent=2, ensure_ascii=False,
|
||||
))
|
||||
return
|
||||
|
||||
print(f"\nDétail des coûts — {args.tenant} ({provider.upper()})")
|
||||
print(f"Période : {start} → {end}")
|
||||
|
||||
if data.get("original_currency") and data["original_currency"] != data["currency"]:
|
||||
print(f"Total : {data['total']:.2f} {data['currency']} "
|
||||
f"(orig. {data['original_total']:.2f} {data['original_currency']})")
|
||||
else:
|
||||
print(f"Total : {data['total']:.2f} {data['currency']}")
|
||||
|
||||
if data.get("conversion_error"):
|
||||
print(f"[Conversion échouée, montant affiché dans sa devise d'origine : {data['conversion_error']}]")
|
||||
|
||||
services = list(data.get("by_service", {}).items())
|
||||
if args.top:
|
||||
services = services[:args.top]
|
||||
|
||||
print()
|
||||
if not services:
|
||||
print("Aucun coût détecté sur la période pour ce tenant.")
|
||||
return
|
||||
|
||||
total = data["total"] or 1 # évite division par zéro
|
||||
header = f"{'Famille de service':45} {'Montant':>12} {'%':>6}"
|
||||
print(header)
|
||||
print("-" * len(header))
|
||||
for service, amount in services:
|
||||
pct = (amount / total) * 100
|
||||
print(f"{service:45} {amount:>12.2f} {pct:>5.1f}%")
|
||||
print("-" * len(header))
|
||||
print(f"{'TOTAL':45} {data['total']:>12.2f} {'100.0':>5}% ({data['currency']})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user