155 lines
6.0 KiB
Python
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()
|