aws-cost-calculator-cli 1.0.3__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.
- aws_cost_calculator_cli-1.0.3.dist-info/METADATA +164 -0
- aws_cost_calculator_cli-1.0.3.dist-info/RECORD +8 -0
- aws_cost_calculator_cli-1.0.3.dist-info/WHEEL +5 -0
- aws_cost_calculator_cli-1.0.3.dist-info/entry_points.txt +2 -0
- aws_cost_calculator_cli-1.0.3.dist-info/licenses/LICENSE +21 -0
- aws_cost_calculator_cli-1.0.3.dist-info/top_level.txt +1 -0
- cost_calculator/__init__.py +2 -0
- cost_calculator/cli.py +543 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aws-cost-calculator-cli
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: AWS Cost Calculator CLI - Calculate daily and annual AWS costs across multiple accounts
|
|
5
|
+
Home-page: https://github.com/yourusername/cost-calculator
|
|
6
|
+
Author: Cost Optimization Team
|
|
7
|
+
Author-email:
|
|
8
|
+
Project-URL: Documentation, https://github.com/yourusername/cost-calculator/blob/main/README.md
|
|
9
|
+
Keywords: aws cost calculator billing optimization cloud
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: System Administrators
|
|
13
|
+
Classifier: Topic :: System :: Monitoring
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: click>=8.0.0
|
|
25
|
+
Requires-Dist: boto3>=1.26.0
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: classifier
|
|
28
|
+
Dynamic: description
|
|
29
|
+
Dynamic: description-content-type
|
|
30
|
+
Dynamic: home-page
|
|
31
|
+
Dynamic: keywords
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
Dynamic: project-url
|
|
34
|
+
Dynamic: requires-dist
|
|
35
|
+
Dynamic: requires-python
|
|
36
|
+
Dynamic: summary
|
|
37
|
+
|
|
38
|
+
# AWS Cost Calculator (cc)
|
|
39
|
+
|
|
40
|
+
A CLI tool to quickly calculate AWS costs across multiple accounts.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cd ~/cost-calculator
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
### 1. Login to AWS SSO
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
aws sso login --profile my_aws_profile
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Note:** You need to do this before running cost calculations. The SSO session typically lasts 8-12 hours.
|
|
58
|
+
|
|
59
|
+
### 2. Initialize a profile
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cc init --profile myprofile \
|
|
63
|
+
--aws-profile my_aws_profile \
|
|
64
|
+
--accounts "123456789012,234567890123,345678901234"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. Calculate costs
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Default: Today minus 2 days, going back 30 days
|
|
71
|
+
cc calculate --profile myprofile
|
|
72
|
+
|
|
73
|
+
# Specific start date
|
|
74
|
+
cc calculate --profile myprofile --start-date 2025-11-04
|
|
75
|
+
|
|
76
|
+
# Custom offset and window
|
|
77
|
+
cc calculate --profile myprofile --offset 2 --window 30
|
|
78
|
+
|
|
79
|
+
# JSON output
|
|
80
|
+
cc calculate --profile myprofile --json-output
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 4. List profiles
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cc list-profiles
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## How It Works
|
|
90
|
+
|
|
91
|
+
### Date Calculation
|
|
92
|
+
- **Start Date**: Defaults to today, or specify with `--start-date`
|
|
93
|
+
- **Offset**: Days to go back from start date (default: 2)
|
|
94
|
+
- **Window**: Number of days to analyze (default: 30)
|
|
95
|
+
|
|
96
|
+
Example: If today is Nov 4, 2025:
|
|
97
|
+
- With offset=2, window=30: Analyzes Oct 3 - Nov 2 (30 days)
|
|
98
|
+
|
|
99
|
+
### Cost Calculation
|
|
100
|
+
1. **Operational Costs**: Sum of daily costs ÷ window days
|
|
101
|
+
2. **Support Allocation**:
|
|
102
|
+
- Gets support cost from the analysis month
|
|
103
|
+
- Divides by 2 (50% allocation)
|
|
104
|
+
- Divides by days in that month
|
|
105
|
+
3. **Daily Rate**: Operational + Support per day
|
|
106
|
+
4. **Annual Projection**: Daily rate × 365
|
|
107
|
+
|
|
108
|
+
### Filters Applied
|
|
109
|
+
- **Billing Entity**: AWS only (excludes marketplace)
|
|
110
|
+
- **Excluded**: Tax, Support (calculated separately)
|
|
111
|
+
- **Metric**: Net Amortized Cost
|
|
112
|
+
|
|
113
|
+
## Configuration
|
|
114
|
+
|
|
115
|
+
Profiles are stored in: `~/.config/cost-calculator/profiles.json`
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"myprofile": {
|
|
121
|
+
"aws_profile": "my_aws_profile",
|
|
122
|
+
"accounts": ["123456789012", "234567890123", "345678901234"]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Examples
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Quick daily check
|
|
131
|
+
cc calculate --profile myprofile
|
|
132
|
+
|
|
133
|
+
# Historical analysis
|
|
134
|
+
cc calculate --profile myprofile --start-date 2025-10-01
|
|
135
|
+
|
|
136
|
+
# Export to JSON for processing
|
|
137
|
+
cc calculate --profile myprofile --json-output > costs.json
|
|
138
|
+
|
|
139
|
+
# Different window size
|
|
140
|
+
cc calculate --profile myprofile --window 60
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Output
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
Analyzing: 2025-10-03 to 2025-11-02 (30 days)
|
|
147
|
+
AWS Profile: my_aws_profile
|
|
148
|
+
Accounts: 3
|
|
149
|
+
|
|
150
|
+
Fetching cost data...
|
|
151
|
+
Fetching support costs...
|
|
152
|
+
============================================================
|
|
153
|
+
Period: 2025-10-03 to 2025-11-02
|
|
154
|
+
Days analyzed: 30
|
|
155
|
+
============================================================
|
|
156
|
+
Total operational cost: $450,000.00
|
|
157
|
+
Daily operational: $14,516.13
|
|
158
|
+
Support (month): $15,000.00
|
|
159
|
+
Support per day (÷2÷days): $241.94
|
|
160
|
+
============================================================
|
|
161
|
+
DAILY RATE: $14,758.07
|
|
162
|
+
ANNUAL PROJECTION: $5,386,695
|
|
163
|
+
============================================================
|
|
164
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
aws_cost_calculator_cli-1.0.3.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
|
|
2
|
+
cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
|
|
3
|
+
cost_calculator/cli.py,sha256=2h6jJreraM5ysKvy6vREvHJw7_3UjDegOpNTRscLnwE,18971
|
|
4
|
+
aws_cost_calculator_cli-1.0.3.dist-info/METADATA,sha256=p2NSEbaQPwt9Q5F1WjAZTFFNBoLp2IjCK1aA0GmxJHQ,4227
|
|
5
|
+
aws_cost_calculator_cli-1.0.3.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
6
|
+
aws_cost_calculator_cli-1.0.3.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
7
|
+
aws_cost_calculator_cli-1.0.3.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
|
|
8
|
+
aws_cost_calculator_cli-1.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Cost Optimization Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cost_calculator
|
cost_calculator/cli.py
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
AWS Cost Calculator CLI
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
cc --profile myprofile
|
|
7
|
+
cc --profile myprofile --start-date 2025-11-04
|
|
8
|
+
cc --profile myprofile --offset 2 --window 30
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import boto3
|
|
13
|
+
import json
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_profile(profile_name):
|
|
19
|
+
"""Load profile configuration from ~/.config/cost-calculator/profiles.json"""
|
|
20
|
+
config_dir = Path.home() / '.config' / 'cost-calculator'
|
|
21
|
+
config_file = config_dir / 'profiles.json'
|
|
22
|
+
creds_file = config_dir / 'credentials.json'
|
|
23
|
+
|
|
24
|
+
if not config_file.exists():
|
|
25
|
+
raise click.ClickException(
|
|
26
|
+
f"Profile configuration not found at {config_file}\n"
|
|
27
|
+
f"Run: cc init --profile {profile_name}"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
with open(config_file) as f:
|
|
31
|
+
profiles = json.load(f)
|
|
32
|
+
|
|
33
|
+
if profile_name not in profiles:
|
|
34
|
+
raise click.ClickException(
|
|
35
|
+
f"Profile '{profile_name}' not found in {config_file}\n"
|
|
36
|
+
f"Available profiles: {', '.join(profiles.keys())}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
profile = profiles[profile_name]
|
|
40
|
+
|
|
41
|
+
# Load credentials if using static credentials (not SSO)
|
|
42
|
+
if 'aws_profile' not in profile:
|
|
43
|
+
if not creds_file.exists():
|
|
44
|
+
raise click.ClickException(
|
|
45
|
+
f"No credentials found for profile '{profile_name}'.\n"
|
|
46
|
+
f"Run: cc configure --profile {profile_name}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
with open(creds_file) as f:
|
|
50
|
+
creds = json.load(f)
|
|
51
|
+
|
|
52
|
+
if profile_name not in creds:
|
|
53
|
+
raise click.ClickException(
|
|
54
|
+
f"No credentials found for profile '{profile_name}'.\n"
|
|
55
|
+
f"Run: cc configure --profile {profile_name}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
profile['credentials'] = creds[profile_name]
|
|
59
|
+
|
|
60
|
+
return profile
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def calculate_costs(profile_config, accounts, start_date, offset, window):
|
|
64
|
+
"""
|
|
65
|
+
Calculate AWS costs for the specified period.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
profile_config: Profile configuration (with aws_profile or credentials)
|
|
69
|
+
accounts: List of AWS account IDs
|
|
70
|
+
start_date: Start date (defaults to today)
|
|
71
|
+
offset: Days to go back from start_date (default: 2)
|
|
72
|
+
window: Number of days to analyze (default: 30)
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
dict with cost breakdown
|
|
76
|
+
"""
|
|
77
|
+
# Calculate date range
|
|
78
|
+
if start_date:
|
|
79
|
+
end_date = datetime.strptime(start_date, '%Y-%m-%d')
|
|
80
|
+
else:
|
|
81
|
+
end_date = datetime.now()
|
|
82
|
+
|
|
83
|
+
# Go back by offset days
|
|
84
|
+
end_date = end_date - timedelta(days=offset)
|
|
85
|
+
|
|
86
|
+
# Start date is window days before end_date
|
|
87
|
+
start_date_calc = end_date - timedelta(days=window)
|
|
88
|
+
|
|
89
|
+
# Format for API (end date is exclusive, so add 1 day)
|
|
90
|
+
api_start = start_date_calc.strftime('%Y-%m-%d')
|
|
91
|
+
api_end = (end_date + timedelta(days=1)).strftime('%Y-%m-%d')
|
|
92
|
+
|
|
93
|
+
click.echo(f"Analyzing: {api_start} to {end_date.strftime('%Y-%m-%d')} ({window} days)")
|
|
94
|
+
|
|
95
|
+
# Initialize boto3 client
|
|
96
|
+
try:
|
|
97
|
+
if 'aws_profile' in profile_config:
|
|
98
|
+
# SSO-based authentication
|
|
99
|
+
aws_profile = profile_config['aws_profile']
|
|
100
|
+
click.echo(f"AWS Profile: {aws_profile} (SSO)")
|
|
101
|
+
click.echo(f"Accounts: {len(accounts)}")
|
|
102
|
+
click.echo("")
|
|
103
|
+
session = boto3.Session(profile_name=aws_profile)
|
|
104
|
+
ce_client = session.client('ce', region_name='us-east-1')
|
|
105
|
+
else:
|
|
106
|
+
# Static credentials
|
|
107
|
+
creds = profile_config['credentials']
|
|
108
|
+
click.echo(f"AWS Credentials: Static")
|
|
109
|
+
click.echo(f"Accounts: {len(accounts)}")
|
|
110
|
+
click.echo("")
|
|
111
|
+
|
|
112
|
+
session_kwargs = {
|
|
113
|
+
'aws_access_key_id': creds['aws_access_key_id'],
|
|
114
|
+
'aws_secret_access_key': creds['aws_secret_access_key'],
|
|
115
|
+
'region_name': creds.get('region', 'us-east-1')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if 'aws_session_token' in creds:
|
|
119
|
+
session_kwargs['aws_session_token'] = creds['aws_session_token']
|
|
120
|
+
|
|
121
|
+
session = boto3.Session(**session_kwargs)
|
|
122
|
+
ce_client = session.client('ce')
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
if 'Token has expired' in str(e) or 'sso' in str(e).lower():
|
|
126
|
+
if 'aws_profile' in profile_config:
|
|
127
|
+
raise click.ClickException(
|
|
128
|
+
f"AWS SSO session expired or not initialized.\n"
|
|
129
|
+
f"Run: aws sso login --profile {profile_config['aws_profile']}"
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
raise click.ClickException(
|
|
133
|
+
f"AWS credentials expired.\n"
|
|
134
|
+
f"Run: cc configure --profile <profile_name>"
|
|
135
|
+
)
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
# Build filter
|
|
139
|
+
cost_filter = {
|
|
140
|
+
"And": [
|
|
141
|
+
{
|
|
142
|
+
"Dimensions": {
|
|
143
|
+
"Key": "LINKED_ACCOUNT",
|
|
144
|
+
"Values": accounts
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"Dimensions": {
|
|
149
|
+
"Key": "BILLING_ENTITY",
|
|
150
|
+
"Values": ["AWS"]
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"Not": {
|
|
155
|
+
"Dimensions": {
|
|
156
|
+
"Key": "RECORD_TYPE",
|
|
157
|
+
"Values": ["Tax", "Support"]
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Get daily costs
|
|
165
|
+
click.echo("Fetching cost data...")
|
|
166
|
+
try:
|
|
167
|
+
response = ce_client.get_cost_and_usage(
|
|
168
|
+
TimePeriod={
|
|
169
|
+
'Start': api_start,
|
|
170
|
+
'End': api_end
|
|
171
|
+
},
|
|
172
|
+
Granularity='DAILY',
|
|
173
|
+
Metrics=['NetAmortizedCost'],
|
|
174
|
+
Filter=cost_filter
|
|
175
|
+
)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
if 'Token has expired' in str(e) or 'expired' in str(e).lower():
|
|
178
|
+
raise click.ClickException(
|
|
179
|
+
f"AWS SSO session expired.\n"
|
|
180
|
+
f"Run: aws sso login --profile {aws_profile}"
|
|
181
|
+
)
|
|
182
|
+
raise
|
|
183
|
+
|
|
184
|
+
# Calculate total
|
|
185
|
+
total_cost = sum(
|
|
186
|
+
float(day['Total']['NetAmortizedCost']['Amount'])
|
|
187
|
+
for day in response['ResultsByTime']
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Get support cost from the 1st of the month containing the end date
|
|
191
|
+
# Support is charged on the 1st of each month for the previous month's usage
|
|
192
|
+
# For Oct 3-Nov 2 analysis, we get support from Nov 1 (which is October's support)
|
|
193
|
+
support_month_date = end_date.replace(day=1)
|
|
194
|
+
support_date_str = support_month_date.strftime('%Y-%m-%d')
|
|
195
|
+
support_date_end = (support_month_date + timedelta(days=1)).strftime('%Y-%m-%d')
|
|
196
|
+
|
|
197
|
+
click.echo("Fetching support costs...")
|
|
198
|
+
support_response = ce_client.get_cost_and_usage(
|
|
199
|
+
TimePeriod={
|
|
200
|
+
'Start': support_date_str,
|
|
201
|
+
'End': support_date_end
|
|
202
|
+
},
|
|
203
|
+
Granularity='DAILY',
|
|
204
|
+
Metrics=['NetAmortizedCost'],
|
|
205
|
+
Filter={
|
|
206
|
+
"And": [
|
|
207
|
+
{
|
|
208
|
+
"Dimensions": {
|
|
209
|
+
"Key": "LINKED_ACCOUNT",
|
|
210
|
+
"Values": accounts
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"Dimensions": {
|
|
215
|
+
"Key": "RECORD_TYPE",
|
|
216
|
+
"Values": ["Support"]
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
support_cost = float(support_response['ResultsByTime'][0]['Total']['NetAmortizedCost']['Amount'])
|
|
224
|
+
|
|
225
|
+
# Calculate days in the month that the support covers
|
|
226
|
+
# Support on Nov 1 covers October (31 days)
|
|
227
|
+
support_month = support_month_date - timedelta(days=1) # Go back to previous month
|
|
228
|
+
days_in_support_month = support_month.day # This gives us the last day of the month
|
|
229
|
+
|
|
230
|
+
# Support allocation: divide by 2 (half to Khoros), then by days in month
|
|
231
|
+
support_per_day = (support_cost / 2) / days_in_support_month
|
|
232
|
+
|
|
233
|
+
# Calculate daily rate
|
|
234
|
+
# NOTE: We divide operational by window, but support by days_in_support_month
|
|
235
|
+
# This matches the console's calculation method
|
|
236
|
+
daily_operational = total_cost / days_in_support_month # Use 31 for October, not 30
|
|
237
|
+
daily_total = daily_operational + support_per_day
|
|
238
|
+
|
|
239
|
+
# Annual projection
|
|
240
|
+
annual = daily_total * 365
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
'period': {
|
|
244
|
+
'start': api_start,
|
|
245
|
+
'end': end_date.strftime('%Y-%m-%d'),
|
|
246
|
+
'days': window
|
|
247
|
+
},
|
|
248
|
+
'costs': {
|
|
249
|
+
'total_operational': total_cost,
|
|
250
|
+
'daily_operational': daily_operational,
|
|
251
|
+
'support_month': support_cost,
|
|
252
|
+
'support_per_day': support_per_day,
|
|
253
|
+
'daily_total': daily_total,
|
|
254
|
+
'annual_projection': annual
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@click.group()
|
|
260
|
+
def cli():
|
|
261
|
+
"""
|
|
262
|
+
AWS Cost Calculator - Calculate daily and annual AWS costs
|
|
263
|
+
|
|
264
|
+
\b
|
|
265
|
+
Two authentication methods:
|
|
266
|
+
1. AWS SSO (recommended for interactive use)
|
|
267
|
+
2. Static credentials (for automation/CI)
|
|
268
|
+
|
|
269
|
+
\b
|
|
270
|
+
Quick Start:
|
|
271
|
+
# SSO Method
|
|
272
|
+
aws sso login --profile my_aws_profile
|
|
273
|
+
cc init --profile myprofile --aws-profile my_aws_profile --accounts "123,456,789"
|
|
274
|
+
cc calculate --profile myprofile
|
|
275
|
+
|
|
276
|
+
# Static Credentials Method
|
|
277
|
+
cc init --profile myprofile --aws-profile dummy --accounts "123,456,789"
|
|
278
|
+
cc configure --profile myprofile
|
|
279
|
+
cc calculate --profile myprofile
|
|
280
|
+
|
|
281
|
+
\b
|
|
282
|
+
For detailed documentation, see:
|
|
283
|
+
- COST_CALCULATION_METHODOLOGY.md
|
|
284
|
+
- README.md
|
|
285
|
+
"""
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@cli.command()
|
|
290
|
+
@click.option('--profile', required=True, help='Profile name (e.g., myprofile)')
|
|
291
|
+
@click.option('--start-date', help='Start date (YYYY-MM-DD, default: today)')
|
|
292
|
+
@click.option('--offset', default=2, help='Days to go back from start date (default: 2)')
|
|
293
|
+
@click.option('--window', default=30, help='Number of days to analyze (default: 30)')
|
|
294
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
295
|
+
def calculate(profile, start_date, offset, window, json_output):
|
|
296
|
+
"""Calculate AWS costs for the specified period"""
|
|
297
|
+
|
|
298
|
+
# Load profile configuration
|
|
299
|
+
config = load_profile(profile)
|
|
300
|
+
|
|
301
|
+
# Calculate costs
|
|
302
|
+
result = calculate_costs(
|
|
303
|
+
profile_config=config,
|
|
304
|
+
accounts=config['accounts'],
|
|
305
|
+
start_date=start_date,
|
|
306
|
+
offset=offset,
|
|
307
|
+
window=window
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if json_output:
|
|
311
|
+
click.echo(json.dumps(result, indent=2))
|
|
312
|
+
else:
|
|
313
|
+
# Pretty print results
|
|
314
|
+
click.echo("=" * 60)
|
|
315
|
+
click.echo(f"Period: {result['period']['start']} to {result['period']['end']}")
|
|
316
|
+
click.echo(f"Days analyzed: {result['period']['days']}")
|
|
317
|
+
click.echo("=" * 60)
|
|
318
|
+
click.echo(f"Total operational cost: ${result['costs']['total_operational']:,.2f}")
|
|
319
|
+
click.echo(f"Daily operational: ${result['costs']['daily_operational']:,.2f}")
|
|
320
|
+
click.echo(f"Support (month): ${result['costs']['support_month']:,.2f}")
|
|
321
|
+
click.echo(f"Support per day (÷2÷days): ${result['costs']['support_per_day']:,.2f}")
|
|
322
|
+
click.echo("=" * 60)
|
|
323
|
+
click.echo(f"DAILY RATE: ${result['costs']['daily_total']:,.2f}")
|
|
324
|
+
click.echo(f"ANNUAL PROJECTION: ${result['costs']['annual_projection']:,.0f}")
|
|
325
|
+
click.echo("=" * 60)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@cli.command()
|
|
329
|
+
@click.option('--profile', required=True, help='Profile name to create')
|
|
330
|
+
@click.option('--aws-profile', required=True, help='AWS CLI profile name')
|
|
331
|
+
@click.option('--accounts', required=True, help='Comma-separated list of account IDs')
|
|
332
|
+
def init(profile, aws_profile, accounts):
|
|
333
|
+
"""Initialize a new profile configuration"""
|
|
334
|
+
|
|
335
|
+
config_dir = Path.home() / '.config' / 'cost-calculator'
|
|
336
|
+
config_file = config_dir / 'profiles.json'
|
|
337
|
+
|
|
338
|
+
# Create config directory if it doesn't exist
|
|
339
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
340
|
+
|
|
341
|
+
# Load existing profiles or create new
|
|
342
|
+
if config_file.exists() and config_file.stat().st_size > 0:
|
|
343
|
+
try:
|
|
344
|
+
with open(config_file) as f:
|
|
345
|
+
profiles = json.load(f)
|
|
346
|
+
except json.JSONDecodeError:
|
|
347
|
+
profiles = {}
|
|
348
|
+
else:
|
|
349
|
+
profiles = {}
|
|
350
|
+
|
|
351
|
+
# Parse accounts
|
|
352
|
+
account_list = [acc.strip() for acc in accounts.split(',')]
|
|
353
|
+
|
|
354
|
+
# Add new profile
|
|
355
|
+
profiles[profile] = {
|
|
356
|
+
'aws_profile': aws_profile,
|
|
357
|
+
'accounts': account_list
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# Save
|
|
361
|
+
with open(config_file, 'w') as f:
|
|
362
|
+
json.dump(profiles, f, indent=2)
|
|
363
|
+
|
|
364
|
+
click.echo(f"✓ Profile '{profile}' created with {len(account_list)} accounts")
|
|
365
|
+
click.echo(f"✓ Configuration saved to {config_file}")
|
|
366
|
+
click.echo(f"\nUsage: cc calculate --profile {profile}")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@cli.command()
|
|
370
|
+
def list_profiles():
|
|
371
|
+
"""List all configured profiles"""
|
|
372
|
+
|
|
373
|
+
config_file = Path.home() / '.config' / 'cost-calculator' / 'profiles.json'
|
|
374
|
+
|
|
375
|
+
if not config_file.exists():
|
|
376
|
+
click.echo("No profiles configured. Run: cc init --profile <name>")
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
with open(config_file) as f:
|
|
380
|
+
profiles = json.load(f)
|
|
381
|
+
|
|
382
|
+
if not profiles:
|
|
383
|
+
click.echo("No profiles configured.")
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
click.echo("Configured profiles:")
|
|
387
|
+
click.echo("")
|
|
388
|
+
for name, config in profiles.items():
|
|
389
|
+
click.echo(f" {name}")
|
|
390
|
+
if 'aws_profile' in config:
|
|
391
|
+
click.echo(f" AWS Profile: {config['aws_profile']} (SSO)")
|
|
392
|
+
else:
|
|
393
|
+
click.echo(f" AWS Credentials: Configured (Static)")
|
|
394
|
+
click.echo(f" Accounts: {len(config['accounts'])}")
|
|
395
|
+
click.echo("")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@cli.command()
|
|
399
|
+
def setup():
|
|
400
|
+
"""Show setup instructions for manual profile configuration"""
|
|
401
|
+
import platform
|
|
402
|
+
|
|
403
|
+
system = platform.system()
|
|
404
|
+
|
|
405
|
+
if system == "Windows":
|
|
406
|
+
config_path = "%USERPROFILE%\\.config\\cost-calculator\\profiles.json"
|
|
407
|
+
config_path_example = "C:\\Users\\YourName\\.config\\cost-calculator\\profiles.json"
|
|
408
|
+
mkdir_cmd = "mkdir %USERPROFILE%\\.config\\cost-calculator"
|
|
409
|
+
edit_cmd = "notepad %USERPROFILE%\\.config\\cost-calculator\\profiles.json"
|
|
410
|
+
else: # macOS/Linux
|
|
411
|
+
config_path = "~/.config/cost-calculator/profiles.json"
|
|
412
|
+
config_path_example = "/Users/yourname/.config/cost-calculator/profiles.json"
|
|
413
|
+
mkdir_cmd = "mkdir -p ~/.config/cost-calculator"
|
|
414
|
+
edit_cmd = "nano ~/.config/cost-calculator/profiles.json"
|
|
415
|
+
|
|
416
|
+
click.echo("=" * 70)
|
|
417
|
+
click.echo("AWS Cost Calculator - Manual Profile Setup")
|
|
418
|
+
click.echo("=" * 70)
|
|
419
|
+
click.echo("")
|
|
420
|
+
click.echo(f"Platform: {system}")
|
|
421
|
+
click.echo(f"Config location: {config_path}")
|
|
422
|
+
click.echo("")
|
|
423
|
+
click.echo("Step 1: Create the config directory")
|
|
424
|
+
click.echo(f" {mkdir_cmd}")
|
|
425
|
+
click.echo("")
|
|
426
|
+
click.echo("Step 2: Create the profiles.json file")
|
|
427
|
+
click.echo(f" {edit_cmd}")
|
|
428
|
+
click.echo("")
|
|
429
|
+
click.echo("Step 3: Add your profile configuration (JSON format):")
|
|
430
|
+
click.echo("")
|
|
431
|
+
click.echo(' {')
|
|
432
|
+
click.echo(' "myprofile": {')
|
|
433
|
+
click.echo(' "aws_profile": "my_aws_profile",')
|
|
434
|
+
click.echo(' "accounts": [')
|
|
435
|
+
click.echo(' "123456789012",')
|
|
436
|
+
click.echo(' "234567890123",')
|
|
437
|
+
click.echo(' "345678901234"')
|
|
438
|
+
click.echo(' ]')
|
|
439
|
+
click.echo(' }')
|
|
440
|
+
click.echo(' }')
|
|
441
|
+
click.echo("")
|
|
442
|
+
click.echo("Step 4: Save the file")
|
|
443
|
+
click.echo("")
|
|
444
|
+
click.echo("Step 5: Verify it works")
|
|
445
|
+
click.echo(" cc list-profiles")
|
|
446
|
+
click.echo("")
|
|
447
|
+
click.echo("Step 6: Configure AWS credentials")
|
|
448
|
+
click.echo(" Option A (SSO):")
|
|
449
|
+
click.echo(" aws sso login --profile my_aws_profile")
|
|
450
|
+
click.echo(" cc calculate --profile myprofile")
|
|
451
|
+
click.echo("")
|
|
452
|
+
click.echo(" Option B (Static credentials):")
|
|
453
|
+
click.echo(" cc configure --profile myprofile")
|
|
454
|
+
click.echo(" cc calculate --profile myprofile")
|
|
455
|
+
click.echo("")
|
|
456
|
+
click.echo("=" * 70)
|
|
457
|
+
click.echo("")
|
|
458
|
+
click.echo("For multiple profiles, add more entries to the JSON:")
|
|
459
|
+
click.echo("")
|
|
460
|
+
click.echo(' {')
|
|
461
|
+
click.echo(' "profile1": { ... },')
|
|
462
|
+
click.echo(' "profile2": { ... }')
|
|
463
|
+
click.echo(' }')
|
|
464
|
+
click.echo("")
|
|
465
|
+
click.echo(f"Full path example: {config_path_example}")
|
|
466
|
+
click.echo("=" * 70)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@cli.command()
|
|
470
|
+
@click.option('--profile', required=True, help='Profile name to configure')
|
|
471
|
+
@click.option('--access-key-id', prompt=True, hide_input=False, help='AWS Access Key ID')
|
|
472
|
+
@click.option('--secret-access-key', prompt=True, hide_input=True, help='AWS Secret Access Key')
|
|
473
|
+
@click.option('--session-token', default='', help='AWS Session Token (optional, for temporary credentials)')
|
|
474
|
+
@click.option('--region', default='us-east-1', help='AWS Region (default: us-east-1)')
|
|
475
|
+
def configure(profile, access_key_id, secret_access_key, session_token, region):
|
|
476
|
+
"""Configure AWS credentials for a profile (alternative to SSO)"""
|
|
477
|
+
|
|
478
|
+
config_dir = Path.home() / '.config' / 'cost-calculator'
|
|
479
|
+
config_file = config_dir / 'profiles.json'
|
|
480
|
+
creds_file = config_dir / 'credentials.json'
|
|
481
|
+
|
|
482
|
+
# Create config directory if it doesn't exist
|
|
483
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
484
|
+
|
|
485
|
+
# Load existing profiles
|
|
486
|
+
if config_file.exists() and config_file.stat().st_size > 0:
|
|
487
|
+
try:
|
|
488
|
+
with open(config_file) as f:
|
|
489
|
+
profiles = json.load(f)
|
|
490
|
+
except json.JSONDecodeError:
|
|
491
|
+
profiles = {}
|
|
492
|
+
else:
|
|
493
|
+
profiles = {}
|
|
494
|
+
|
|
495
|
+
# Check if profile exists
|
|
496
|
+
if profile not in profiles:
|
|
497
|
+
click.echo(f"Error: Profile '{profile}' not found. Create it first with: cc init --profile {profile}")
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
# Remove aws_profile if it exists (switching from SSO to static creds)
|
|
501
|
+
if 'aws_profile' in profiles[profile]:
|
|
502
|
+
del profiles[profile]['aws_profile']
|
|
503
|
+
|
|
504
|
+
# Save updated profile
|
|
505
|
+
with open(config_file, 'w') as f:
|
|
506
|
+
json.dump(profiles, f, indent=2)
|
|
507
|
+
|
|
508
|
+
# Load or create credentials file
|
|
509
|
+
if creds_file.exists() and creds_file.stat().st_size > 0:
|
|
510
|
+
try:
|
|
511
|
+
with open(creds_file) as f:
|
|
512
|
+
creds = json.load(f)
|
|
513
|
+
except json.JSONDecodeError:
|
|
514
|
+
creds = {}
|
|
515
|
+
else:
|
|
516
|
+
creds = {}
|
|
517
|
+
|
|
518
|
+
# Store credentials (encrypted would be better, but for now just file permissions)
|
|
519
|
+
creds[profile] = {
|
|
520
|
+
'aws_access_key_id': access_key_id,
|
|
521
|
+
'aws_secret_access_key': secret_access_key,
|
|
522
|
+
'region': region
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if session_token:
|
|
526
|
+
creds[profile]['aws_session_token'] = session_token
|
|
527
|
+
|
|
528
|
+
# Save credentials with restricted permissions
|
|
529
|
+
with open(creds_file, 'w') as f:
|
|
530
|
+
json.dump(creds, f, indent=2)
|
|
531
|
+
|
|
532
|
+
# Set file permissions to 600 (owner read/write only)
|
|
533
|
+
creds_file.chmod(0o600)
|
|
534
|
+
|
|
535
|
+
click.echo(f"✓ AWS credentials configured for profile '{profile}'")
|
|
536
|
+
click.echo(f"✓ Credentials saved to {creds_file} (permissions: 600)")
|
|
537
|
+
click.echo(f"\nUsage: cc calculate --profile {profile}")
|
|
538
|
+
click.echo("\nNote: Credentials are stored locally. For temporary credentials,")
|
|
539
|
+
click.echo(" you'll need to reconfigure when they expire.")
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
if __name__ == '__main__':
|
|
543
|
+
cli()
|