first (big) draft
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user