aws-cost-calculator-cli 1.6.3__py3-none-any.whl → 1.9.1__py3-none-any.whl

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.

Potentially problematic release.


This version of aws-cost-calculator-cli might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-cost-calculator-cli
3
- Version: 1.6.3
3
+ Version: 1.9.1
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
@@ -229,6 +229,16 @@ cc setup-api
229
229
  # Enter your API secret when prompted (input will be hidden)
230
230
  ```
231
231
 
232
+ **CUR Configuration (for --resources flag):**
233
+
234
+ To use resource-level queries, configure CUR settings:
235
+ ```bash
236
+ cc setup-cur
237
+ # Enter: Database name, table name, S3 output location
238
+ ```
239
+
240
+ This saves configuration to `~/.config/cost-calculator/cur_config.json`
241
+
232
242
  Or set manually:
233
243
  ```bash
234
244
  export COST_API_SECRET="your-api-secret"
@@ -374,11 +384,13 @@ The `drill` command allows you to investigate cost changes at different levels o
374
384
  1. **Start broad:** `cc trends` → See EC2 costs up $1000
375
385
  2. **Drill by service:** `cc drill --service "EC2 - Other"` → See which accounts
376
386
  3. **Drill deeper:** `cc drill --service "EC2 - Other" --account 123` → See usage types
387
+ 4. **Resource-level:** `cc drill --service "EC2 - Other" --account 123 --resources` → See individual instance IDs
377
388
 
378
389
  **Features:**
379
390
  - **Week-over-week cost analysis**: Compare costs between consecutive weeks
380
391
  - **Month-over-month cost analysis**: Compare costs between consecutive months
381
392
  - **Drill-down analysis**: Analyze costs by service, account, or usage type
393
+ - **Resource-level analysis**: See individual resource IDs and costs using CUR data (NEW in v1.7.0)
382
394
  - **Pandas aggregations**: Time series analysis with sum, avg, std across all weeks
383
395
  - **Volatility detection**: Identify services with high cost variability and outliers
384
396
  - **Trend detection**: Auto-detect increasing/decreasing cost patterns
@@ -0,0 +1,15 @@
1
+ aws_cost_calculator_cli-1.9.1.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
2
+ cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
3
+ cost_calculator/api_client.py,sha256=4ZI2XcGIN3FBeQqb7xOxQ91kCoeM43-rExiOELXoKBQ,2485
4
+ cost_calculator/cli.py,sha256=IRlRxefn9rfrt6EWwSEcMi9HCISHuOfnUs2_Bf5IZkA,54085
5
+ cost_calculator/cur.py,sha256=QaZ_nyDSw5_cti-h5Ho6eYLbqzY5TWoub24DpyzIiSs,9502
6
+ cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
7
+ cost_calculator/executor.py,sha256=vZX3BCgTRHwBfxC0WqQsHgv1ww5rpmKqLCTrW2sflSY,6509
8
+ cost_calculator/forensics.py,sha256=uhRo3I_zOeMEaBENHfgq65URga31W0Z4vzS2UN6VmTY,12819
9
+ cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
10
+ cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
11
+ aws_cost_calculator_cli-1.9.1.dist-info/METADATA,sha256=jMNKiSSMAbMNajhVxBOyzSuJaWg3lrGfuCBSz0THo38,11978
12
+ aws_cost_calculator_cli-1.9.1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
13
+ aws_cost_calculator_cli-1.9.1.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
14
+ aws_cost_calculator_cli-1.9.1.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
15
+ aws_cost_calculator_cli-1.9.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.7.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -50,7 +50,8 @@ def call_lambda_api(endpoint, credentials, accounts, **kwargs):
50
50
  'monthly': 'https://6aueebodw6q4zdeu3aaexb6tle0fqhhr.lambda-url.us-east-1.on.aws/',
51
51
  'drill': 'https://3ncm2gzxrsyptrhud3ua3x5lju0akvsr.lambda-url.us-east-1.on.aws/',
52
52
  'analyze': 'https://y6npmidtxwzg62nrqzkbacfs5q0edwgs.lambda-url.us-east-1.on.aws/',
53
- 'profiles': 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.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
54
55
  }
55
56
 
