Files
simple-cost-dashboard/collector/cost_report.py
T
2026-08-07 17:19:49 +02:00

155 lines
6.0 KiB
Python

#!/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()