aws-cost-calculator-cli 1.3.0__py3-none-any.whl → 1.6.0__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.
- {aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.6.0.dist-info}/METADATA +41 -22
- aws_cost_calculator_cli-1.6.0.dist-info/RECORD +13 -0
- {aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.6.0.dist-info}/WHEEL +1 -1
- cost_calculator/api_client.py +84 -0
- cost_calculator/cli.py +291 -137
- cost_calculator/executor.py +241 -0
- aws_cost_calculator_cli-1.3.0.dist-info/RECORD +0 -11
- {aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.6.0.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.6.0.dist-info}/top_level.txt +0 -0
{aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.6.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-cost-calculator-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: AWS Cost Calculator CLI - Calculate daily and annual AWS costs across multiple accounts
|
|
5
5
|
Home-page: https://github.com/yourusername/cost-calculator
|
|
6
6
|
Author: Cost Optimization Team
|
|
@@ -23,6 +23,7 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: click>=8.0.0
|
|
25
25
|
Requires-Dist: boto3>=1.26.0
|
|
26
|
+
Requires-Dist: requests>=2.28.0
|
|
26
27
|
Dynamic: author
|
|
27
28
|
Dynamic: classifier
|
|
28
29
|
Dynamic: description
|
|
@@ -48,36 +49,51 @@ pip install -e .
|
|
|
48
49
|
|
|
49
50
|
## Quick Start
|
|
50
51
|
|
|
51
|
-
###
|
|
52
|
+
### Authentication Methods
|
|
52
53
|
|
|
54
|
+
The CLI supports three authentication methods:
|
|
55
|
+
|
|
56
|
+
#### 1. SSO (Recommended)
|
|
53
57
|
```bash
|
|
54
|
-
|
|
58
|
+
# The CLI will automatically trigger SSO login if needed
|
|
59
|
+
cc calculate --profile khoros --sso khoros_umbrella
|
|
55
60
|
```
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
#### 2. Static Credentials
|
|
63
|
+
```bash
|
|
64
|
+
cc calculate --profile khoros \
|
|
65
|
+
--access-key-id ASIA3D3QOXPO6EBAPXVI \
|
|
66
|
+
--secret-access-key /9ijZEUoszN/S2A8IlCrHpW+1fMZ7aUb7fPvU0dL \
|
|
67
|
+
--session-token IQoJb3JpZ2luX2VjENr...
|
|
68
|
+
```
|
|
60
69
|
|
|
70
|
+
#### 3. Environment Variables
|
|
61
71
|
```bash
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
# For SSO
|
|
73
|
+
export AWS_PROFILE=khoros_umbrella
|
|
74
|
+
cc calculate --profile khoros
|
|
75
|
+
|
|
76
|
+
# For static credentials
|
|
77
|
+
export AWS_ACCESS_KEY_ID=ASIA...
|
|
78
|
+
export AWS_SECRET_ACCESS_KEY=...
|
|
79
|
+
export AWS_SESSION_TOKEN=...
|
|
80
|
+
cc calculate --profile khoros
|
|
65
81
|
```
|
|
66
82
|
|
|
67
|
-
###
|
|
83
|
+
### Basic Usage
|
|
68
84
|
|
|
69
85
|
```bash
|
|
70
86
|
# Default: Today minus 2 days, going back 30 days
|
|
71
|
-
cc calculate --profile
|
|
87
|
+
cc calculate --profile khoros --sso khoros_umbrella
|
|
72
88
|
|
|
73
89
|
# Specific start date
|
|
74
|
-
cc calculate --profile
|
|
90
|
+
cc calculate --profile khoros --sso khoros_umbrella --start-date 2025-11-04
|
|
75
91
|
|
|
76
92
|
# Custom offset and window
|
|
77
|
-
cc calculate --profile
|
|
93
|
+
cc calculate --profile khoros --sso khoros_umbrella --offset 2 --window 30
|
|
78
94
|
|
|
79
95
|
# JSON output
|
|
80
|
-
cc calculate --profile
|
|
96
|
+
cc calculate --profile khoros --sso khoros_umbrella --json-output
|
|
81
97
|
```
|
|
82
98
|
|
|
83
99
|
### 4. Analyze cost trends
|
|
@@ -241,14 +257,17 @@ The `drill` command allows you to investigate cost changes at different levels o
|
|
|
241
257
|
3. **Drill deeper:** `cc drill --service "EC2 - Other" --account 123` → See usage types
|
|
242
258
|
|
|
243
259
|
**Features:**
|
|
244
|
-
- **
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
- **
|
|
250
|
-
- **
|
|
251
|
-
- **
|
|
260
|
+
- **Week-over-week cost analysis**: Compare costs between consecutive weeks
|
|
261
|
+
- **Month-over-month cost analysis**: Compare costs between consecutive months
|
|
262
|
+
- **Drill-down analysis**: Analyze costs by service, account, or usage type
|
|
263
|
+
- **Pandas aggregations**: Time series analysis with sum, avg, std across all weeks
|
|
264
|
+
- **Volatility detection**: Identify services with high cost variability and outliers
|
|
265
|
+
- **Trend detection**: Auto-detect increasing/decreasing cost patterns
|
|
266
|
+
- **Search & filter**: Find services by pattern or cost threshold
|
|
267
|
+
- **Profile management**: CRUD operations for account profiles in DynamoDB
|
|
268
|
+
- **Markdown reports**: Generate formatted reports
|
|
269
|
+
- **JSON output**: Machine-readable output for automation
|
|
270
|
+
- **Lambda API backend**: Serverless backend with pandas/numpy support
|
|
252
271
|
|
|
253
272
|
Example output:
|
|
254
273
|
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
aws_cost_calculator_cli-1.6.0.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
|
|
2
|
+
cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
|
|
3
|
+
cost_calculator/api_client.py,sha256=LUzQmveDF0X9MqAyThp9mbSzJzkOO73Pk4F7IEJjASU,2353
|
|
4
|
+
cost_calculator/cli.py,sha256=sJYvzbdHCxOEcCgOjZs4o9MOogV1Yh8r7x0hJtd__K0,38639
|
|
5
|
+
cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
|
|
6
|
+
cost_calculator/executor.py,sha256=tVyyBtXIj9OPyG-xQj8CUmyFjDhb9IVK639360dUZDc,8076
|
|
7
|
+
cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
|
|
8
|
+
cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
|
|
9
|
+
aws_cost_calculator_cli-1.6.0.dist-info/METADATA,sha256=AaLbUH-anBc1lv2o2DpdJR3v0tSavhPtDo5Sjb7VsHA,8612
|
|
10
|
+
aws_cost_calculator_cli-1.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
aws_cost_calculator_cli-1.6.0.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
12
|
+
aws_cost_calculator_cli-1.6.0.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
|
|
13
|
+
aws_cost_calculator_cli-1.6.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API client for calling Lambda backend.
|
|
3
|
+
Falls back to local execution if API is not configured.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import requests
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_api_config():
|
|
12
|
+
"""
|
|
13
|
+
Get API configuration.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
dict: API configuration with api_secret, or None if not configured
|
|
17
|
+
"""
|
|
18
|
+
api_secret = os.environ.get('COST_API_SECRET', '')
|
|
19
|
+
|
|
20
|
+
if api_secret:
|
|
21
|
+
return {'api_secret': api_secret}
|
|
22
|
+
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def call_lambda_api(endpoint, credentials, accounts, **kwargs):
|
|
27
|
+
"""
|
|
28
|
+
Call Lambda API endpoint.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
endpoint: API endpoint name ('trends', 'monthly', 'drill')
|
|
32
|
+
credentials: dict with AWS credentials
|
|
33
|
+
accounts: list of account IDs
|
|
34
|
+
**kwargs: additional parameters for the specific endpoint
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dict: API response data
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
Exception: if API call fails
|
|
41
|
+
"""
|
|
42
|
+
api_config = get_api_config()
|
|
43
|
+
|
|
44
|
+
if not api_config:
|
|
45
|
+
raise Exception("API not configured. Set COST_API_SECRET environment variable.")
|
|
46
|
+
|
|
47
|
+
# Map endpoint names to Lambda URLs
|
|
48
|
+
endpoint_urls = {
|
|
49
|
+
'trends': 'https://pq3mqntc6vuwi4zw5flulsoleq0yiqtl.lambda-url.us-east-1.on.aws/',
|
|
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
|
+
}
|
|
55
|
+
|
|
56
|
+
url = endpoint_urls.get(endpoint)
|
|
57
|
+
|
|
58
|
+
if not url:
|
|
59
|
+
raise Exception(f"Unknown endpoint: {endpoint}")
|
|
60
|
+
|
|
61
|
+
# Build request payload
|
|
62
|
+
payload = {
|
|
63
|
+
'credentials': credentials,
|
|
64
|
+
'accounts': accounts
|
|
65
|
+
}
|
|
66
|
+
payload.update(kwargs)
|
|
67
|
+
|
|
68
|
+
# Make API call
|
|
69
|
+
headers = {
|
|
70
|
+
'X-API-Secret': api_config['api_secret'],
|
|
71
|
+
'Content-Type': 'application/json'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
response = requests.post(url, headers=headers, json=payload, timeout=300)
|
|
75
|
+
|
|
76
|
+
if response.status_code != 200:
|
|
77
|
+
raise Exception(f"API call failed: {response.status_code} - {response.text}")
|
|
78
|
+
|
|
79
|
+
return response.json()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_api_configured():
|
|
83
|
+
"""Check if API is configured."""
|
|
84
|
+
return get_api_config() is not None
|
cost_calculator/cli.py
CHANGED
|
@@ -13,54 +13,149 @@ import boto3
|
|
|
13
13
|
import json
|
|
14
14
|
from datetime import datetime, timedelta
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from cost_calculator.trends import
|
|
17
|
-
from cost_calculator.monthly import
|
|
18
|
-
from cost_calculator.drill import
|
|
16
|
+
from cost_calculator.trends import format_trends_markdown
|
|
17
|
+
from cost_calculator.monthly import format_monthly_markdown
|
|
18
|
+
from cost_calculator.drill import format_drill_down_markdown
|
|
19
|
+
from cost_calculator.executor import execute_trends, execute_monthly, execute_drill
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def apply_auth_options(config, sso=None, access_key_id=None, secret_access_key=None, session_token=None):
|
|
23
|
+
"""Apply authentication options to profile config
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: Profile configuration dict
|
|
27
|
+
sso: AWS SSO profile name
|
|
28
|
+
access_key_id: AWS Access Key ID
|
|
29
|
+
secret_access_key: AWS Secret Access Key
|
|
30
|
+
session_token: AWS Session Token
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Updated config dict
|
|
34
|
+
"""
|
|
35
|
+
import subprocess
|
|
36
|
+
|
|
37
|
+
if sso:
|
|
38
|
+
# SSO authentication - trigger login if needed
|
|
39
|
+
try:
|
|
40
|
+
# Test if SSO session is valid
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
['aws', 'sts', 'get-caller-identity', '--profile', sso],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
timeout=5
|
|
46
|
+
)
|
|
47
|
+
if result.returncode != 0:
|
|
48
|
+
if 'expired' in result.stderr.lower() or 'token' in result.stderr.lower():
|
|
49
|
+
click.echo(f"SSO session expired or not initialized. Logging in...")
|
|
50
|
+
subprocess.run(['aws', 'sso', 'login', '--profile', sso], check=True)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
click.echo(f"Warning: Could not verify SSO session: {e}")
|
|
53
|
+
|
|
54
|
+
config['aws_profile'] = sso
|
|
55
|
+
elif access_key_id and secret_access_key:
|
|
56
|
+
# Static credentials provided via CLI
|
|
57
|
+
config['credentials'] = {
|
|
58
|
+
'aws_access_key_id': access_key_id,
|
|
59
|
+
'aws_secret_access_key': secret_access_key,
|
|
60
|
+
'region': 'us-east-1'
|
|
61
|
+
}
|
|
62
|
+
if session_token:
|
|
63
|
+
config['credentials']['aws_session_token'] = session_token
|
|
64
|
+
|
|
65
|
+
return config
|
|
19
66
|
|
|
20
67
|
|
|
21
68
|
def load_profile(profile_name):
|
|
22
|
-
"""Load profile configuration from
|
|
69
|
+
"""Load profile configuration from local file or DynamoDB API"""
|
|
70
|
+
import os
|
|
71
|
+
import requests
|
|
72
|
+
|
|
23
73
|
config_dir = Path.home() / '.config' / 'cost-calculator'
|
|
24
74
|
config_file = config_dir / 'profiles.json'
|
|
25
75
|
creds_file = config_dir / 'credentials.json'
|
|
26
76
|
|
|
27
|
-
|
|
77
|
+
# Try local file first
|
|
78
|
+
if config_file.exists():
|
|
79
|
+
with open(config_file) as f:
|
|
80
|
+
profiles = json.load(f)
|
|
81
|
+
|
|
82
|
+
if profile_name in profiles:
|
|
83
|
+
profile = profiles[profile_name]
|
|
84
|
+
|
|
85
|
+
# Load credentials if using static credentials (not SSO)
|
|
86
|
+
if 'aws_profile' not in profile:
|
|
87
|
+
if not creds_file.exists():
|
|
88
|
+
# Try environment variables
|
|
89
|
+
if os.environ.get('AWS_ACCESS_KEY_ID'):
|
|
90
|
+
profile['credentials'] = {
|
|
91
|
+
'aws_access_key_id': os.environ['AWS_ACCESS_KEY_ID'],
|
|
92
|
+
'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
|
|
93
|
+
'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
|
|
94
|
+
}
|
|
95
|
+
return profile
|
|
96
|
+
|
|
97
|
+
raise click.ClickException(
|
|
98
|
+
f"No credentials found for profile '{profile_name}'.\n"
|
|
99
|
+
f"Run: cc configure --profile {profile_name}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
with open(creds_file) as f:
|
|
103
|
+
creds = json.load(f)
|
|
104
|
+
|
|
105
|
+
if profile_name not in creds:
|
|
106
|
+
raise click.ClickException(
|
|
107
|
+
f"No credentials found for profile '{profile_name}'.\n"
|
|
108
|
+
f"Run: cc configure --profile {profile_name}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
profile['credentials'] = creds[profile_name]
|
|
112
|
+
|
|
113
|
+
return profile
|
|
114
|
+
|
|
115
|
+
# Profile not found locally - try DynamoDB API
|
|
116
|
+
api_secret = os.environ.get('COST_API_SECRET')
|
|
117
|
+
if not api_secret:
|
|
28
118
|
raise click.ClickException(
|
|
29
|
-
f"Profile
|
|
119
|
+
f"Profile '{profile_name}' not found locally and COST_API_SECRET not set.\n"
|
|
30
120
|
f"Run: cc init --profile {profile_name}"
|
|
31
121
|
)
|
|
32
122
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
f"Available profiles: {', '.join(profiles.keys())}"
|
|
123
|
+
try:
|
|
124
|
+
response = requests.post(
|
|
125
|
+
'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/',
|
|
126
|
+
headers={'X-API-Secret': api_secret, 'Content-Type': 'application/json'},
|
|
127
|
+
json={'operation': 'get', 'profile_name': profile_name},
|
|
128
|
+
timeout=10
|
|
40
129
|
)
|
|
41
|
-
|
|
42
|
-
profile = profiles[profile_name]
|
|
43
|
-
|
|
44
|
-
# Load credentials if using static credentials (not SSO)
|
|
45
|
-
if 'aws_profile' not in profile:
|
|
46
|
-
if not creds_file.exists():
|
|
47
|
-
raise click.ClickException(
|
|
48
|
-
f"No credentials found for profile '{profile_name}'.\n"
|
|
49
|
-
f"Run: cc configure --profile {profile_name}"
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
with open(creds_file) as f:
|
|
53
|
-
creds = json.load(f)
|
|
54
130
|
|
|
55
|
-
if
|
|
131
|
+
if response.status_code == 200:
|
|
132
|
+
response_data = response.json()
|
|
133
|
+
# API returns {"profile": {...}} wrapper
|
|
134
|
+
profile_data = response_data.get('profile', response_data)
|
|
135
|
+
profile = {'accounts': profile_data['accounts']}
|
|
136
|
+
|
|
137
|
+
# Check for AWS_PROFILE environment variable (SSO support)
|
|
138
|
+
if os.environ.get('AWS_PROFILE'):
|
|
139
|
+
profile['aws_profile'] = os.environ['AWS_PROFILE']
|
|
140
|
+
# Use environment credentials
|
|
141
|
+
elif os.environ.get('AWS_ACCESS_KEY_ID'):
|
|
142
|
+
profile['credentials'] = {
|
|
143
|
+
'aws_access_key_id': os.environ['AWS_ACCESS_KEY_ID'],
|
|
144
|
+
'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
|
|
145
|
+
'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return profile
|
|
149
|
+
else:
|
|
56
150
|
raise click.ClickException(
|
|
57
|
-
f"
|
|
58
|
-
f"Run: cc
|
|
151
|
+
f"Profile '{profile_name}' not found in DynamoDB.\n"
|
|
152
|
+
f"Run: cc profile create --name {profile_name} --accounts \"...\""
|
|
59
153
|
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
154
|
+
except requests.exceptions.RequestException as e:
|
|
155
|
+
raise click.ClickException(
|
|
156
|
+
f"Failed to fetch profile from API: {e}\n"
|
|
157
|
+
f"Run: cc init --profile {profile_name}"
|
|
158
|
+
)
|
|
64
159
|
|
|
65
160
|
|
|
66
161
|
def calculate_costs(profile_config, accounts, start_date, offset, window):
|
|
@@ -295,12 +390,30 @@ def cli():
|
|
|
295
390
|
@click.option('--offset', default=2, help='Days to go back from start date (default: 2)')
|
|
296
391
|
@click.option('--window', default=30, help='Number of days to analyze (default: 30)')
|
|
297
392
|
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
298
|
-
|
|
299
|
-
|
|
393
|
+
@click.option('--sso', help='AWS SSO profile name (e.g., khoros_umbrella)')
|
|
394
|
+
@click.option('--access-key-id', help='AWS Access Key ID (for static credentials)')
|
|
395
|
+
@click.option('--secret-access-key', help='AWS Secret Access Key (for static credentials)')
|
|
396
|
+
@click.option('--session-token', help='AWS Session Token (for static credentials)')
|
|
397
|
+
def calculate(profile, start_date, offset, window, json_output, sso, access_key_id, secret_access_key, session_token):
|
|
398
|
+
"""Calculate AWS costs for the specified period
|
|
399
|
+
|
|
400
|
+
\b
|
|
401
|
+
Authentication Options:
|
|
402
|
+
1. SSO: --sso <profile_name>
|
|
403
|
+
Example: cc calculate --profile khoros --sso khoros_umbrella
|
|
404
|
+
|
|
405
|
+
2. Static Credentials: --access-key-id, --secret-access-key, --session-token
|
|
406
|
+
Example: cc calculate --profile khoros --access-key-id ASIA... --secret-access-key ... --session-token ...
|
|
407
|
+
|
|
408
|
+
3. Environment Variables: AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY
|
|
409
|
+
"""
|
|
300
410
|
|
|
301
411
|
# Load profile configuration
|
|
302
412
|
config = load_profile(profile)
|
|
303
413
|
|
|
414
|
+
# Apply authentication options
|
|
415
|
+
config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
|
|
416
|
+
|
|
304
417
|
# Calculate costs
|
|
305
418
|
result = calculate_costs(
|
|
306
419
|
profile_config=config,
|
|
@@ -546,55 +659,23 @@ def configure(profile, access_key_id, secret_access_key, session_token, region):
|
|
|
546
659
|
@click.option('--profile', required=True, help='Profile name')
|
|
547
660
|
@click.option('--weeks', default=3, help='Number of weeks to analyze (default: 3)')
|
|
548
661
|
@click.option('--output', default='cost_trends.md', help='Output markdown file (default: cost_trends.md)')
|
|
549
|
-
@click.option('--json-output', is_flag=True, help='Output as JSON
|
|
550
|
-
|
|
662
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
663
|
+
@click.option('--sso', help='AWS SSO profile name')
|
|
664
|
+
@click.option('--access-key-id', help='AWS Access Key ID')
|
|
665
|
+
@click.option('--secret-access-key', help='AWS Secret Access Key')
|
|
666
|
+
@click.option('--session-token', help='AWS Session Token')
|
|
667
|
+
def trends(profile, weeks, output, json_output, sso, access_key_id, secret_access_key, session_token):
|
|
551
668
|
"""Analyze cost trends with Week-over-Week and Trailing 30-Day comparisons"""
|
|
552
669
|
|
|
553
670
|
# Load profile configuration
|
|
554
671
|
config = load_profile(profile)
|
|
555
|
-
|
|
556
|
-
# Initialize boto3 client
|
|
557
|
-
try:
|
|
558
|
-
if 'aws_profile' in config:
|
|
559
|
-
aws_profile = config['aws_profile']
|
|
560
|
-
click.echo(f"AWS Profile: {aws_profile} (SSO)")
|
|
561
|
-
session = boto3.Session(profile_name=aws_profile)
|
|
562
|
-
ce_client = session.client('ce', region_name='us-east-1')
|
|
563
|
-
else:
|
|
564
|
-
creds = config['credentials']
|
|
565
|
-
click.echo(f"AWS Credentials: Static")
|
|
566
|
-
|
|
567
|
-
session_kwargs = {
|
|
568
|
-
'aws_access_key_id': creds['aws_access_key_id'],
|
|
569
|
-
'aws_secret_access_key': creds['aws_secret_access_key'],
|
|
570
|
-
'region_name': creds.get('region', 'us-east-1')
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if 'aws_session_token' in creds:
|
|
574
|
-
session_kwargs['aws_session_token'] = creds['aws_session_token']
|
|
575
|
-
|
|
576
|
-
session = boto3.Session(**session_kwargs)
|
|
577
|
-
ce_client = session.client('ce')
|
|
578
|
-
|
|
579
|
-
except Exception as e:
|
|
580
|
-
if 'Token has expired' in str(e) or 'sso' in str(e).lower():
|
|
581
|
-
if 'aws_profile' in config:
|
|
582
|
-
raise click.ClickException(
|
|
583
|
-
f"AWS SSO session expired or not initialized.\n"
|
|
584
|
-
f"Run: aws sso login --profile {config['aws_profile']}"
|
|
585
|
-
)
|
|
586
|
-
else:
|
|
587
|
-
raise click.ClickException(
|
|
588
|
-
f"AWS credentials expired.\n"
|
|
589
|
-
f"Run: cc configure --profile {profile}"
|
|
590
|
-
)
|
|
591
|
-
raise
|
|
672
|
+
config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
|
|
592
673
|
|
|
593
674
|
click.echo(f"Analyzing last {weeks} weeks...")
|
|
594
675
|
click.echo("")
|
|
595
676
|
|
|
596
|
-
#
|
|
597
|
-
trends_data =
|
|
677
|
+
# Execute via API or locally
|
|
678
|
+
trends_data = execute_trends(config, weeks)
|
|
598
679
|
|
|
599
680
|
if json_output:
|
|
600
681
|
# Output as JSON
|
|
@@ -649,41 +730,23 @@ def trends(profile, weeks, output, json_output):
|
|
|
649
730
|
@click.option('--profile', required=True, help='Profile name')
|
|
650
731
|
@click.option('--months', default=6, help='Number of months to analyze (default: 6)')
|
|
651
732
|
@click.option('--output', default='monthly_trends.md', help='Output markdown file (default: monthly_trends.md)')
|
|
652
|
-
@click.option('--json-output', is_flag=True, help='Output as JSON
|
|
653
|
-
|
|
733
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
734
|
+
@click.option('--sso', help='AWS SSO profile name')
|
|
735
|
+
@click.option('--access-key-id', help='AWS Access Key ID')
|
|
736
|
+
@click.option('--secret-access-key', help='AWS Secret Access Key')
|
|
737
|
+
@click.option('--session-token', help='AWS Session Token')
|
|
738
|
+
def monthly(profile, months, output, json_output, sso, access_key_id, secret_access_key, session_token):
|
|
654
739
|
"""Analyze month-over-month cost trends at service level"""
|
|
655
740
|
|
|
656
|
-
# Load profile
|
|
741
|
+
# Load profile
|
|
657
742
|
config = load_profile(profile)
|
|
658
|
-
|
|
659
|
-
# Initialize boto3 client
|
|
660
|
-
try:
|
|
661
|
-
if 'aws_profile' in config:
|
|
662
|
-
aws_profile = config['aws_profile']
|
|
663
|
-
click.echo(f"AWS Profile: {aws_profile} (SSO)")
|
|
664
|
-
session = boto3.Session(profile_name=aws_profile)
|
|
665
|
-
else:
|
|
666
|
-
# Use static credentials
|
|
667
|
-
creds = config['credentials']
|
|
668
|
-
click.echo("AWS Credentials: Static")
|
|
669
|
-
session = boto3.Session(
|
|
670
|
-
aws_access_key_id=creds['aws_access_key_id'],
|
|
671
|
-
aws_secret_access_key=creds['aws_secret_access_key'],
|
|
672
|
-
aws_session_token=creds.get('aws_session_token')
|
|
673
|
-
)
|
|
674
|
-
|
|
675
|
-
ce_client = session.client('ce', region_name='us-east-1')
|
|
676
|
-
except Exception as e:
|
|
677
|
-
raise click.ClickException(f"Failed to initialize AWS session: {str(e)}")
|
|
678
|
-
|
|
679
|
-
# Get account list
|
|
680
|
-
accounts = config['accounts']
|
|
743
|
+
config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
|
|
681
744
|
|
|
682
745
|
click.echo(f"Analyzing last {months} months...")
|
|
683
746
|
click.echo("")
|
|
684
747
|
|
|
685
|
-
#
|
|
686
|
-
monthly_data =
|
|
748
|
+
# Execute via API or locally
|
|
749
|
+
monthly_data = execute_monthly(config, months)
|
|
687
750
|
|
|
688
751
|
if json_output:
|
|
689
752
|
# Output as JSON
|
|
@@ -739,35 +802,17 @@ def monthly(profile, months, output, json_output):
|
|
|
739
802
|
@click.option('--account', help='Filter by account ID')
|
|
740
803
|
@click.option('--usage-type', help='Filter by usage type')
|
|
741
804
|
@click.option('--output', default='drill_down.md', help='Output markdown file (default: drill_down.md)')
|
|
742
|
-
@click.option('--json-output', is_flag=True, help='Output as JSON
|
|
743
|
-
|
|
805
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
806
|
+
@click.option('--sso', help='AWS SSO profile name')
|
|
807
|
+
@click.option('--access-key-id', help='AWS Access Key ID')
|
|
808
|
+
@click.option('--secret-access-key', help='AWS Secret Access Key')
|
|
809
|
+
@click.option('--session-token', help='AWS Session Token')
|
|
810
|
+
def drill(profile, weeks, service, account, usage_type, output, json_output, sso, access_key_id, secret_access_key, session_token):
|
|
744
811
|
"""Drill down into cost changes by service, account, or usage type"""
|
|
745
812
|
|
|
746
|
-
# Load profile
|
|
813
|
+
# Load profile
|
|
747
814
|
config = load_profile(profile)
|
|
748
|
-
|
|
749
|
-
# Initialize boto3 client
|
|
750
|
-
try:
|
|
751
|
-
if 'aws_profile' in config:
|
|
752
|
-
aws_profile = config['aws_profile']
|
|
753
|
-
click.echo(f"AWS Profile: {aws_profile} (SSO)")
|
|
754
|
-
session = boto3.Session(profile_name=aws_profile)
|
|
755
|
-
else:
|
|
756
|
-
# Use static credentials
|
|
757
|
-
creds = config['credentials']
|
|
758
|
-
click.echo("AWS Credentials: Static")
|
|
759
|
-
session = boto3.Session(
|
|
760
|
-
aws_access_key_id=creds['aws_access_key_id'],
|
|
761
|
-
aws_secret_access_key=creds['aws_secret_access_key'],
|
|
762
|
-
aws_session_token=creds.get('aws_session_token')
|
|
763
|
-
)
|
|
764
|
-
|
|
765
|
-
ce_client = session.client('ce', region_name='us-east-1')
|
|
766
|
-
except Exception as e:
|
|
767
|
-
raise click.ClickException(f"Failed to initialize AWS session: {str(e)}")
|
|
768
|
-
|
|
769
|
-
# Get account list
|
|
770
|
-
accounts = config['accounts']
|
|
815
|
+
config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
|
|
771
816
|
|
|
772
817
|
# Show filters
|
|
773
818
|
click.echo(f"Analyzing last {weeks} weeks...")
|
|
@@ -779,13 +824,8 @@ def drill(profile, weeks, service, account, usage_type, output, json_output):
|
|
|
779
824
|
click.echo(f" Usage type filter: {usage_type}")
|
|
780
825
|
click.echo("")
|
|
781
826
|
|
|
782
|
-
#
|
|
783
|
-
drill_data =
|
|
784
|
-
ce_client, accounts, weeks,
|
|
785
|
-
service_filter=service,
|
|
786
|
-
account_filter=account,
|
|
787
|
-
usage_type_filter=usage_type
|
|
788
|
-
)
|
|
827
|
+
# Execute via API or locally
|
|
828
|
+
drill_data = execute_drill(config, weeks, service, account, usage_type)
|
|
789
829
|
|
|
790
830
|
if json_output:
|
|
791
831
|
# Output as JSON
|
|
@@ -844,5 +884,119 @@ def drill(profile, weeks, service, account, usage_type, output, json_output):
|
|
|
844
884
|
click.echo("")
|
|
845
885
|
|
|
846
886
|
|
|
887
|
+
@cli.command()
|
|
888
|
+
@click.option('--profile', required=True, help='Profile name')
|
|
889
|
+
@click.option('--type', 'analysis_type', default='summary',
|
|
890
|
+
type=click.Choice(['summary', 'volatility', 'trends', 'search']),
|
|
891
|
+
help='Analysis type')
|
|
892
|
+
@click.option('--weeks', default=12, help='Number of weeks (default: 12)')
|
|
893
|
+
@click.option('--pattern', help='Service search pattern (for search type)')
|
|
894
|
+
@click.option('--min-cost', type=float, help='Minimum cost filter (for search type)')
|
|
895
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
896
|
+
def analyze(profile, analysis_type, weeks, pattern, min_cost, json_output):
|
|
897
|
+
"""Perform pandas-based analysis (aggregations, volatility, trends, search)"""
|
|
898
|
+
|
|
899
|
+
config = load_profile(profile)
|
|
900
|
+
|
|
901
|
+
if not json_output:
|
|
902
|
+
click.echo(f"Running {analysis_type} analysis for {weeks} weeks...")
|
|
903
|
+
|
|
904
|
+
from cost_calculator.executor import execute_analyze
|
|
905
|
+
result = execute_analyze(config, weeks, analysis_type, pattern, min_cost)
|
|
906
|
+
|
|
907
|
+
if json_output:
|
|
908
|
+
import json
|
|
909
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
910
|
+
else:
|
|
911
|
+
# Format output based on type
|
|
912
|
+
if analysis_type == 'summary':
|
|
913
|
+
click.echo(f"\n📊 Summary ({result.get('total_services', 0)} services)")
|
|
914
|
+
click.echo(f"Weeks analyzed: {result.get('weeks_analyzed', 0)}")
|
|
915
|
+
click.echo(f"\nTop 10 Services (by total change):")
|
|
916
|
+
for svc in result.get('services', [])[:10]:
|
|
917
|
+
click.echo(f" {svc['service']}")
|
|
918
|
+
click.echo(f" Total: ${svc['change_sum']:,.2f}")
|
|
919
|
+
click.echo(f" Average: ${svc['change_mean']:,.2f}")
|
|
920
|
+
click.echo(f" Volatility: {svc['volatility']:.3f}")
|
|
921
|
+
|
|
922
|
+
elif analysis_type == 'volatility':
|
|
923
|
+
click.echo(f"\n📈 High Volatility Services:")
|
|
924
|
+
for svc in result.get('high_volatility_services', [])[:10]:
|
|
925
|
+
click.echo(f" {svc['service']}: CV={svc['coefficient_of_variation']:.3f}")
|
|
926
|
+
|
|
927
|
+
outliers = result.get('outliers', [])
|
|
928
|
+
if outliers:
|
|
929
|
+
click.echo(f"\n⚠️ Outliers ({len(outliers)}):")
|
|
930
|
+
for o in outliers[:5]:
|
|
931
|
+
click.echo(f" {o['service']} ({o['week']}): ${o['change']:,.2f} (z={o['z_score']:.2f})")
|
|
932
|
+
|
|
933
|
+
elif analysis_type == 'trends':
|
|
934
|
+
inc = result.get('increasing_trends', [])
|
|
935
|
+
dec = result.get('decreasing_trends', [])
|
|
936
|
+
|
|
937
|
+
click.echo(f"\n📈 Increasing Trends ({len(inc)}):")
|
|
938
|
+
for t in inc[:5]:
|
|
939
|
+
click.echo(f" {t['service']}: ${t['avg_change']:,.2f}/week")
|
|
940
|
+
|
|
941
|
+
click.echo(f"\n📉 Decreasing Trends ({len(dec)}):")
|
|
942
|
+
for t in dec[:5]:
|
|
943
|
+
click.echo(f" {t['service']}: ${t['avg_change']:,.2f}/week")
|
|
944
|
+
|
|
945
|
+
elif analysis_type == 'search':
|
|
946
|
+
matches = result.get('matches', [])
|
|
947
|
+
click.echo(f"\n🔍 Search Results ({len(matches)} matches)")
|
|
948
|
+
if pattern:
|
|
949
|
+
click.echo(f"Pattern: {pattern}")
|
|
950
|
+
if min_cost:
|
|
951
|
+
click.echo(f"Min cost: ${min_cost:,.2f}")
|
|
952
|
+
|
|
953
|
+
for m in matches[:20]:
|
|
954
|
+
click.echo(f" {m['service']}: ${m['curr_cost']:,.2f}")
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
@cli.command()
|
|
958
|
+
@click.argument('operation', type=click.Choice(['list', 'get', 'create', 'update', 'delete']))
|
|
959
|
+
@click.option('--name', help='Profile name')
|
|
960
|
+
@click.option('--accounts', help='Comma-separated account IDs')
|
|
961
|
+
@click.option('--description', help='Profile description')
|
|
962
|
+
def profile(operation, name, accounts, description):
|
|
963
|
+
"""Manage profiles (CRUD operations)"""
|
|
964
|
+
|
|
965
|
+
from cost_calculator.executor import execute_profile_operation
|
|
966
|
+
|
|
967
|
+
# Parse accounts if provided
|
|
968
|
+
account_list = None
|
|
969
|
+
if accounts:
|
|
970
|
+
account_list = [a.strip() for a in accounts.split(',')]
|
|
971
|
+
|
|
972
|
+
result = execute_profile_operation(
|
|
973
|
+
operation=operation,
|
|
974
|
+
profile_name=name,
|
|
975
|
+
accounts=account_list,
|
|
976
|
+
description=description
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
if operation == 'list':
|
|
980
|
+
profiles = result.get('profiles', [])
|
|
981
|
+
click.echo(f"\n📋 Profiles ({len(profiles)}):")
|
|
982
|
+
for p in profiles:
|
|
983
|
+
click.echo(f" {p['profile_name']}: {len(p.get('accounts', []))} accounts")
|
|
984
|
+
if p.get('description'):
|
|
985
|
+
click.echo(f" {p['description']}")
|
|
986
|
+
|
|
987
|
+
elif operation == 'get':
|
|
988
|
+
profile_data = result.get('profile', {})
|
|
989
|
+
click.echo(f"\n📋 Profile: {profile_data.get('profile_name')}")
|
|
990
|
+
click.echo(f"Accounts: {len(profile_data.get('accounts', []))}")
|
|
991
|
+
if profile_data.get('description'):
|
|
992
|
+
click.echo(f"Description: {profile_data['description']}")
|
|
993
|
+
click.echo(f"\nAccounts:")
|
|
994
|
+
for acc in profile_data.get('accounts', []):
|
|
995
|
+
click.echo(f" {acc}")
|
|
996
|
+
|
|
997
|
+
else:
|
|
998
|
+
click.echo(result.get('message', 'Operation completed'))
|
|
999
|
+
|
|
1000
|
+
|
|
847
1001
|
if __name__ == '__main__':
|
|
848
1002
|
cli()
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Executor that routes to either API or local execution.
|
|
3
|
+
"""
|
|
4
|
+
import boto3
|
|
5
|
+
import click
|
|
6
|
+
from cost_calculator.api_client import is_api_configured, call_lambda_api
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_credentials_dict(config):
|
|
10
|
+
"""
|
|
11
|
+
Extract credentials from config in format needed for API.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
dict with access_key, secret_key, session_token, or None if profile is 'dummy'
|
|
15
|
+
"""
|
|
16
|
+
if 'aws_profile' in config:
|
|
17
|
+
# Skip credential loading for dummy profile (API-only mode)
|
|
18
|
+
if config['aws_profile'] == 'dummy':
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
# Get temporary credentials from SSO session
|
|
22
|
+
try:
|
|
23
|
+
session = boto3.Session(profile_name=config['aws_profile'])
|
|
24
|
+
credentials = session.get_credentials()
|
|
25
|
+
frozen_creds = credentials.get_frozen_credentials()
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
'access_key': frozen_creds.access_key,
|
|
29
|
+
'secret_key': frozen_creds.secret_key,
|
|
30
|
+
'session_token': frozen_creds.token
|
|
31
|
+
}
|
|
32
|
+
except Exception:
|
|
33
|
+
# If profile not found, return None (API will handle)
|
|
34
|
+
return None
|
|
35
|
+
else:
|
|
36
|
+
# Use static credentials
|
|
37
|
+
creds = config.get('credentials', {})
|
|
38
|
+
if not creds:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
result = {
|
|
42
|
+
'access_key': creds['aws_access_key_id'],
|
|
43
|
+
'secret_key': creds['aws_secret_access_key']
|
|
44
|
+
}
|
|
45
|
+
if 'aws_session_token' in creds:
|
|
46
|
+
result['session_token'] = creds['aws_session_token']
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def execute_trends(config, weeks):
|
|
51
|
+
"""
|
|
52
|
+
Execute trends analysis via API or locally.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
dict: trends data
|
|
56
|
+
"""
|
|
57
|
+
accounts = config['accounts']
|
|
58
|
+
|
|
59
|
+
if is_api_configured():
|
|
60
|
+
# Use API
|
|
61
|
+
click.echo("Using Lambda API...")
|
|
62
|
+
credentials = get_credentials_dict(config)
|
|
63
|
+
return call_lambda_api('trends', credentials, accounts, weeks=weeks)
|
|
64
|
+
else:
|
|
65
|
+
# Use local execution
|
|
66
|
+
click.echo("Using local execution...")
|
|
67
|
+
from cost_calculator.trends import analyze_trends
|
|
68
|
+
|
|
69
|
+
# Initialize boto3 client
|
|
70
|
+
if 'aws_profile' in config:
|
|
71
|
+
session = boto3.Session(profile_name=config['aws_profile'])
|
|
72
|
+
else:
|
|
73
|
+
creds = config['credentials']
|
|
74
|
+
session_kwargs = {
|
|
75
|
+
'aws_access_key_id': creds['aws_access_key_id'],
|
|
76
|
+
'aws_secret_access_key': creds['aws_secret_access_key'],
|
|
77
|
+
'region_name': creds.get('region', 'us-east-1')
|
|
78
|
+
}
|
|
79
|
+
if 'aws_session_token' in creds:
|
|
80
|
+
session_kwargs['aws_session_token'] = creds['aws_session_token']
|
|
81
|
+
session = boto3.Session(**session_kwargs)
|
|
82
|
+
|
|
83
|
+
ce_client = session.client('ce', region_name='us-east-1')
|
|
84
|
+
return analyze_trends(ce_client, accounts, weeks)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def execute_monthly(config, months):
|
|
88
|
+
"""
|
|
89
|
+
Execute monthly analysis via API or locally.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
dict: monthly data
|
|
93
|
+
"""
|
|
94
|
+
accounts = config['accounts']
|
|
95
|
+
|
|
96
|
+
if is_api_configured():
|
|
97
|
+
# Use API
|
|
98
|
+
click.echo("Using Lambda API...")
|
|
99
|
+
credentials = get_credentials_dict(config)
|
|
100
|
+
return call_lambda_api('monthly', credentials, accounts, months=months)
|
|
101
|
+
else:
|
|
102
|
+
# Use local execution
|
|
103
|
+
click.echo("Using local execution...")
|
|
104
|
+
from cost_calculator.monthly import analyze_monthly_trends
|
|
105
|
+
|
|
106
|
+
# Initialize boto3 client
|
|
107
|
+
if 'aws_profile' in config:
|
|
108
|
+
session = boto3.Session(profile_name=config['aws_profile'])
|
|
109
|
+
else:
|
|
110
|
+
creds = config['credentials']
|
|
111
|
+
session_kwargs = {
|
|
112
|
+
'aws_access_key_id': creds['aws_access_key_id'],
|
|
113
|
+
'aws_secret_access_key': creds['aws_secret_access_key'],
|
|
114
|
+
'region_name': creds.get('region', 'us-east-1')
|
|
115
|
+
}
|
|
116
|
+
if 'aws_session_token' in creds:
|
|
117
|
+
session_kwargs['aws_session_token'] = creds['aws_session_token']
|
|
118
|
+
session = boto3.Session(**session_kwargs)
|
|
119
|
+
|
|
120
|
+
ce_client = session.client('ce', region_name='us-east-1')
|
|
121
|
+
return analyze_monthly_trends(ce_client, accounts, months)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def execute_drill(config, weeks, service_filter=None, account_filter=None, usage_type_filter=None):
|
|
125
|
+
"""
|
|
126
|
+
Execute drill-down analysis via API or locally.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
dict: drill data
|
|
130
|
+
"""
|
|
131
|
+
accounts = config['accounts']
|
|
132
|
+
|
|
133
|
+
if is_api_configured():
|
|
134
|
+
# Use API
|
|
135
|
+
click.echo("Using Lambda API...")
|
|
136
|
+
credentials = get_credentials_dict(config)
|
|
137
|
+
kwargs = {'weeks': weeks}
|
|
138
|
+
if service_filter:
|
|
139
|
+
kwargs['service'] = service_filter
|
|
140
|
+
if account_filter:
|
|
141
|
+
kwargs['account'] = account_filter
|
|
142
|
+
if usage_type_filter:
|
|
143
|
+
kwargs['usage_type'] = usage_type_filter
|
|
144
|
+
return call_lambda_api('drill', credentials, accounts, **kwargs)
|
|
145
|
+
else:
|
|
146
|
+
# Use local execution
|
|
147
|
+
click.echo("Using local execution...")
|
|
148
|
+
from cost_calculator.drill import analyze_drill_down
|
|
149
|
+
|
|
150
|
+
# Initialize boto3 client
|
|
151
|
+
if 'aws_profile' in config:
|
|
152
|
+
session = boto3.Session(profile_name=config['aws_profile'])
|
|
153
|
+
else:
|
|
154
|
+
creds = config['credentials']
|
|
155
|
+
session_kwargs = {
|
|
156
|
+
'aws_access_key_id': creds['aws_access_key_id'],
|
|
157
|
+
'aws_secret_access_key': creds['aws_secret_access_key'],
|
|
158
|
+
'region_name': creds.get('region', 'us-east-1')
|
|
159
|
+
}
|
|
160
|
+
if 'aws_session_token' in creds:
|
|
161
|
+
session_kwargs['aws_session_token'] = creds['aws_session_token']
|
|
162
|
+
session = boto3.Session(**session_kwargs)
|
|
163
|
+
|
|
164
|
+
ce_client = session.client('ce', region_name='us-east-1')
|
|
165
|
+
return analyze_drill_down(
|
|
166
|
+
ce_client, accounts, weeks,
|
|
167
|
+
service_filter=service_filter,
|
|
168
|
+
account_filter=account_filter,
|
|
169
|
+
usage_type_filter=usage_type_filter
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def execute_analyze(config, weeks, analysis_type, pattern=None, min_cost=None):
|
|
174
|
+
"""
|
|
175
|
+
Execute pandas-based analysis via API.
|
|
176
|
+
Note: This only works via API (requires pandas layer).
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
dict: analysis results
|
|
180
|
+
"""
|
|
181
|
+
accounts = config['accounts']
|
|
182
|
+
|
|
183
|
+
if not is_api_configured():
|
|
184
|
+
raise click.ClickException(
|
|
185
|
+
"Analyze command requires API configuration.\n"
|
|
186
|
+
"Set COST_API_SECRET environment variable."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
credentials = get_credentials_dict(config)
|
|
190
|
+
kwargs = {'weeks': weeks, 'type': analysis_type}
|
|
191
|
+
|
|
192
|
+
if pattern:
|
|
193
|
+
kwargs['pattern'] = pattern
|
|
194
|
+
if min_cost:
|
|
195
|
+
kwargs['min_cost'] = min_cost
|
|
196
|
+
|
|
197
|
+
return call_lambda_api('analyze', credentials, accounts, **kwargs)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def execute_profile_operation(operation, profile_name=None, accounts=None, description=None):
|
|
201
|
+
"""
|
|
202
|
+
Execute profile CRUD operations via API.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
dict: operation result
|
|
206
|
+
"""
|
|
207
|
+
if not is_api_configured():
|
|
208
|
+
raise click.ClickException(
|
|
209
|
+
"Profile commands require API configuration.\n"
|
|
210
|
+
"Set COST_API_SECRET environment variable."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Profile operations don't need AWS credentials, just API secret
|
|
214
|
+
import os
|
|
215
|
+
import requests
|
|
216
|
+
import json
|
|
217
|
+
|
|
218
|
+
api_secret = os.environ.get('COST_API_SECRET', '')
|
|
219
|
+
|
|
220
|
+
# Use profiles endpoint (hardcoded URL)
|
|
221
|
+
url = 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/'
|
|
222
|
+
|
|
223
|
+
payload = {'operation': operation}
|
|
224
|
+
if profile_name:
|
|
225
|
+
payload['profile_name'] = profile_name
|
|
226
|
+
if accounts:
|
|
227
|
+
payload['accounts'] = accounts
|
|
228
|
+
if description:
|
|
229
|
+
payload['description'] = description
|
|
230
|
+
|
|
231
|
+
headers = {
|
|
232
|
+
'X-API-Secret': api_secret,
|
|
233
|
+
'Content-Type': 'application/json'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
response = requests.post(url, headers=headers, json=payload, timeout=60)
|
|
237
|
+
|
|
238
|
+
if response.status_code != 200:
|
|
239
|
+
raise Exception(f"API call failed: {response.status_code} - {response.text}")
|
|
240
|
+
|
|
241
|
+
return response.json()
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
aws_cost_calculator_cli-1.3.0.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
|
|
2
|
-
cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
|
|
3
|
-
cost_calculator/cli.py,sha256=YJOWA-uWSMEZc4QHof8DF_An6VpKmTsxh0X_G31y1SM,31018
|
|
4
|
-
cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
|
|
5
|
-
cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
|
|
6
|
-
cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
|
|
7
|
-
aws_cost_calculator_cli-1.3.0.dist-info/METADATA,sha256=jZYbRF46p0qvjkw4Dqwja-4cnOJv59i7zTzmFXxfCsw,7761
|
|
8
|
-
aws_cost_calculator_cli-1.3.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
9
|
-
aws_cost_calculator_cli-1.3.0.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
10
|
-
aws_cost_calculator_cli-1.3.0.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
|
|
11
|
-
aws_cost_calculator_cli-1.3.0.dist-info/RECORD,,
|
{aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.6.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.6.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.6.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|