56
57
  url = endpoint_urls.get(endpoint)
cost_calculator/cli.py CHANGED
@@ -136,8 +136,11 @@ def load_profile(profile_name):
136
136
  profile_data = response_data.get('profile', response_data)
137
137
  profile = {'accounts': profile_data['accounts']}
138
138
 
139
+ # If profile has aws_profile field, use it
140
+ if 'aws_profile' in profile_data:
141
+ profile['aws_profile'] = profile_data['aws_profile']
139
142
  # Check for AWS_PROFILE environment variable (SSO support)
140
- if os.environ.get('AWS_PROFILE'):
143
+ elif os.environ.get('AWS_PROFILE'):
141
144
  profile['aws_profile'] = os.environ['AWS_PROFILE']
142
145
  # Use environment credentials
143
146
  elif os.environ.get('AWS_ACCESS_KEY_ID'):
@@ -146,6 +149,33 @@ def load_profile(profile_name):
146
149
  'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
147
150
  'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
148
151
  }
152
+ else:
153
+ # Try to find a matching AWS profile by name
154
+ # This allows "khoros" profile to work with "khoros_umbrella" AWS profile
155
+ import subprocess
156
+ try:
157
+ result = subprocess.run(
158
+ ['aws', 'configure', 'list-profiles'],
159
+ capture_output=True,
160
+ text=True,
161
+ timeout=5
162
+ )
163
+ if result.returncode == 0:
164
+ available_profiles = result.stdout.strip().split('\n')
165
+ # Try exact match first
166
+ if profile_name in available_profiles:
167
+ profile['aws_profile'] = profile_name
168
+ # Try with common suffixes
169
+ elif f"{profile_name}_umbrella" in available_profiles:
170
+ profile['aws_profile'] = f"{profile_name}_umbrella"
171
+ elif f"{profile_name}-umbrella" in available_profiles:
172
+ profile['aws_profile'] = f"{profile_name}-umbrella"
173
+ elif f"{profile_name}_prod" in available_profiles:
174
+ profile['aws_profile'] = f"{profile_name}_prod"
175
+ # If no match found, leave it unset - user must provide --sso
176
+ except:
177
+ # If we can't list profiles, leave it unset - user must provide --sso
178
+ pass
149
179
 
150
180
  return profile
151
181
  else:
@@ -325,7 +355,8 @@ def calculate_costs(profile_config, accounts, start_date, offset, window):
325
355
  # Calculate days in the month that the support covers
326
356
  # Support on Nov 1 covers October (31 days)
327
357
  support_month = support_month_date - timedelta(days=1) # Go back to previous month
328
- days_in_support_month = support_month.day # This gives us the last day of the month
358
+ import calendar
359
+ days_in_support_month = calendar.monthrange(support_month.year, support_month.month)[1]
329
360
 
330
361
  # Support allocation: divide by 2 (50% allocation), then by days in month
331
362
  support_per_day = (support_cost / 2) / days_in_support_month
@@ -386,6 +417,43 @@ def cli():
386
417
  pass
387
418
 
388
419
 
420
+ @cli.command('setup-cur')
421
+ @click.option('--database', required=True, prompt='CUR Athena Database', help='Athena database name for CUR')
422
+ @click.option('--table', required=True, prompt='CUR Table Name', help='CUR table name')
423
+ @click.option('--s3-output', required=True, prompt='S3 Output Location', help='S3 bucket for Athena query results')
424
+ def setup_cur(database, table, s3_output):
425
+ """
426
+ Configure CUR (Cost and Usage Report) settings for resource-level queries
427
+
428
+ Saves CUR configuration to ~/.config/cost-calculator/cur_config.json
429
+
430
+ Example:
431
+ cc setup-cur --database my_cur_db --table cur_table --s3-output s3://my-bucket/
432
+ """
433
+ import json
434
+
435
+ config_dir = Path.home() / '.config' / 'cost-calculator'
436
+ config_dir.mkdir(parents=True, exist_ok=True)
437
+
438
+ config_file = config_dir / 'cur_config.json'
439
+
440
+ config = {
441
+ 'database': database,
442
+ 'table': table,
443
+ 's3_output': s3_output
444
+ }
445
+
446
+ with open(config_file, 'w') as f:
447
+ json.dump(config, f, indent=2)
448
+
449
+ click.echo(f"✓ CUR configuration saved to {config_file}")
450
+ click.echo(f" Database: {database}")
451
+ click.echo(f" Table: {table}")
452
+ click.echo(f" S3 Output: {s3_output}")
453
+ click.echo("")
454
+ click.echo("You can now use: cc drill --service 'EC2 - Other' --resources")
455
+
456
+
389
457
  @cli.command('setup-api')
