#!/usr/bin/env python3 """ cost_report.py — Récupère les coûts de plusieurs tenants AWS et Azure. Utilisation : python cost_report.py # coûts du mois en cours (MTD) python cost_report.py --start 2026-06-01 --end 2026-07-01 python cost_report.py --config mon_config.json python cost_report.py --json # sortie JSON au lieu du tableau Prérequis : pip install -r requirements.txt Config : Copier config.example.json en config.json et remplir tes comptes. Permissions nécessaires : - AWS : le profil doit avoir le droit IAM "ce:GetCostAndUsage" (policy managée "Billing" ou custom policy dessus). - Azure : le service principal doit avoir le rôle "Cost Management Reader" (ou "Reader") sur la subscription visée. """ import argparse import json import sys from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import date, timedelta from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from providers import get_aws_cost, get_azure_cost, convert_result # ---------------------------------------------------------------------- # Orchestration # ---------------------------------------------------------------------- def default_period(): """Du 1er du mois en cours à aujourd'hui (Cost Explorer/Cost Management veulent 'end' exclusif).""" today = date.today() start = today.replace(day=1) end = today + timedelta(days=1) return start.isoformat(), end.isoformat() def run(config_path: str, start: str, end: str, target_currency: str = None): with open(config_path) as f: config = json.load(f) results = [] def handle_aws(entry): name = entry["name"] try: data = get_aws_cost(entry, start, end) if target_currency: data = convert_result(data, target_currency) return {"provider": "AWS", "name": name, "status": "ok", **data} except Exception as e: return {"provider": "AWS", "name": name, "status": "error", "error": str(e)} def handle_azure(entry): name = entry["name"] try: data = get_azure_cost( entry["tenant_id"], entry["client_id"], entry["client_secret"], entry["subscription_id"], start, end, ) if target_currency: data = convert_result(data, target_currency) return {"provider": "Azure", "name": name, "status": "ok", **data} except Exception as e: return {"provider": "Azure", "name": name, "status": "error", "error": str(e)} tasks = [] with ThreadPoolExecutor(max_workers=10) as pool: for entry in config.get("aws", []): tasks.append(pool.submit(handle_aws, entry)) for entry in config.get("azure", []): tasks.append(pool.submit(handle_azure, entry)) for t in as_completed(tasks): results.append(t.result()) return results def print_table(results, start, end): print(f"\nCoûts du {start} au {end}\n") header = f"{'Provider':8} {'Tenant':30} {'Statut':8} {'Total':>12} Devise" print(header) print("-" * len(header)) grand_total_by_currency = {} for r in sorted(results, key=lambda x: (x["provider"], x["name"])): if r["status"] == "ok": line = f"{r['provider']:8} {r['name']:30} {'OK':8} {r['total']:>12.2f} {r['currency']}" if r.get("original_currency") and r["original_currency"] != r["currency"]: line += f" (orig. {r['original_total']:.2f} {r['original_currency']})" if r.get("conversion_error"): line += f" [conversion échouée: {r['conversion_error'][:40]}]" print(line) grand_total_by_currency[r["currency"]] = grand_total_by_currency.get(r["currency"], 0) + r["total"] else: print(f"{r['provider']:8} {r['name']:30} {'ERREUR':8} {r['error'][:60]}") print("-" * len(header)) for currency, total in grand_total_by_currency.items(): print(f"{'TOTAL':8} {'':30} {'':8} {total:>12.2f} {currency}") if len(grand_total_by_currency) > 1: print(" (Attention : plusieurs devises encore présentes, vérifie les erreurs de conversion ci-dessus)") def main(): parser = argparse.ArgumentParser(description="Rapport de coûts multi-tenants AWS/Azure") 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("--json", action="store_true", help="Sortie en JSON brut") parser.add_argument("--detail", action="store_true", help="Afficher la répartition par service") parser.add_argument("--currency", default="EUR", help="Devise cible pour la conversion (défaut: EUR). " "Utiliser --no-convert pour désactiver.") parser.add_argument("--no-convert", action="store_true", help="Désactive la conversion, affiche les montants dans leur devise d'origine") args = parser.parse_args() default_start, default_end = default_period() start = args.start or default_start end = args.end or default_end target_currency = None if args.no_convert else args.currency results = run(args.config, start, end, target_currency) if args.json: print(json.dumps({"start": start, "end": end, "results": results}, indent=2, ensure_ascii=False)) return print_table(results, start, end) if args.detail: for r in results: if r["status"] == "ok" and r.get("by_service"): print(f"\n--- Détail {r['provider']} / {r['name']} ---") for service, amount in list(r["by_service"].items())[:15]: print(f" {service:45} {amount:>10.2f} {r['currency']}") if __name__ == "__main__": main()