aws-cost-calculator-cli 2.1.0__tar.gz → 2.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aws_cost_calculator_cli-2.1.0/aws_cost_calculator_cli.egg-info → aws_cost_calculator_cli-2.3.2}/PKG-INFO +1 -1
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2/aws_cost_calculator_cli.egg-info}/PKG-INFO +1 -1
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/aws_cost_calculator_cli.egg-info/SOURCES.txt +1 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/api_client.py +18 -21
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/cli.py +92 -133
- aws_cost_calculator_cli-2.3.2/cost_calculator/dimensions.py +141 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/executor.py +20 -10
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/setup.py +1 -1
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/CHANGES.md +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/LICENSE +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/MANIFEST.in +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/README.md +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/aws_cost_calculator_cli.egg-info/dependency_links.txt +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/aws_cost_calculator_cli.egg-info/entry_points.txt +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/aws_cost_calculator_cli.egg-info/requires.txt +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/aws_cost_calculator_cli.egg-info/top_level.txt +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/__init__.py +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/cur.py +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/drill.py +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/forensics.py +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/monthly.py +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/trends.py +0 -0
- {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-cost-calculator-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: AWS Cost Calculator CLI - Calculate daily and annual AWS costs across multiple accounts
|
|
5
5
|
Home-page: https://github.com/trilogy-group/aws-cost-calculator
|
|
6
6
|
Author: Cost Optimization Team
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-cost-calculator-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: AWS Cost Calculator CLI - Calculate daily and annual AWS costs across multiple accounts
|
|
5
5
|
Home-page: https://github.com/trilogy-group/aws-cost-calculator
|
|
6
6
|
Author: Cost Optimization Team
|
{aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/api_client.py
RENAMED
|
@@ -23,14 +23,15 @@ def get_api_config():
|
|
|
23
23
|
return None
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def call_lambda_api(endpoint, credentials, accounts, **kwargs):
|
|
26
|
+
def call_lambda_api(endpoint, credentials, accounts=None, profile=None, **kwargs):
|
|
27
27
|
"""
|
|
28
|
-
Call
|
|
28
|
+
Call unified API Gateway endpoint.
|
|
29
29
|
|
|
30
30
|
Args:
|
|
31
|
-
endpoint: API endpoint name ('trends', 'monthly', 'drill')
|
|
31
|
+
endpoint: API endpoint name ('calculate', 'trends', 'monthly', 'drill', 'query', etc.)
|
|
32
32
|
credentials: dict with AWS credentials
|
|
33
|
-
accounts: list of account IDs
|
|
33
|
+
accounts: list of account IDs (deprecated - use profile instead)
|
|
34
|
+
profile: profile name (preferred over accounts)
|
|
34
35
|
**kwargs: additional parameters for the specific endpoint
|
|
35
36
|
|
|
36
37
|
Returns:
|
|
@@ -44,26 +45,22 @@ def call_lambda_api(endpoint, credentials, accounts, **kwargs):
|
|
|
44
45
|
if not api_config:
|
|
45
46
|
raise Exception("API not configured. Set COST_API_SECRET environment variable.")
|
|
46
47
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'monthly': 'https://6aueebodw6q4zdeu3aaexb6tle0fqhhr.lambda-url.us-east-1.on.aws/',
|
|
51
|
-
'drill': 'https://3ncm2gzxrsyptrhud3ua3x5lju0akvsr.lambda-url.us-east-1.on.aws/',
|
|
52
|
-
'analyze': 'https://y6npmidtxwzg62nrqzkbacfs5q0edwgs.lambda-url.us-east-1.on.aws/',
|
|
53
|
-
'profiles': 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/',
|
|
54
|
-
'forensics': 'https://gaekfzz7sc2hwn4mjyk64sieke0vadfo.lambda-url.us-east-1.on.aws/' # Will be populated after deployment
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
url = endpoint_urls.get(endpoint)
|
|
58
|
-
|
|
59
|
-
if not url:
|
|
60
|
-
raise Exception(f"Unknown endpoint: {endpoint}")
|
|
48
|
+
# Get base URL from environment or use default
|
|
49
|
+
base_url = os.environ.get('COST_CALCULATOR_API_URL', 'https://api.costcop.cloudfix.dev')
|
|
50
|
+
url = f"{base_url}/{endpoint}"
|
|
61
51
|
|
|
62
52
|
# Build request payload
|
|
63
53
|
payload = {
|
|
64
|
-
'credentials': credentials
|
|
65
|
-
'accounts': accounts
|
|
54
|
+
'credentials': credentials
|
|
66
55
|
}
|
|
56
|
+
|
|
57
|
+
# Add profile or accounts
|
|
58
|
+
if profile:
|
|
59
|
+
payload['profile'] = profile
|
|
60
|
+
elif accounts:
|
|
61
|
+
payload['accounts'] = accounts
|
|
62
|
+
|
|
63
|
+
# Add additional parameters
|
|
67
64
|
payload.update(kwargs)
|
|
68
65
|
|
|
69
66
|
# Make API call
|
|
@@ -72,7 +69,7 @@ def call_lambda_api(endpoint, credentials, accounts, **kwargs):
|
|
|
72
69
|
'Content-Type': 'application/json'
|
|
73
70
|
}
|
|
74
71
|
|
|
75
|
-
response = requests.post(url, headers=headers, json=payload, timeout=
|
|
72
|
+
response = requests.post(url, headers=headers, json=payload, timeout=900) # 15 min timeout
|
|
76
73
|
|
|
77
74
|
if response.status_code != 200:
|
|
78
75
|
raise Exception(f"API call failed: {response.status_code} - {response.text}")
|
|
@@ -20,9 +20,14 @@ from cost_calculator.monthly import format_monthly_markdown
|
|
|
20
20
|
from cost_calculator.drill import format_drill_down_markdown
|
|
21
21
|
|
|
22
22
|
# API Configuration - can be overridden via environment variable
|
|
23
|
+
API_BASE_URL = os.environ.get(
|
|
24
|
+
'COST_CALCULATOR_API_URL',
|
|
25
|
+
'https://api.costcop.cloudfix.dev'
|
|
26
|
+
)
|
|
27
|
+
# Legacy profiles URL for backward compatibility
|
|
23
28
|
PROFILES_API_URL = os.environ.get(
|
|
24
29
|
'COST_CALCULATOR_PROFILES_URL',
|
|
25
|
-
'
|
|
30
|
+
f'{API_BASE_URL}/profiles'
|
|
26
31
|
)
|
|
27
32
|
from cost_calculator.executor import execute_trends, execute_monthly, execute_drill
|
|
28
33
|
|
|
@@ -210,7 +215,10 @@ def load_profile(profile_name):
|
|
|
210
215
|
response_data = response.json()
|
|
211
216
|
# API returns {"profile": {...}} wrapper
|
|
212
217
|
profile_data = response_data.get('profile', response_data)
|
|
213
|
-
profile = {
|
|
218
|
+
profile = {
|
|
219
|
+
'accounts': profile_data['accounts'],
|
|
220
|
+
'profile_name': profile_name # Store profile name for API calls
|
|
221
|
+
}
|
|
214
222
|
|
|
215
223
|
# If profile has aws_profile field, use it
|
|
216
224
|
if 'aws_profile' in profile_data:
|
|
@@ -669,145 +677,61 @@ def calculate(profile, start_date, offset, window, json_output, sso, access_key_
|
|
|
669
677
|
click.echo("=" * 60)
|
|
670
678
|
|
|
671
679
|
|
|
672
|
-
|
|
673
|
-
@click.option('--profile', required=True, help='Profile name to create')
|
|
674
|
-
@click.option('--aws-profile', required=True, help='AWS CLI profile name')
|
|
675
|
-
@click.option('--accounts', required=True, help='Comma-separated list of account IDs')
|
|
676
|
-
def init(profile, aws_profile, accounts):
|
|
677
|
-
"""Initialize a new profile configuration"""
|
|
678
|
-
|
|
679
|
-
config_dir = Path.home() / '.config' / 'cost-calculator'
|
|
680
|
-
config_file = config_dir / 'profiles.json'
|
|
681
|
-
|
|
682
|
-
# Create config directory if it doesn't exist
|
|
683
|
-
config_dir.mkdir(parents=True, exist_ok=True)
|
|
684
|
-
|
|
685
|
-
# Load existing profiles or create new
|
|
686
|
-
if config_file.exists() and config_file.stat().st_size > 0:
|
|
687
|
-
try:
|
|
688
|
-
with open(config_file) as f:
|
|
689
|
-
profiles = json.load(f)
|
|
690
|
-
except json.JSONDecodeError:
|
|
691
|
-
profiles = {}
|
|
692
|
-
else:
|
|
693
|
-
profiles = {}
|
|
694
|
-
|
|
695
|
-
# Parse accounts
|
|
696
|
-
account_list = [acc.strip() for acc in accounts.split(',')]
|
|
697
|
-
|
|
698
|
-
# Add new profile
|
|
699
|
-
profiles[profile] = {
|
|
700
|
-
'aws_profile': aws_profile,
|
|
701
|
-
'accounts': account_list
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
# Save
|
|
705
|
-
with open(config_file, 'w') as f:
|
|
706
|
-
json.dump(profiles, f, indent=2)
|
|
707
|
-
|
|
708
|
-
click.echo(f"✓ Profile '{profile}' created with {len(account_list)} accounts")
|
|
709
|
-
click.echo(f"✓ Configuration saved to {config_file}")
|
|
710
|
-
click.echo(f"\nUsage: cc calculate --profile {profile}")
|
|
680
|
+
# init command removed - use backend API via 'cc profile create' instead
|
|
711
681
|
|
|
712
682
|
|
|
713
683
|
@cli.command()
|
|
714
684
|
def list_profiles():
|
|
715
|
-
"""List all
|
|
716
|
-
|
|
717
|
-
config_file = Path.home() / '.config' / 'cost-calculator' / 'profiles.json'
|
|
718
|
-
|
|
719
|
-
if not config_file.exists():
|
|
720
|
-
click.echo("No profiles configured. Run: cc init --profile <name>")
|
|
721
|
-
return
|
|
722
|
-
|
|
723
|
-
with open(config_file) as f:
|
|
724
|
-
profiles = json.load(f)
|
|
685
|
+
"""List all profiles from backend API (no local caching)"""
|
|
686
|
+
import requests
|
|
725
687
|
|
|
726
|
-
|
|
727
|
-
|
|
688
|
+
api_secret = get_api_secret()
|
|
689
|
+
if not api_secret:
|
|
690
|
+
click.echo("No API secret configured.")
|
|
691
|
+
click.echo("Run: cc configure --api-secret YOUR_SECRET")
|
|
728
692
|
return
|
|
729
693
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
694
|
+
try:
|
|
695
|
+
response = requests.post(
|
|
696
|
+
f"{API_BASE_URL}/profiles",
|
|
697
|
+
headers={'X-API-Secret': api_secret, 'Content-Type': 'application/json'},
|
|
698
|
+
json={'operation': 'list'},
|
|
699
|
+
timeout=10
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
if response.status_code != 200:
|
|
703
|
+
click.echo(f"Error: {response.status_code} - {response.text}")
|
|
704
|
+
return
|
|
705
|
+
|
|
706
|
+
result = response.json()
|
|
707
|
+
profiles = result.get('profiles', [])
|
|
708
|
+
|
|
709
|
+
if not profiles:
|
|
710
|
+
click.echo("No profiles found in backend.")
|
|
711
|
+
click.echo("Contact admin to create profiles in DynamoDB.")
|
|
712
|
+
return
|
|
713
|
+
|
|
714
|
+
click.echo("Profiles (from backend API):")
|
|
739
715
|
click.echo("")
|
|
716
|
+
for profile in profiles:
|
|
717
|
+
# Each profile is a dict, extract the profile_name
|
|
718
|
+
if isinstance(profile, dict):
|
|
719
|
+
name = profile.get('profile_name', 'unknown')
|
|
720
|
+
# Skip exclusions entries
|
|
721
|
+
if not name.startswith('exclusions:'):
|
|
722
|
+
accounts = profile.get('accounts', [])
|
|
723
|
+
click.echo(f" {name}")
|
|
724
|
+
click.echo(f" Accounts: {len(accounts)}")
|
|
725
|
+
click.echo("")
|
|
726
|
+
else:
|
|
727
|
+
click.echo(f" {profile}")
|
|
728
|
+
click.echo(f"Total: {len([p for p in profiles if isinstance(p, dict) and not p.get('profile_name', '').startswith('exclusions:')])} profile(s)")
|
|
729
|
+
|
|
730
|
+
except Exception as e:
|
|
731
|
+
click.echo(f"Error loading profiles: {e}")
|
|
740
732
|
|
|
741
733
|
|
|
742
|
-
|
|
743
|
-
def setup():
|
|
744
|
-
"""Show setup instructions for manual profile configuration"""
|
|
745
|
-
import platform
|
|
746
|
-
|
|
747
|
-
system = platform.system()
|
|
748
|
-
|
|
749
|
-
if system == "Windows":
|
|
750
|
-
config_path = "%USERPROFILE%\\.config\\cost-calculator\\profiles.json"
|
|
751
|
-
config_path_example = "C:\\Users\\YourName\\.config\\cost-calculator\\profiles.json"
|
|
752
|
-
mkdir_cmd = "mkdir %USERPROFILE%\\.config\\cost-calculator"
|
|
753
|
-
edit_cmd = "notepad %USERPROFILE%\\.config\\cost-calculator\\profiles.json"
|
|
754
|
-
else: # macOS/Linux
|
|
755
|
-
config_path = "~/.config/cost-calculator/profiles.json"
|
|
756
|
-
config_path_example = "/Users/yourname/.config/cost-calculator/profiles.json"
|
|
757
|
-
mkdir_cmd = "mkdir -p ~/.config/cost-calculator"
|
|
758
|
-
edit_cmd = "nano ~/.config/cost-calculator/profiles.json"
|
|
759
|
-
|
|
760
|
-
click.echo("=" * 70)
|
|
761
|
-
click.echo("AWS Cost Calculator - Manual Profile Setup")
|
|
762
|
-
click.echo("=" * 70)
|
|
763
|
-
click.echo("")
|
|
764
|
-
click.echo(f"Platform: {system}")
|
|
765
|
-
click.echo(f"Config location: {config_path}")
|
|
766
|
-
click.echo("")
|
|
767
|
-
click.echo("Step 1: Create the config directory")
|
|
768
|
-
click.echo(f" {mkdir_cmd}")
|
|
769
|
-
click.echo("")
|
|
770
|
-
click.echo("Step 2: Create the profiles.json file")
|
|
771
|
-
click.echo(f" {edit_cmd}")
|
|
772
|
-
click.echo("")
|
|
773
|
-
click.echo("Step 3: Add your profile configuration (JSON format):")
|
|
774
|
-
click.echo("")
|
|
775
|
-
click.echo(' {')
|
|
776
|
-
click.echo(' "myprofile": {')
|
|
777
|
-
click.echo(' "aws_profile": "my_aws_profile",')
|
|
778
|
-
click.echo(' "accounts": [')
|
|
779
|
-
click.echo(' "123456789012",')
|
|
780
|
-
click.echo(' "234567890123",')
|
|
781
|
-
click.echo(' "345678901234"')
|
|
782
|
-
click.echo(' ]')
|
|
783
|
-
click.echo(' }')
|
|
784
|
-
click.echo(' }')
|
|
785
|
-
click.echo("")
|
|
786
|
-
click.echo("Step 4: Save the file")
|
|
787
|
-
click.echo("")
|
|
788
|
-
click.echo("Step 5: Verify it works")
|
|
789
|
-
click.echo(" cc list-profiles")
|
|
790
|
-
click.echo("")
|
|
791
|
-
click.echo("Step 6: Configure AWS credentials")
|
|
792
|
-
click.echo(" Option A (SSO):")
|
|
793
|
-
click.echo(" aws sso login --profile my_aws_profile")
|
|
794
|
-
click.echo(" cc calculate --profile myprofile")
|
|
795
|
-
click.echo("")
|
|
796
|
-
click.echo(" Option B (Static credentials):")
|
|
797
|
-
click.echo(" cc configure --profile myprofile")
|
|
798
|
-
click.echo(" cc calculate --profile myprofile")
|
|
799
|
-
click.echo("")
|
|
800
|
-
click.echo("=" * 70)
|
|
801
|
-
click.echo("")
|
|
802
|
-
click.echo("For multiple profiles, add more entries to the JSON:")
|
|
803
|
-
click.echo("")
|
|
804
|
-
click.echo(' {')
|
|
805
|
-
click.echo(' "profile1": { ... },')
|
|
806
|
-
click.echo(' "profile2": { ... }')
|
|
807
|
-
click.echo(' }')
|
|
808
|
-
click.echo("")
|
|
809
|
-
click.echo(f"Full path example: {config_path_example}")
|
|
810
|
-
click.echo("=" * 70)
|
|
734
|
+
# setup command removed - profiles are managed in DynamoDB backend only
|
|
811
735
|
|
|
812
736
|
|
|
813
737
|
@cli.command()
|
|
@@ -1033,6 +957,10 @@ def monthly(profile, months, output, json_output, sso, access_key_id, secret_acc
|
|
|
1033
957
|
@click.option('--service', help='Filter by service name (e.g., "EC2 - Other")')
|
|
1034
958
|
@click.option('--account', help='Filter by account ID')
|
|
1035
959
|
@click.option('--usage-type', help='Filter by usage type')
|
|
960
|
+
@click.option('--dimension', type=click.Choice(['service', 'account', 'region', 'usage_type', 'resource', 'instance_type', 'operation', 'availability_zone']),
|
|
961
|
+
help='Dimension to analyze by (overrides --service/--account filters)')
|
|
962
|
+
@click.option('--backend', type=click.Choice(['auto', 'ce', 'athena']), default='auto',
|
|
963
|
+
help='Data source: auto (smart selection), ce (Cost Explorer), athena (CUR). Default: auto')
|
|
1036
964
|
@click.option('--resources', is_flag=True, help='Show individual resource IDs (requires CUR, uses Athena)')
|
|
1037
965
|
@click.option('--output', default='drill_down.md', help='Output markdown file (default: drill_down.md)')
|
|
1038
966
|
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
@@ -1040,19 +968,49 @@ def monthly(profile, months, output, json_output, sso, access_key_id, secret_acc
|
|
|
1040
968
|
@click.option('--access-key-id', help='AWS Access Key ID')
|
|
1041
969
|
@click.option('--secret-access-key', help='AWS Secret Access Key')
|
|
1042
970
|
@click.option('--session-token', help='AWS Session Token')
|
|
1043
|
-
def drill(profile, weeks, service, account, usage_type, resources, output, json_output, sso, access_key_id, secret_access_key, session_token):
|
|
971
|
+
def drill(profile, weeks, service, account, usage_type, dimension, backend, resources, output, json_output, sso, access_key_id, secret_access_key, session_token):
|
|
1044
972
|
"""
|
|
1045
973
|
Drill down into cost changes by service, account, or usage type
|
|
1046
974
|
|
|
1047
975
|
Add --resources flag to see individual resource IDs and costs (requires CUR data via Athena)
|
|
976
|
+
|
|
977
|
+
Examples:
|
|
978
|
+
# Analyze by service (auto-selects Cost Explorer for speed)
|
|
979
|
+
cc drill --profile khoros --dimension service
|
|
980
|
+
|
|
981
|
+
# Analyze by region with specific service filter
|
|
982
|
+
cc drill --profile khoros --dimension region --service AWSELB
|
|
983
|
+
|
|
984
|
+
# Analyze by resource (auto-selects Athena - only source with resource IDs)
|
|
985
|
+
cc drill --profile khoros --dimension resource --service AWSELB --account 820054669588
|
|
986
|
+
|
|
987
|
+
# Force Athena backend for detailed analysis
|
|
988
|
+
cc drill --profile khoros --dimension service --backend athena
|
|
1048
989
|
"""
|
|
1049
990
|
|
|
1050
991
|
# Load profile
|
|
1051
992
|
config = load_profile(profile)
|
|
1052
993
|
config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
|
|
1053
994
|
|
|
995
|
+
# Smart backend selection
|
|
996
|
+
def select_backend(dimension, resources_flag, backend_choice):
|
|
997
|
+
"""Auto-select backend based on query requirements"""
|
|
998
|
+
if backend_choice != 'auto':
|
|
999
|
+
return backend_choice
|
|
1000
|
+
|
|
1001
|
+
# Must use Athena if:
|
|
1002
|
+
if dimension == 'resource' or resources_flag:
|
|
1003
|
+
return 'athena'
|
|
1004
|
+
|
|
1005
|
+
# Prefer CE for speed (unless explicitly requesting Athena)
|
|
1006
|
+
return 'ce'
|
|
1007
|
+
|
|
1008
|
+
selected_backend = select_backend(dimension, resources, backend)
|
|
1009
|
+
|
|
1054
1010
|
# Show filters
|
|
1055
1011
|
click.echo(f"Analyzing last {weeks} weeks...")
|
|
1012
|
+
if dimension:
|
|
1013
|
+
click.echo(f" Dimension: {dimension}")
|
|
1056
1014
|
if service:
|
|
1057
1015
|
click.echo(f" Service filter: {service}")
|
|
1058
1016
|
if account:
|
|
@@ -1061,10 +1019,11 @@ def drill(profile, weeks, service, account, usage_type, resources, output, json_
|
|
|
1061
1019
|
click.echo(f" Usage type filter: {usage_type}")
|
|
1062
1020
|
if resources:
|
|
1063
1021
|
click.echo(f" Mode: Resource-level (CUR via Athena)")
|
|
1022
|
+
click.echo(f" Backend: {selected_backend.upper()}")
|
|
1064
1023
|
click.echo("")
|
|
1065
1024
|
|
|
1066
1025
|
# Execute via API or locally
|
|
1067
|
-
drill_data = execute_drill(config, weeks, service, account, usage_type, resources)
|
|
1026
|
+
drill_data = execute_drill(config, weeks, service, account, usage_type, resources, dimension, selected_backend)
|
|
1068
1027
|
|
|
1069
1028
|
# Handle resource-level output differently
|
|
1070
1029
|
if resources:
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dimension mapping between CLI, Cost Explorer, and Athena CUR.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for translating dimension names across different backends.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Dimension mapping: CLI dimension -> (CE dimension, Athena column)
|
|
8
|
+
DIMENSION_MAP = {
|
|
9
|
+
'service': ('SERVICE', 'line_item_product_code'),
|
|
10
|
+
'account': ('LINKED_ACCOUNT', 'line_item_usage_account_id'),
|
|
11
|
+
'region': ('REGION', 'product_region'),
|
|
12
|
+
'usage_type': ('USAGE_TYPE', 'line_item_usage_type'),
|
|
13
|
+
'resource': (None, 'line_item_resource_id'), # Athena only
|
|
14
|
+
'instance_type': ('INSTANCE_TYPE', 'product_instance_type'),
|
|
15
|
+
'operation': ('OPERATION', 'line_item_operation'),
|
|
16
|
+
'availability_zone': ('AVAILABILITY_ZONE', 'product_availability_zone'),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_ce_dimension(cli_dimension):
|
|
21
|
+
"""
|
|
22
|
+
Get Cost Explorer dimension key for a CLI dimension.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
cli_dimension: CLI dimension name (e.g., 'service', 'account')
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
str: Cost Explorer dimension key (e.g., 'SERVICE', 'LINKED_ACCOUNT')
|
|
29
|
+
None: If dimension is not available in Cost Explorer
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError: If dimension is unknown
|
|
33
|
+
"""
|
|
34
|
+
if cli_dimension not in DIMENSION_MAP:
|
|
35
|
+
raise ValueError(f"Unknown dimension: {cli_dimension}")
|
|
36
|
+
|
|
37
|
+
ce_dim, _ = DIMENSION_MAP[cli_dimension]
|
|
38
|
+
return ce_dim
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_athena_column(cli_dimension):
|
|
42
|
+
"""
|
|
43
|
+
Get Athena CUR column name for a CLI dimension.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
cli_dimension: CLI dimension name (e.g., 'service', 'account')
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
str: Athena column name (e.g., 'line_item_product_code', 'line_item_usage_account_id')
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ValueError: If dimension is unknown
|
|
53
|
+
"""
|
|
54
|
+
if cli_dimension not in DIMENSION_MAP:
|
|
55
|
+
raise ValueError(f"Unknown dimension: {cli_dimension}")
|
|
56
|
+
|
|
57
|
+
_, athena_col = DIMENSION_MAP[cli_dimension]
|
|
58
|
+
return athena_col
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_athena_only(cli_dimension):
|
|
62
|
+
"""
|
|
63
|
+
Check if a dimension is only available in Athena (not in Cost Explorer).
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
cli_dimension: CLI dimension name
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
bool: True if dimension requires Athena, False otherwise
|
|
70
|
+
"""
|
|
71
|
+
if cli_dimension not in DIMENSION_MAP:
|
|
72
|
+
raise ValueError(f"Unknown dimension: {cli_dimension}")
|
|
73
|
+
|
|
74
|
+
ce_dim, _ = DIMENSION_MAP[cli_dimension]
|
|
75
|
+
return ce_dim is None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_available_dimensions(backend='auto'):
|
|
79
|
+
"""
|
|
80
|
+
Get list of available dimensions for a given backend.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
backend: 'auto', 'ce', or 'athena'
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
list: List of available dimension names
|
|
87
|
+
"""
|
|
88
|
+
if backend == 'athena':
|
|
89
|
+
# All dimensions available in Athena
|
|
90
|
+
return list(DIMENSION_MAP.keys())
|
|
91
|
+
elif backend == 'ce':
|
|
92
|
+
# Only dimensions with CE mapping
|
|
93
|
+
return [dim for dim, (ce_dim, _) in DIMENSION_MAP.items() if ce_dim is not None]
|
|
94
|
+
else: # auto
|
|
95
|
+
# All dimensions (backend will be auto-selected)
|
|
96
|
+
return list(DIMENSION_MAP.keys())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def validate_dimension_backend(dimension, backend):
|
|
100
|
+
"""
|
|
101
|
+
Validate that a dimension is available for the specified backend.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
dimension: CLI dimension name
|
|
105
|
+
backend: 'auto', 'ce', or 'athena'
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
tuple: (is_valid, error_message)
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
>>> validate_dimension_backend('resource', 'ce')
|
|
112
|
+
(False, "Dimension 'resource' requires Athena backend (not available in Cost Explorer)")
|
|
113
|
+
|
|
114
|
+
>>> validate_dimension_backend('service', 'ce')
|
|
115
|
+
(True, None)
|
|
116
|
+
"""
|
|
117
|
+
if dimension not in DIMENSION_MAP:
|
|
118
|
+
return False, f"Unknown dimension: {dimension}"
|
|
119
|
+
|
|
120
|
+
if backend == 'ce' and is_athena_only(dimension):
|
|
121
|
+
return False, f"Dimension '{dimension}' requires Athena backend (not available in Cost Explorer)"
|
|
122
|
+
|
|
123
|
+
return True, None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Human-readable dimension descriptions
|
|
127
|
+
DIMENSION_DESCRIPTIONS = {
|
|
128
|
+
'service': 'AWS Service (e.g., EC2, S3, RDS)',
|
|
129
|
+
'account': 'AWS Account ID',
|
|
130
|
+
'region': 'AWS Region (e.g., us-east-1, eu-west-1)',
|
|
131
|
+
'usage_type': 'Usage Type (e.g., BoxUsage:t3.micro)',
|
|
132
|
+
'resource': 'Resource ID/ARN (Athena only)',
|
|
133
|
+
'instance_type': 'Instance Type (e.g., t3.micro, m5.large)',
|
|
134
|
+
'operation': 'Operation (e.g., RunInstances, CreateBucket)',
|
|
135
|
+
'availability_zone': 'Availability Zone (e.g., us-east-1a)',
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_dimension_description(dimension):
|
|
140
|
+
"""Get human-readable description for a dimension."""
|
|
141
|
+
return DIMENSION_DESCRIPTIONS.get(dimension, dimension)
|
|
@@ -92,12 +92,16 @@ def get_credentials_dict(config):
|
|
|
92
92
|
|
|
93
93
|
def execute_trends(config, weeks):
|
|
94
94
|
"""
|
|
95
|
-
Execute trends analysis via API
|
|
95
|
+
Execute trends analysis via API.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
config: Profile configuration
|
|
99
|
+
weeks: Number of weeks to analyze
|
|
96
100
|
|
|
97
101
|
Returns:
|
|
98
102
|
dict: trends data
|
|
99
103
|
"""
|
|
100
|
-
|
|
104
|
+
profile_name = config.get('profile_name', config.get('name'))
|
|
101
105
|
|
|
102
106
|
if not is_api_configured():
|
|
103
107
|
raise Exception(
|
|
@@ -110,7 +114,7 @@ def execute_trends(config, weeks):
|
|
|
110
114
|
credentials = get_credentials_dict(config)
|
|
111
115
|
if not credentials:
|
|
112
116
|
raise Exception("Failed to get AWS credentials. Check your AWS SSO session.")
|
|
113
|
-
return call_lambda_api('trends', credentials,
|
|
117
|
+
return call_lambda_api('trends', credentials, profile=profile_name, weeks=weeks)
|
|
114
118
|
|
|
115
119
|
|
|
116
120
|
def execute_monthly(config, months):
|
|
@@ -120,7 +124,7 @@ def execute_monthly(config, months):
|
|
|
120
124
|
Returns:
|
|
121
125
|
dict: monthly data
|
|
122
126
|
"""
|
|
123
|
-
|
|
127
|
+
profile_name = config.get('profile_name', config.get('name'))
|
|
124
128
|
|
|
125
129
|
if not is_api_configured():
|
|
126
130
|
raise Exception(
|
|
@@ -133,10 +137,10 @@ def execute_monthly(config, months):
|
|
|
133
137
|
credentials = get_credentials_dict(config)
|
|
134
138
|
if not credentials:
|
|
135
139
|
raise Exception("Failed to get AWS credentials. Check your AWS SSO session.")
|
|
136
|
-
return call_lambda_api('monthly', credentials,
|
|
140
|
+
return call_lambda_api('monthly', credentials, profile=profile_name, months=months)
|
|
137
141
|
|
|
138
142
|
|
|
139
|
-
def execute_drill(config, weeks, service_filter=None, account_filter=None, usage_type_filter=None, resources=False):
|
|
143
|
+
def execute_drill(config, weeks, service_filter=None, account_filter=None, usage_type_filter=None, resources=False, dimension=None, backend='auto'):
|
|
140
144
|
"""
|
|
141
145
|
Execute drill-down analysis via API.
|
|
142
146
|
|
|
@@ -147,11 +151,13 @@ def execute_drill(config, weeks, service_filter=None, account_filter=None, usage
|
|
|
147
151
|
account_filter: Optional account ID filter
|
|
148
152
|
usage_type_filter: Optional usage type filter
|
|
149
153
|
resources: If True, query CUR for resource-level details
|
|
154
|
+
dimension: Dimension to analyze by (service, account, region, usage_type, resource, etc.)
|
|
155
|
+
backend: Backend to use ('auto', 'ce', 'athena')
|
|
150
156
|
|
|
151
157
|
Returns:
|
|
152
158
|
dict: drill data or resource data
|
|
153
159
|
"""
|
|
154
|
-
|
|
160
|
+
profile_name = config.get('profile_name', config.get('name'))
|
|
155
161
|
|
|
156
162
|
if not is_api_configured():
|
|
157
163
|
raise Exception(
|
|
@@ -172,12 +178,16 @@ def execute_drill(config, weeks, service_filter=None, account_filter=None, usage
|
|
|
172
178
|
kwargs['account'] = account_filter
|
|
173
179
|
if usage_type_filter:
|
|
174
180
|
kwargs['usage_type'] = usage_type_filter
|
|
181
|
+
if dimension:
|
|
182
|
+
kwargs['dimension'] = dimension
|
|
183
|
+
if backend and backend != 'auto':
|
|
184
|
+
kwargs['backend'] = backend
|
|
175
185
|
if resources:
|
|
176
|
-
if not service_filter:
|
|
177
|
-
raise click.ClickException("--service is required when using --resources flag")
|
|
186
|
+
if not service_filter and not dimension:
|
|
187
|
+
raise click.ClickException("--service or --dimension is required when using --resources flag")
|
|
178
188
|
kwargs['resources'] = True
|
|
179
189
|
|
|
180
|
-
return call_lambda_api('drill', credentials,
|
|
190
|
+
return call_lambda_api('drill', credentials, profile=profile_name, **kwargs)
|
|
181
191
|
|
|
182
192
|
|
|
183
193
|
def execute_analyze(config, weeks, analysis_type, pattern=None, min_cost=None):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/forensics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|