390
458
  @click.option('--api-secret', required=True, prompt=True, hide_input=True, help='COST_API_SECRET value')
391
459
  def setup_api(api_secret):
@@ -887,14 +955,19 @@ def monthly(profile, months, output, json_output, sso, access_key_id, secret_acc
887
955
  @click.option('--service', help='Filter by service name (e.g., "EC2 - Other")')
888
956
  @click.option('--account', help='Filter by account ID')
889
957
  @click.option('--usage-type', help='Filter by usage type')
958
+ @click.option('--resources', is_flag=True, help='Show individual resource IDs (requires CUR, uses Athena)')
890
959
  @click.option('--output', default='drill_down.md', help='Output markdown file (default: drill_down.md)')
891
960
  @click.option('--json-output', is_flag=True, help='Output as JSON')
892
961
  @click.option('--sso', help='AWS SSO profile name')
893
962
  @click.option('--access-key-id', help='AWS Access Key ID')
894
963
  @click.option('--secret-access-key', help='AWS Secret Access Key')
895
964
  @click.option('--session-token', help='AWS Session Token')
896
- def drill(profile, weeks, service, account, usage_type, output, json_output, sso, access_key_id, secret_access_key, session_token):
897
- """Drill down into cost changes by service, account, or usage type"""
965
+ def drill(profile, weeks, service, account, usage_type, resources, output, json_output, sso, access_key_id, secret_access_key, session_token):
966
+ """
967
+ Drill down into cost changes by service, account, or usage type
968
+
969
+ Add --resources flag to see individual resource IDs and costs (requires CUR data via Athena)
970
+ """
898
971
 
899
972
  # Load profile
900
973
  config = load_profile(profile)
@@ -908,10 +981,19 @@ def drill(profile, weeks, service, account, usage_type, output, json_output, sso
908
981
  click.echo(f" Account filter: {account}")
909
982
  if usage_type:
910
983
  click.echo(f" Usage type filter: {usage_type}")
984
+ if resources:
985
+ click.echo(f" Mode: Resource-level (CUR via Athena)")
911
986
  click.echo("")
912
987
 
913
988
  # Execute via API or locally
914
- drill_data = execute_drill(config, weeks, service, account, usage_type)
989
+ drill_data = execute_drill(config, weeks, service, account, usage_type, resources)
990
+
991
+ # Handle resource-level output differently
992
+ if resources:
993
+ from cost_calculator.cur import format_resource_output
994
+ output_text = format_resource_output(drill_data)
995
+ click.echo(output_text)
996
+ return
915
997
 
916
998
  if json_output:
917
999
  # Output as JSON
@@ -1084,5 +1166,219 @@ def profile(operation, name, accounts, description):
1084
1166
  click.echo(result.get('message', 'Operation completed'))
1085
1167
 
1086
1168
 
1169
+ @cli.command()
1170
+ @click.option('--profile', required=True, help='Profile name')
1171
+ @click.option('--sso', help='AWS SSO profile to use')
1172
+ @click.option('--weeks', default=8, help='Number of weeks to analyze')
1173
+ @click.option('--account', help='Focus on specific account ID')
1174
+ @click.option('--service', help='Focus on specific service')
1175
+ @click.option('--no-cloudtrail', is_flag=True, help='Skip CloudTrail analysis (faster)')
1176
+ @click.option('--output', default='investigation_report.md', help='Output file path')
1177
+ def investigate(profile, sso, weeks, account, service, no_cloudtrail, output):
1178
+ """
1179
+ Multi-stage cost investigation:
1180
+ 1. Analyze cost trends and drill-downs
1181
+ 2. Inventory actual resources in problem accounts
1182
+ 3. Analyze CloudTrail events (optional)
1183
+ 4. Generate comprehensive report
1184
+ """
1185
+ from cost_calculator.executor import execute_trends, execute_drill, get_credentials_dict
1186
+ from cost_calculator.api_client import call_lambda_api, is_api_configured
1187
+ from cost_calculator.forensics import format_investigation_report
1188
+ from datetime import datetime, timedelta
1189
+
1190
+ click.echo("=" * 80)
1191
+ click.echo("COST INVESTIGATION")
1192
+ click.echo("=" * 80)
1193
+ click.echo(f"Profile: {profile}")
1194
+ click.echo(f"Weeks: {weeks}")
1195
+ if account:
1196
+ click.echo(f"Account: {account}")
1197
+ if service:
1198
+ click.echo(f"Service: {service}")
1199
+ click.echo("")
1200
+
1201
+ # Load profile
1202
+ config = load_profile(profile)
1203
+
1204
+ # Override with SSO if provided
1205
+ if sso:
1206
+ config['aws_profile'] = sso
1207
+
1208
+ # Validate that we have a way to get credentials
1209
+ if 'aws_profile' not in config and 'credentials' not in config:
1210
+ import subprocess
1211
+ try:
1212
+ result = subprocess.run(
1213
+ ['aws', 'configure', 'list-profiles'],
1214
+ capture_output=True,
1215
+ text=True,
1216
+ timeout=5
1217
+ )
1218
+ available = result.stdout.strip().split('\n') if result.returncode == 0 else []
1219
+ suggestion = f"\nAvailable AWS profiles: {', '.join(available[:5])}" if available else ""
1220
+ except:
1221
+ suggestion = ""
1222
+
1223
+ raise click.ClickException(
1224
+ f"Profile '{profile}' has no AWS authentication configured.\n"
1225
+ f"Use --sso flag to specify your AWS SSO profile:\n"
1226
+ f" cc investigate --profile {profile} --sso YOUR_AWS_PROFILE{suggestion}"
1227
+ )
1228
+
1229
+ # Step 1: Cost Analysis
1230
+ click.echo("Step 1/3: Analyzing cost trends...")
1231
+ try:
1232
+ trends_data = execute_trends(config, weeks)
1233
+ click.echo(f"✓ Found cost data for {weeks} weeks")
1234
+ except Exception as e:
1235
+ click.echo(f"✗ Error analyzing trends: {str(e)}")
1236
+ trends_data = None
1237
+
1238
+ # Step 2: Drill-down
1239
+ click.echo("\nStep 2/3: Drilling down into costs...")
1240
+ drill_data = None
1241
+ if service or account:
1242
+ try:
1243
+ drill_data = execute_drill(config, weeks, service, account, None, False)
1244
+ click.echo(f"✓ Drill-down complete")
1245
+ except Exception as e:
1246
+ click.echo(f"✗ Error in drill-down: {str(e)}")
1247
+
1248
+ # Step 3: Resource Inventory
1249
+ click.echo("\nStep 3/3: Inventorying resources...")
1250
+ inventories = []
1251
+ cloudtrail_analyses = []
1252
+
1253
+ # Determine which accounts to investigate
1254
+ accounts_to_investigate = []
1255
+ if account:
1256
+ accounts_to_investigate = [account]
1257
+ else:
1258
+ # Extract top cost accounts from trends/drill data
1259
+ # For now, we'll need the user to specify
1260
+ click.echo("⚠️ No account specified. Use --account to inventory resources.")
1261
+
1262
+ # For each account, do inventory and CloudTrail via backend API
1263
+ for acc_id in accounts_to_investigate:
1264
+ click.echo(f"\n Investigating account {acc_id}...")
1265
+
1266
+ # Get credentials (SSO or static)
1267
+ account_creds = get_credentials_dict(config)
1268
+ if not account_creds:
1269
+ click.echo(f" ⚠️ No credentials available for account")
1270
+ continue
1271
+
1272
+ # Inventory resources via backend API only
1273
+ if not is_api_configured():
1274
+ click.echo(f" ✗ API not configured. Set COST_API_SECRET environment variable.")
1275
+ continue
1276
+
1277
+ try:
1278
+ regions = ['us-west-2', 'us-east-1', 'eu-west-1']
1279
+ for region in regions:
1280
+ try:
1281
+ inv = call_lambda_api(
1282
+ 'forensics',
1283
+ account_creds,
1284
+ [], # accounts not needed for forensics
1285
+ operation='inventory',
1286
+ account_id=acc_id,
1287
+ region=region
1288
+ )
1289
+
1290
+ if not inv.get('error'):
1291
+ inventories.append(inv)
1292
+ click.echo(f" ✓ Inventory complete for {region}")
1293
+ click.echo(f" - EC2: {len(inv['ec2_instances'])} instances")
1294
+ click.echo(f" - EFS: {len(inv['efs_file_systems'])} file systems ({inv.get('total_efs_size_gb', 0):,.0f} GB)")
1295
+ click.echo(f" - ELB: {len(inv['load_balancers'])} load balancers")
1296
+ break
1297
+ except Exception as e:
1298
+ continue
1299
+ except Exception as e:
1300
+ click.echo(f" ✗ Inventory error: {str(e)}")
1301
+
1302
+ # CloudTrail analysis via backend API only
1303
+ if not no_cloudtrail:
1304
+ if not is_api_configured():
1305
+ click.echo(f" ✗ CloudTrail skipped: API not configured")
1306
+ else:
1307
+ try:
1308
+ start_date = (datetime.now() - timedelta(days=weeks * 7)).isoformat() + 'Z'
1309
+ end_date = datetime.now().isoformat() + 'Z'
1310
+
1311
+ ct_analysis = call_lambda_api(
1312
+ 'forensics',
1313
+ account_creds,
1314
+ [],
1315
+ operation='cloudtrail',
1316
+ account_id=acc_id,
1317
+ start_date=start_date,
1318
+ end_date=end_date,
1319
+ region='us-west-2'
1320
+ )
1321
+
1322
+ cloudtrail_analyses.append(ct_analysis)
1323
+
1324
+ if ct_analysis.get('error'):
1325
+ click.echo(f" ⚠️ CloudTrail: {ct_analysis['error']}")
1326
+ else:
1327
+ click.echo(f" ✓ CloudTrail analysis complete")
1328
+ click.echo(f" - {len(ct_analysis['event_summary'])} event types")
1329
+ click.echo(f" - {len(ct_analysis['write_events'])} resource changes")
1330
+ except Exception as e:
1331
+ click.echo(f" ✗ CloudTrail error: {str(e)}")
1332
+
1333
+ # Generate report
1334
+ click.echo(f"\nGenerating report...")
1335
+ report = format_investigation_report(trends_data, inventories, cloudtrail_analyses if not no_cloudtrail else None)
1336
+
1337
+ # Write to file
1338
+ with open(output, 'w') as f:
1339
+ f.write(report)
1340
+
1341
+ click.echo(f"\n✓ Investigation complete!")
1342
+ click.echo(f"✓ Report saved to: {output}")
1343
+ click.echo("")
1344
+
1345
+
1346
+ def find_account_profile(account_id):
1347
+ """
1348
+ Find the SSO profile name for a given account ID
1349
+ Returns profile name or None
1350
+ """
1351
+ import subprocess
1352
+
1353
+ try:
1354
+ # Get list of profiles
1355
+ result = subprocess.run(
1356
+ ['aws', 'configure', 'list-profiles'],
1357
+ capture_output=True,
1358
+ text=True
1359
+ )
1360
+
1361
+ profiles = result.stdout.strip().split('\n')
1362
+
1363
+ # Check each profile
1364
+ for profile in profiles:
1365
+ try:
1366
+ result = subprocess.run(
1367
+ ['aws', 'sts', 'get-caller-identity', '--profile', profile],
1368
+ capture_output=True,
1369
+ text=True,
1370
+ timeout=5
1371
+ )
1372
+
1373
+ if account_id in result.stdout:
1374
+ return profile
1375
+ except:
1376
+ continue
1377
+
1378
+ return None
1379
+ except:
1380
+ return None
1381
+
1382
+
1087
1383
  if __name__ == '__main__':
1088
1384
  cli()