From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from bombadil.infradead.org (bombadil.infradead.org [198.137.202.133]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A48A713957E for ; Wed, 17 Sep 2025 00:34:52 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=198.137.202.133 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1758069294; cv=none; b=c4dga16kbZ5nOJJLohuXkWJPajxwoZFb3i+5xkSlmgRE61qFBr49GpTt9Q46HnpvWOCt/uaZeTxH4SVrwgjHXXfCrmmYOnNvtQYH+KGhAmIEgjLIJISL9C6cRn9ekJ3MacoI8g3IKFwHnuNzQX3qMcR6jY0IP2YonbgMS772B7A= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1758069294; c=relaxed/simple; bh=gF/Yyf/ujifejdJAAsgnlKqDZOBpcXyBgmlTzehueS8=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=ujRIk8/ZIjKuPAlVpoKvxzvFa4PLQZDrgB16YdsfNXy3KZ92RvyqtSkOW2Dvx5Iwz/JP3Sy2K3vY3cK6bNUX+JzuCefgB/q3C+EoG1RQ1xCi2lkDZ7TZ7Vdd2CawweCASQ4fwPyGGJpbcCq4/WYjHb3s5OZpUfjBejFagc/Ao0c= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=fail (p=quarantine dis=none) header.from=kernel.org; spf=none smtp.mailfrom=infradead.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b=efcO4/i6; arc=none smtp.client-ip=198.137.202.133 Authentication-Results: smtp.subspace.kernel.org; dmarc=fail (p=quarantine dis=none) header.from=kernel.org Authentication-Results: smtp.subspace.kernel.org; spf=none smtp.mailfrom=infradead.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b="efcO4/i6" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=infradead.org; s=bombadil.20210309; h=Sender:Content-Transfer-Encoding: MIME-Version:References:In-Reply-To:Message-ID:Date:Subject:Cc:To:From: Reply-To:Content-Type:Content-ID:Content-Description; bh=XSyTu6H5sgWinIVeWDqQGu+37mEe3dtWeUch7oR4vhI=; b=efcO4/i63FM/rxf7dF/lRNSgsM ErzQqVn3QB5mnq15/JvZWCtRpKPPPgqqJCAGkJk7/B50WoKFBvQxTUObGsqi797i9v0vVJflpHBtq r/NoI2okOsEndyxM6Eqs1RQZT3EvHTlp16HyO2/Tg7LqsWbPbkWwqBti2860nE5H7d/o+y+3WvAP0 LHQORK4CR0eIm604fZFYTX2L9Mk4cfQzUSLMlyZ8/xG4Hv3eil6o15Vf93kZL/ApXDVvxe6AMSRhA Y8OVMqwjZGcjKNZ5bRrkzQvde7r0sTJxMZj9q0HjAe+QhtTLTbWEyY5XJcEW7arE2z6VcTHMsbbcz o2F5iOIA==; Received: from mcgrof by bombadil.infradead.org with local (Exim 4.98.2 #2 (Red Hat Linux)) id 1uyg8K-00000009j5P-0ERA; Wed, 17 Sep 2025 00:34:52 +0000 From: Luis Chamberlain To: Chuck Lever , Daniel Gomez , kdevops@lists.linux.dev Cc: Luis Chamberlain Subject: [PATCH v4 5/8] aws: add cloud billing support with make cloud-bill Date: Tue, 16 Sep 2025 17:34:46 -0700 Message-ID: <20250917003451.2318229-6-mcgrof@kernel.org> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20250917003451.2318229-1-mcgrof@kernel.org> References: <20250917003451.2318229-1-mcgrof@kernel.org> Precedence: bulk X-Mailing-List: kdevops@lists.linux.dev List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Sender: Luis Chamberlain Add AWS cost tracking functionality to quickly check monthly spending. This includes my personal hack scripts for monitoring AWS costs: - scripts/aws-costs.sh: Queries AWS Cost Explorer for current month - scripts/aws-parse-costs.py: Parses and displays costs by service - make cloud-bill: Show cloud provider costs (currently AWS only) - make cloud-bill-aws: Show detailed AWS costs for current month The scripts provide: - Total monthly cost to date - Breakdown by AWS service - Daily average spending - Projected monthly cost (when mid-month) This is useful for monitoring cloud spending during development and testing, especially when running expensive instances or long tests. Generated-by: Claude AI Signed-off-by: Luis Chamberlain --- scripts/aws-costs.sh | 39 ++++++++++ scripts/aws-parse-costs.py | 98 ++++++++++++++++++++++++++ scripts/dynamic-cloud-kconfig.Makefile | 13 +++- 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100755 scripts/aws-costs.sh create mode 100755 scripts/aws-parse-costs.py diff --git a/scripts/aws-costs.sh b/scripts/aws-costs.sh new file mode 100755 index 00000000..e2298008 --- /dev/null +++ b/scripts/aws-costs.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# SPDX-License-Identifier: copyleft-next-0.3.1 +# +# AWS cost tracking script - quick hack to check AWS spending +# This queries AWS Cost Explorer to get current month's costs + +set -e + +# Get the first and last day of the current month +FIRST_DAY=$(date +%Y-%m-01) +LAST_DAY=$(date -d "$FIRST_DAY +1 month -1 day" +%Y-%m-%d) +TODAY=$(date +%Y-%m-%d) + +# If we're still in the current month, use today as the end date +if [[ "$TODAY" < "$LAST_DAY" ]]; then + END_DATE="$TODAY" +else + END_DATE="$LAST_DAY" +fi + +echo "Fetching AWS costs from $FIRST_DAY to $END_DATE..." >&2 + +# Query AWS Cost Explorer +aws ce get-cost-and-usage \ + --time-period Start=$FIRST_DAY,End=$END_DATE \ + --granularity MONTHLY \ + --metrics UnblendedCost \ + --group-by Type=DIMENSION,Key=SERVICE \ + --output json > cost.json + +# Parse and display the results +if [ -f cost.json ]; then + echo "Cost data saved to cost.json" >&2 + echo "Parsing costs..." >&2 + python3 scripts/aws-parse-costs.py cost.json +else + echo "Error: Failed to retrieve cost data" >&2 + exit 1 +fi \ No newline at end of file diff --git a/scripts/aws-parse-costs.py b/scripts/aws-parse-costs.py new file mode 100755 index 00000000..7f3256a7 --- /dev/null +++ b/scripts/aws-parse-costs.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: copyleft-next-0.3.1 +# +# Parse AWS Cost Explorer JSON output and display costs + +import json +import sys +from datetime import datetime + + +def parse_costs(filename): + """Parse AWS Cost Explorer JSON output.""" + try: + with open(filename, 'r') as f: + data = json.load(f) + except FileNotFoundError: + print(f"Error: File {filename} not found", file=sys.stderr) + sys.exit(1) + except json.JSONDecodeError as e: + print(f"Error parsing JSON: {e}", file=sys.stderr) + sys.exit(1) + + # Extract time period + if 'ResultsByTime' in data and data['ResultsByTime']: + result = data['ResultsByTime'][0] + time_period = result.get('TimePeriod', {}) + start = time_period.get('Start', 'Unknown') + end = time_period.get('End', 'Unknown') + + print(f"\nAWS Cost Report") + print(f"Period: {start} to {end}") + print("=" * 60) + + # Get total cost + total = result.get('Total', {}) + if 'UnblendedCost' in total: + total_amount = float(total['UnblendedCost'].get('Amount', 0)) + currency = total['UnblendedCost'].get('Unit', 'USD') + print(f"\nTotal Cost: ${total_amount:.2f} {currency}") + + # Get costs by service + groups = result.get('Groups', []) + if groups: + print("\nCosts by Service:") + print("-" * 40) + + # Sort by cost (descending) + sorted_groups = sorted(groups, + key=lambda x: float(x['Metrics']['UnblendedCost']['Amount']), + reverse=True) + + for group in sorted_groups: + service = group['Keys'][0] if group.get('Keys') else 'Unknown' + metrics = group.get('Metrics', {}) + if 'UnblendedCost' in metrics: + amount = float(metrics['UnblendedCost'].get('Amount', 0)) + if amount > 0.01: # Only show services with costs > $0.01 + print(f" {service:30} ${amount:10.2f}") + + # Show total at the bottom + print("-" * 40) + if 'UnblendedCost' in total: + print(f" {'TOTAL':30} ${total_amount:10.2f}") + + # Calculate daily average + try: + start_date = datetime.strptime(start, '%Y-%m-%d') + end_date = datetime.strptime(end, '%Y-%m-%d') + days = (end_date - start_date).days + if days > 0 and 'UnblendedCost' in total: + daily_avg = total_amount / days + print(f"\nDaily Average: ${daily_avg:.2f}") + + # Project monthly cost if we're mid-month + today = datetime.now() + if end_date.date() == today.date() and start_date.day == 1: + days_in_month = 30 # Approximate + projected = daily_avg * days_in_month + print(f"Projected Monthly: ${projected:.2f}") + except ValueError: + pass + + else: + print("No cost data available in the response", file=sys.stderr) + sys.exit(1) + + +def main(): + """Main function.""" + if len(sys.argv) != 2: + print("Usage: aws-parse-costs.py ", file=sys.stderr) + sys.exit(1) + + parse_costs(sys.argv[1]) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/dynamic-cloud-kconfig.Makefile b/scripts/dynamic-cloud-kconfig.Makefile index ed2d5366..0ec14966 100644 --- a/scripts/dynamic-cloud-kconfig.Makefile +++ b/scripts/dynamic-cloud-kconfig.Makefile @@ -72,6 +72,8 @@ cloud-config-help: @echo "cloud-update-aws - refreshes AWS data (clears cache and regenerates)" @echo "clean-cloud-config - removes all generated cloud kconfig files" @echo "cloud-list-all - list all cloud instances for configured provider" + @echo "cloud-bill - show current month's cloud provider costs" + @echo "cloud-bill-aws - show AWS costs for current month" HELP_TARGETS += cloud-config-help @@ -88,6 +90,15 @@ cloud-list-all: $(Q)chmod +x scripts/cloud_list_all.sh $(Q)scripts/cloud_list_all.sh +# Cloud billing targets +cloud-bill-aws: + $(Q)chmod +x scripts/aws-costs.sh + $(Q)scripts/aws-costs.sh + +cloud-bill: cloud-bill-aws + $(Q)echo "" + $(Q)echo "Note: Only AWS billing is currently supported" + PHONY += cloud-config cloud-config-lambdalabs cloud-config-aws cloud-update cloud-update-aws PHONY += clean-cloud-config clean-cloud-config-lambdalabs clean-cloud-config-aws -PHONY += cloud-config-help cloud-list-all +PHONY += cloud-config-help cloud-list-all cloud-bill cloud-bill-aws -- 2.51.0