runbooks 0.2.5__py3-none-any.whl → 0.6.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.
- conftest.py +26 -0
- jupyter-agent/.env.template +2 -0
- jupyter-agent/.gitattributes +35 -0
- jupyter-agent/README.md +16 -0
- jupyter-agent/app.py +256 -0
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +154 -0
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +123 -0
- jupyter-agent/requirements.txt +9 -0
- jupyter-agent/utils.py +409 -0
- runbooks/__init__.py +71 -3
- runbooks/__main__.py +13 -0
- runbooks/aws/ec2_describe_instances.py +1 -1
- runbooks/aws/ec2_run_instances.py +8 -2
- runbooks/aws/ec2_start_stop_instances.py +17 -4
- runbooks/aws/ec2_unused_volumes.py +5 -1
- runbooks/aws/s3_create_bucket.py +4 -2
- runbooks/aws/s3_list_objects.py +6 -1
- runbooks/aws/tagging_lambda_handler.py +13 -2
- runbooks/aws/tags.json +12 -0
- runbooks/base.py +353 -0
- runbooks/cfat/README.md +49 -0
- runbooks/cfat/__init__.py +74 -0
- runbooks/cfat/app.ts +644 -0
- runbooks/cfat/assessment/__init__.py +40 -0
- runbooks/cfat/assessment/asana-import.csv +39 -0
- runbooks/cfat/assessment/cfat-checks.csv +31 -0
- runbooks/cfat/assessment/cfat.txt +520 -0
- runbooks/cfat/assessment/collectors.py +200 -0
- runbooks/cfat/assessment/jira-import.csv +39 -0
- runbooks/cfat/assessment/runner.py +387 -0
- runbooks/cfat/assessment/validators.py +290 -0
- runbooks/cfat/cli.py +103 -0
- runbooks/cfat/docs/asana-import.csv +24 -0
- runbooks/cfat/docs/cfat-checks.csv +31 -0
- runbooks/cfat/docs/cfat.txt +335 -0
- runbooks/cfat/docs/checks-output.png +0 -0
- runbooks/cfat/docs/cloudshell-console-run.png +0 -0
- runbooks/cfat/docs/cloudshell-download.png +0 -0
- runbooks/cfat/docs/cloudshell-output.png +0 -0
- runbooks/cfat/docs/downloadfile.png +0 -0
- runbooks/cfat/docs/jira-import.csv +24 -0
- runbooks/cfat/docs/open-cloudshell.png +0 -0
- runbooks/cfat/docs/report-header.png +0 -0
- runbooks/cfat/models.py +1026 -0
- runbooks/cfat/package-lock.json +5116 -0
- runbooks/cfat/package.json +38 -0
- runbooks/cfat/report.py +496 -0
- runbooks/cfat/reporting/__init__.py +46 -0
- runbooks/cfat/reporting/exporters.py +337 -0
- runbooks/cfat/reporting/formatters.py +496 -0
- runbooks/cfat/reporting/templates.py +135 -0
- runbooks/cfat/run-assessment.sh +23 -0
- runbooks/cfat/runner.py +69 -0
- runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
- runbooks/cfat/src/actions/check-config-existence.ts +37 -0
- runbooks/cfat/src/actions/check-control-tower.ts +37 -0
- runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
- runbooks/cfat/src/actions/check-iam-users.ts +50 -0
- runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
- runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
- runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
- runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
- runbooks/cfat/src/actions/create-backlog.ts +372 -0
- runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
- runbooks/cfat/src/actions/create-report.ts +616 -0
- runbooks/cfat/src/actions/define-account-type.ts +51 -0
- runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
- runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
- runbooks/cfat/src/actions/get-idc-info.ts +34 -0
- runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
- runbooks/cfat/src/actions/get-org-details.ts +35 -0
- runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
- runbooks/cfat/src/actions/get-org-ous.ts +35 -0
- runbooks/cfat/src/actions/get-regions.ts +22 -0
- runbooks/cfat/src/actions/zip-assessment.ts +27 -0
- runbooks/cfat/src/types/index.d.ts +147 -0
- runbooks/cfat/tests/__init__.py +141 -0
- runbooks/cfat/tests/test_cli.py +340 -0
- runbooks/cfat/tests/test_integration.py +290 -0
- runbooks/cfat/tests/test_models.py +505 -0
- runbooks/cfat/tests/test_reporting.py +354 -0
- runbooks/cfat/tsconfig.json +16 -0
- runbooks/cfat/webpack.config.cjs +27 -0
- runbooks/config.py +260 -0
- runbooks/finops/__init__.py +88 -0
- runbooks/finops/aws_client.py +245 -0
- runbooks/finops/cli.py +151 -0
- runbooks/finops/cost_processor.py +410 -0
- runbooks/finops/dashboard_runner.py +448 -0
- runbooks/finops/helpers.py +355 -0
- runbooks/finops/main.py +14 -0
- runbooks/finops/profile_processor.py +174 -0
- runbooks/finops/types.py +66 -0
- runbooks/finops/visualisations.py +80 -0
- runbooks/inventory/.gitignore +354 -0
- runbooks/inventory/ArgumentsClass.py +261 -0
- runbooks/inventory/Inventory_Modules.py +6130 -0
- runbooks/inventory/LandingZone/delete_lz.py +1075 -0
- runbooks/inventory/README.md +1320 -0
- runbooks/inventory/__init__.py +62 -0
- runbooks/inventory/account_class.py +532 -0
- runbooks/inventory/all_my_instances_wrapper.py +123 -0
- runbooks/inventory/aws_decorators.py +201 -0
- runbooks/inventory/cfn_move_stack_instances.py +1526 -0
- runbooks/inventory/check_cloudtrail_compliance.py +614 -0
- runbooks/inventory/check_controltower_readiness.py +1107 -0
- runbooks/inventory/check_landingzone_readiness.py +711 -0
- runbooks/inventory/cloudtrail.md +727 -0
- runbooks/inventory/collectors/__init__.py +20 -0
- runbooks/inventory/collectors/aws_compute.py +518 -0
- runbooks/inventory/collectors/aws_networking.py +275 -0
- runbooks/inventory/collectors/base.py +222 -0
- runbooks/inventory/core/__init__.py +19 -0
- runbooks/inventory/core/collector.py +303 -0
- runbooks/inventory/core/formatter.py +296 -0
- runbooks/inventory/delete_s3_buckets_objects.py +169 -0
- runbooks/inventory/discovery.md +81 -0
- runbooks/inventory/draw_org_structure.py +748 -0
- runbooks/inventory/ec2_vpc_utils.py +341 -0
- runbooks/inventory/find_cfn_drift_detection.py +272 -0
- runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
- runbooks/inventory/find_cfn_stackset_drift.py +733 -0
- runbooks/inventory/find_ec2_security_groups.py +669 -0
- runbooks/inventory/find_landingzone_versions.py +201 -0
- runbooks/inventory/find_vpc_flow_logs.py +1221 -0
- runbooks/inventory/inventory.sh +659 -0
- runbooks/inventory/list_cfn_stacks.py +558 -0
- runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
- runbooks/inventory/list_cfn_stackset_operations.py +734 -0
- runbooks/inventory/list_cfn_stacksets.py +453 -0
- runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
- runbooks/inventory/list_ds_directories.py +354 -0
- runbooks/inventory/list_ec2_availability_zones.py +286 -0
- runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
- runbooks/inventory/list_ec2_instances.py +425 -0
- runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
- runbooks/inventory/list_elbs_load_balancers.py +411 -0
- runbooks/inventory/list_enis_network_interfaces.py +526 -0
- runbooks/inventory/list_guardduty_detectors.py +568 -0
- runbooks/inventory/list_iam_policies.py +404 -0
- runbooks/inventory/list_iam_roles.py +518 -0
- runbooks/inventory/list_iam_saml_providers.py +359 -0
- runbooks/inventory/list_lambda_functions.py +882 -0
- runbooks/inventory/list_org_accounts.py +446 -0
- runbooks/inventory/list_org_accounts_users.py +354 -0
- runbooks/inventory/list_rds_db_instances.py +406 -0
- runbooks/inventory/list_route53_hosted_zones.py +318 -0
- runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
- runbooks/inventory/list_sns_topics.py +360 -0
- runbooks/inventory/list_ssm_parameters.py +402 -0
- runbooks/inventory/list_vpc_subnets.py +433 -0
- runbooks/inventory/list_vpcs.py +422 -0
- runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
- runbooks/inventory/models/__init__.py +24 -0
- runbooks/inventory/models/account.py +192 -0
- runbooks/inventory/models/inventory.py +309 -0
- runbooks/inventory/models/resource.py +247 -0
- runbooks/inventory/recover_cfn_stack_ids.py +205 -0
- runbooks/inventory/requirements.txt +12 -0
- runbooks/inventory/run_on_multi_accounts.py +211 -0
- runbooks/inventory/tests/common_test_data.py +3661 -0
- runbooks/inventory/tests/common_test_functions.py +204 -0
- runbooks/inventory/tests/script_test_data.py +0 -0
- runbooks/inventory/tests/setup.py +24 -0
- runbooks/inventory/tests/src.py +18 -0
- runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
- runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
- runbooks/inventory/tests/test_inventory_modules.py +55 -0
- runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
- runbooks/inventory/tests/test_moto_integration_example.py +273 -0
- runbooks/inventory/tests/test_org_list_accounts.py +49 -0
- runbooks/inventory/update_aws_actions.py +173 -0
- runbooks/inventory/update_cfn_stacksets.py +1215 -0
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
- runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
- runbooks/inventory/update_s3_public_access_block.py +539 -0
- runbooks/inventory/utils/__init__.py +23 -0
- runbooks/inventory/utils/aws_helpers.py +510 -0
- runbooks/inventory/utils/threading_utils.py +493 -0
- runbooks/inventory/utils/validation.py +682 -0
- runbooks/inventory/verify_ec2_security_groups.py +1430 -0
- runbooks/main.py +785 -0
- runbooks/organizations/__init__.py +12 -0
- runbooks/organizations/manager.py +374 -0
- runbooks/security_baseline/README.md +324 -0
- runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
- runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
- runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
- runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
- runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
- runbooks/security_baseline/checklist/root_access_key.py +6 -1
- runbooks/security_baseline/config-origin.json +1 -1
- runbooks/security_baseline/config.json +1 -1
- runbooks/security_baseline/permission.json +1 -1
- runbooks/security_baseline/report_generator.py +10 -2
- runbooks/security_baseline/report_template_en.html +7 -7
- runbooks/security_baseline/report_template_jp.html +7 -7
- runbooks/security_baseline/report_template_kr.html +12 -12
- runbooks/security_baseline/report_template_vn.html +7 -7
- runbooks/security_baseline/requirements.txt +7 -0
- runbooks/security_baseline/run_script.py +8 -2
- runbooks/security_baseline/security_baseline_tester.py +10 -2
- runbooks/security_baseline/utils/common.py +5 -1
- runbooks/utils/__init__.py +204 -0
- runbooks-0.6.1.dist-info/METADATA +373 -0
- runbooks-0.6.1.dist-info/RECORD +237 -0
- {runbooks-0.2.5.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
- runbooks-0.6.1.dist-info/entry_points.txt +7 -0
- runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
- runbooks-0.6.1.dist-info/top_level.txt +3 -0
- runbooks/python101/calculator.py +0 -34
- runbooks/python101/config.py +0 -1
- runbooks/python101/exceptions.py +0 -16
- runbooks/python101/file_manager.py +0 -218
- runbooks/python101/toolkit.py +0 -153
- runbooks-0.2.5.dist-info/METADATA +0 -439
- runbooks-0.2.5.dist-info/RECORD +0 -61
- runbooks-0.2.5.dist-info/entry_points.txt +0 -3
- runbooks-0.2.5.dist-info/top_level.txt +0 -1
@@ -0,0 +1,410 @@
|
|
1
|
+
import csv
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
from collections import defaultdict
|
5
|
+
from datetime import date, datetime, timedelta
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple
|
7
|
+
|
8
|
+
from boto3.session import Session
|
9
|
+
from rich.console import Console
|
10
|
+
|
11
|
+
from runbooks.finops.aws_client import get_account_id
|
12
|
+
from runbooks.finops.types import BudgetInfo, CostData, EC2Summary, ProfileData
|
13
|
+
|
14
|
+
console = Console()
|
15
|
+
|
16
|
+
|
17
|
+
def get_trend(session: Session, tag: Optional[List[str]] = None) -> Dict[str, Any]:
|
18
|
+
"""
|
19
|
+
Get cost trend data for an AWS account.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
session: The boto3 session to use
|
23
|
+
tag: Optional list of tags in "Key=Value" format to filter resources.
|
24
|
+
|
25
|
+
"""
|
26
|
+
ce = session.client("ce")
|
27
|
+
tag_filters: List[Dict[str, Any]] = []
|
28
|
+
if tag:
|
29
|
+
for t in tag:
|
30
|
+
key, value = t.split("=", 1)
|
31
|
+
tag_filters.append({"Key": key, "Values": [value]})
|
32
|
+
|
33
|
+
filter_param: Optional[Dict[str, Any]] = None
|
34
|
+
if tag_filters:
|
35
|
+
if len(tag_filters) == 1:
|
36
|
+
filter_param = {
|
37
|
+
"Tags": {
|
38
|
+
"Key": tag_filters[0]["Key"],
|
39
|
+
"Values": tag_filters[0]["Values"],
|
40
|
+
"MatchOptions": ["EQUALS"],
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
else:
|
45
|
+
filter_param = {
|
46
|
+
"And": [
|
47
|
+
{
|
48
|
+
"Tags": {
|
49
|
+
"Key": f["Key"],
|
50
|
+
"Values": f["Values"],
|
51
|
+
"MatchOptions": ["EQUALS"],
|
52
|
+
}
|
53
|
+
}
|
54
|
+
for f in tag_filters
|
55
|
+
]
|
56
|
+
}
|
57
|
+
kwargs = {}
|
58
|
+
if filter_param:
|
59
|
+
kwargs["Filter"] = filter_param
|
60
|
+
|
61
|
+
end_date = date.today()
|
62
|
+
start_date = (end_date - timedelta(days=180)).replace(day=1)
|
63
|
+
account_id = get_account_id(session)
|
64
|
+
profile = session.profile_name
|
65
|
+
|
66
|
+
monthly_costs = []
|
67
|
+
|
68
|
+
try:
|
69
|
+
monthly_data = ce.get_cost_and_usage(
|
70
|
+
TimePeriod={
|
71
|
+
"Start": start_date.isoformat(),
|
72
|
+
"End": end_date.isoformat(),
|
73
|
+
},
|
74
|
+
Granularity="MONTHLY",
|
75
|
+
Metrics=["UnblendedCost"],
|
76
|
+
**kwargs,
|
77
|
+
)
|
78
|
+
for period in monthly_data.get("ResultsByTime", []):
|
79
|
+
month = datetime.strptime(period["TimePeriod"]["Start"], "%Y-%m-%d").strftime("%b %Y")
|
80
|
+
cost = float(period["Total"]["UnblendedCost"]["Amount"])
|
81
|
+
monthly_costs.append((month, cost))
|
82
|
+
except Exception as e:
|
83
|
+
console.log(f"[yellow]Error getting monthly trend data: {e}[/]")
|
84
|
+
monthly_costs = []
|
85
|
+
|
86
|
+
return {
|
87
|
+
"monthly_costs": monthly_costs,
|
88
|
+
"account_id": account_id,
|
89
|
+
"profile": profile,
|
90
|
+
}
|
91
|
+
|
92
|
+
|
93
|
+
def get_cost_data(
|
94
|
+
session: Session,
|
95
|
+
time_range: Optional[int] = None,
|
96
|
+
tag: Optional[List[str]] = None,
|
97
|
+
get_trend: bool = False,
|
98
|
+
) -> CostData:
|
99
|
+
"""
|
100
|
+
Get cost data for an AWS account.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
session: The boto3 session to use
|
104
|
+
time_range: Optional time range in days for cost data (default: current month)
|
105
|
+
tag: Optional list of tags in "Key=Value" format to filter resources.
|
106
|
+
get_trend: Optional boolean to get trend data for last 6 months (default).
|
107
|
+
|
108
|
+
"""
|
109
|
+
ce = session.client("ce")
|
110
|
+
budgets = session.client("budgets", region_name="us-east-1")
|
111
|
+
today = date.today()
|
112
|
+
|
113
|
+
tag_filters: List[Dict[str, Any]] = []
|
114
|
+
if tag:
|
115
|
+
for t in tag:
|
116
|
+
key, value = t.split("=", 1)
|
117
|
+
tag_filters.append({"Key": key, "Values": [value]})
|
118
|
+
|
119
|
+
filter_param: Optional[Dict[str, Any]] = None
|
120
|
+
if tag_filters:
|
121
|
+
if len(tag_filters) == 1:
|
122
|
+
filter_param = {
|
123
|
+
"Tags": {
|
124
|
+
"Key": tag_filters[0]["Key"],
|
125
|
+
"Values": tag_filters[0]["Values"],
|
126
|
+
"MatchOptions": ["EQUALS"],
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
else:
|
131
|
+
filter_param = {
|
132
|
+
"And": [
|
133
|
+
{
|
134
|
+
"Tags": {
|
135
|
+
"Key": f["Key"],
|
136
|
+
"Values": f["Values"],
|
137
|
+
"MatchOptions": ["EQUALS"],
|
138
|
+
}
|
139
|
+
}
|
140
|
+
for f in tag_filters
|
141
|
+
]
|
142
|
+
}
|
143
|
+
kwargs = {}
|
144
|
+
if filter_param:
|
145
|
+
kwargs["Filter"] = filter_param
|
146
|
+
|
147
|
+
if time_range:
|
148
|
+
end_date = today
|
149
|
+
start_date = today - timedelta(days=time_range)
|
150
|
+
previous_period_end = start_date - timedelta(days=1)
|
151
|
+
previous_period_start = previous_period_end - timedelta(days=time_range)
|
152
|
+
|
153
|
+
else:
|
154
|
+
start_date = today.replace(day=1)
|
155
|
+
end_date = today
|
156
|
+
|
157
|
+
# Edge case when user runs the tool on the first day of the month
|
158
|
+
if start_date == end_date:
|
159
|
+
end_date += timedelta(days=1)
|
160
|
+
|
161
|
+
# Last calendar month
|
162
|
+
previous_period_end = start_date - timedelta(days=1)
|
163
|
+
previous_period_start = previous_period_end.replace(day=1)
|
164
|
+
|
165
|
+
account_id = get_account_id(session)
|
166
|
+
|
167
|
+
try:
|
168
|
+
this_period = ce.get_cost_and_usage(
|
169
|
+
TimePeriod={"Start": start_date.isoformat(), "End": end_date.isoformat()},
|
170
|
+
Granularity="MONTHLY",
|
171
|
+
Metrics=["UnblendedCost"],
|
172
|
+
**kwargs,
|
173
|
+
)
|
174
|
+
except Exception as e:
|
175
|
+
console.log(f"[yellow]Error getting current period cost: {e}[/]")
|
176
|
+
this_period = {"ResultsByTime": [{"Total": {"UnblendedCost": {"Amount": 0}}}]}
|
177
|
+
|
178
|
+
try:
|
179
|
+
previous_period = ce.get_cost_and_usage(
|
180
|
+
TimePeriod={
|
181
|
+
"Start": previous_period_start.isoformat(),
|
182
|
+
"End": previous_period_end.isoformat(),
|
183
|
+
},
|
184
|
+
Granularity="MONTHLY",
|
185
|
+
Metrics=["UnblendedCost"],
|
186
|
+
**kwargs,
|
187
|
+
)
|
188
|
+
except Exception as e:
|
189
|
+
console.log(f"[yellow]Error getting previous period cost: {e}[/]")
|
190
|
+
previous_period = {"ResultsByTime": [{"Total": {"UnblendedCost": {"Amount": 0}}}]}
|
191
|
+
|
192
|
+
try:
|
193
|
+
current_period_cost_by_service = ce.get_cost_and_usage(
|
194
|
+
TimePeriod={"Start": start_date.isoformat(), "End": end_date.isoformat()},
|
195
|
+
Granularity="DAILY" if time_range else "MONTHLY",
|
196
|
+
Metrics=["UnblendedCost"],
|
197
|
+
GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
|
198
|
+
**kwargs,
|
199
|
+
)
|
200
|
+
except Exception as e:
|
201
|
+
console.log(f"[yellow]Error getting current period cost by service: {e}[/]")
|
202
|
+
current_period_cost_by_service = {"ResultsByTime": [{"Groups": []}]}
|
203
|
+
|
204
|
+
# Aggregate cost by service across all days
|
205
|
+
aggregated_service_costs: Dict[str, float] = defaultdict(float)
|
206
|
+
|
207
|
+
for result in current_period_cost_by_service.get("ResultsByTime", []):
|
208
|
+
for group in result.get("Groups", []):
|
209
|
+
service = group["Keys"][0]
|
210
|
+
amount = float(group["Metrics"]["UnblendedCost"]["Amount"])
|
211
|
+
aggregated_service_costs[service] += amount
|
212
|
+
|
213
|
+
# Reformat into groups by service
|
214
|
+
aggregated_groups = [
|
215
|
+
{"Keys": [service], "Metrics": {"UnblendedCost": {"Amount": str(amount)}}}
|
216
|
+
for service, amount in aggregated_service_costs.items()
|
217
|
+
]
|
218
|
+
|
219
|
+
budgets_data: List[BudgetInfo] = []
|
220
|
+
try:
|
221
|
+
response = budgets.describe_budgets(AccountId=account_id)
|
222
|
+
for budget in response["Budgets"]:
|
223
|
+
budgets_data.append(
|
224
|
+
{
|
225
|
+
"name": budget["BudgetName"],
|
226
|
+
"limit": float(budget["BudgetLimit"]["Amount"]),
|
227
|
+
"actual": float(budget["CalculatedSpend"]["ActualSpend"]["Amount"]),
|
228
|
+
"forecast": float(budget["CalculatedSpend"].get("ForecastedSpend", {}).get("Amount", 0.0)) or None,
|
229
|
+
}
|
230
|
+
)
|
231
|
+
except Exception as e:
|
232
|
+
pass
|
233
|
+
|
234
|
+
current_period_cost = 0.0
|
235
|
+
for period in this_period.get("ResultsByTime", []):
|
236
|
+
if "Total" in period and "UnblendedCost" in period["Total"]:
|
237
|
+
current_period_cost += float(period["Total"]["UnblendedCost"]["Amount"])
|
238
|
+
|
239
|
+
previous_period_cost = 0.0
|
240
|
+
for period in previous_period.get("ResultsByTime", []):
|
241
|
+
if "Total" in period and "UnblendedCost" in period["Total"]:
|
242
|
+
previous_period_cost += float(period["Total"]["UnblendedCost"]["Amount"])
|
243
|
+
|
244
|
+
current_period_name = f"Current {time_range} days cost" if time_range else "Current month's cost"
|
245
|
+
previous_period_name = f"Previous {time_range} days cost" if time_range else "Last month's cost"
|
246
|
+
|
247
|
+
return {
|
248
|
+
"account_id": account_id,
|
249
|
+
"current_month": current_period_cost,
|
250
|
+
"last_month": previous_period_cost,
|
251
|
+
"current_month_cost_by_service": aggregated_groups,
|
252
|
+
"budgets": budgets_data,
|
253
|
+
"current_period_name": current_period_name,
|
254
|
+
"previous_period_name": previous_period_name,
|
255
|
+
"time_range": time_range,
|
256
|
+
"current_period_start": start_date.isoformat(),
|
257
|
+
"current_period_end": end_date.isoformat(),
|
258
|
+
"previous_period_start": previous_period_start.isoformat(),
|
259
|
+
"previous_period_end": previous_period_end.isoformat(),
|
260
|
+
"monthly_costs": None,
|
261
|
+
}
|
262
|
+
|
263
|
+
|
264
|
+
def process_service_costs(
|
265
|
+
cost_data: CostData,
|
266
|
+
) -> Tuple[List[str], List[Tuple[str, float]]]:
|
267
|
+
"""Process and format service costs from cost data."""
|
268
|
+
service_costs: List[str] = []
|
269
|
+
service_cost_data: List[Tuple[str, float]] = []
|
270
|
+
|
271
|
+
for group in cost_data["current_month_cost_by_service"]:
|
272
|
+
if "Keys" in group and "Metrics" in group:
|
273
|
+
service_name = group["Keys"][0]
|
274
|
+
cost_amount = float(group["Metrics"]["UnblendedCost"]["Amount"])
|
275
|
+
if cost_amount > 0.001:
|
276
|
+
service_cost_data.append((service_name, cost_amount))
|
277
|
+
|
278
|
+
service_cost_data.sort(key=lambda x: x[1], reverse=True)
|
279
|
+
|
280
|
+
if not service_cost_data:
|
281
|
+
service_costs.append("No costs associated with this account")
|
282
|
+
else:
|
283
|
+
for service_name, cost_amount in service_cost_data:
|
284
|
+
service_costs.append(f"{service_name}: ${cost_amount:.2f}")
|
285
|
+
|
286
|
+
return service_costs, service_cost_data
|
287
|
+
|
288
|
+
|
289
|
+
def format_budget_info(budgets: List[BudgetInfo]) -> List[str]:
|
290
|
+
"""Format budget information for display."""
|
291
|
+
budget_info: List[str] = []
|
292
|
+
for budget in budgets:
|
293
|
+
budget_info.append(f"{budget['name']} limit: ${budget['limit']}")
|
294
|
+
budget_info.append(f"{budget['name']} actual: ${budget['actual']:.2f}")
|
295
|
+
if budget["forecast"] is not None:
|
296
|
+
budget_info.append(f"{budget['name']} forecast: ${budget['forecast']:.2f}")
|
297
|
+
|
298
|
+
if not budget_info:
|
299
|
+
budget_info.append("No budgets found;\nCreate a budget for this account")
|
300
|
+
|
301
|
+
return budget_info
|
302
|
+
|
303
|
+
|
304
|
+
def format_ec2_summary(ec2_data: EC2Summary) -> List[str]:
|
305
|
+
"""Format EC2 instance summary for display."""
|
306
|
+
ec2_summary_text: List[str] = []
|
307
|
+
for state, count in sorted(ec2_data.items()):
|
308
|
+
if count > 0:
|
309
|
+
state_color = (
|
310
|
+
"bright_green" if state == "running" else "bright_yellow" if state == "stopped" else "bright_cyan"
|
311
|
+
)
|
312
|
+
ec2_summary_text.append(f"[{state_color}]{state}: {count}[/]")
|
313
|
+
|
314
|
+
if not ec2_summary_text:
|
315
|
+
ec2_summary_text = ["No instances found"]
|
316
|
+
|
317
|
+
return ec2_summary_text
|
318
|
+
|
319
|
+
|
320
|
+
def change_in_total_cost(current_period: float, previous_period: float) -> Optional[float]:
|
321
|
+
"""Calculate the change in total cost between current period and previous period."""
|
322
|
+
if abs(previous_period) < 0.01:
|
323
|
+
if abs(current_period) < 0.01:
|
324
|
+
return 0.00 # No change if both periods are zero
|
325
|
+
return None # Undefined percentage change if previous is zero but current is non-zero
|
326
|
+
|
327
|
+
# Calculate percentage change
|
328
|
+
return ((current_period - previous_period) / previous_period) * 100.00
|
329
|
+
|
330
|
+
|
331
|
+
def export_to_csv(
|
332
|
+
data: List[ProfileData],
|
333
|
+
filename: str,
|
334
|
+
output_dir: Optional[str] = None,
|
335
|
+
previous_period_dates: str = "N/A",
|
336
|
+
current_period_dates: str = "N/A",
|
337
|
+
) -> Optional[str]:
|
338
|
+
"""Export dashboard data to a CSV file."""
|
339
|
+
try:
|
340
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
|
341
|
+
base_filename = f"{filename}_{timestamp}.csv"
|
342
|
+
|
343
|
+
if output_dir:
|
344
|
+
os.makedirs(output_dir, exist_ok=True)
|
345
|
+
output_filename = os.path.join(output_dir, base_filename)
|
346
|
+
else:
|
347
|
+
output_filename = base_filename
|
348
|
+
|
349
|
+
previous_period_header = f"Cost for period\n({previous_period_dates})"
|
350
|
+
current_period_header = f"Cost for period\n({current_period_dates})"
|
351
|
+
|
352
|
+
with open(output_filename, "w", newline="") as csvfile:
|
353
|
+
fieldnames = [
|
354
|
+
"CLI Profile",
|
355
|
+
"AWS Account ID",
|
356
|
+
previous_period_header,
|
357
|
+
current_period_header,
|
358
|
+
"Cost By Service",
|
359
|
+
"Budget Status",
|
360
|
+
"EC2 Instances",
|
361
|
+
]
|
362
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
363
|
+
writer.writeheader()
|
364
|
+
for row in data:
|
365
|
+
services_data = "\n".join([f"{service}: ${cost:.2f}" for service, cost in row["service_costs"]])
|
366
|
+
|
367
|
+
budgets_data = "\n".join(row["budget_info"]) if row["budget_info"] else "No budgets"
|
368
|
+
|
369
|
+
ec2_data_summary = "\n".join(
|
370
|
+
[f"{state}: {count}" for state, count in row["ec2_summary"].items() if count > 0]
|
371
|
+
)
|
372
|
+
|
373
|
+
writer.writerow(
|
374
|
+
{
|
375
|
+
"CLI Profile": row["profile"],
|
376
|
+
"AWS Account ID": row["account_id"],
|
377
|
+
previous_period_header: f"${row['last_month']:.2f}",
|
378
|
+
current_period_header: f"${row['current_month']:.2f}",
|
379
|
+
"Cost By Service": services_data or "No costs",
|
380
|
+
"Budget Status": budgets_data or "No budgets",
|
381
|
+
"EC2 Instances": ec2_data_summary or "No instances",
|
382
|
+
}
|
383
|
+
)
|
384
|
+
console.print(f"[bright_green]Exported dashboard data to {os.path.abspath(output_filename)}[/]")
|
385
|
+
return os.path.abspath(output_filename)
|
386
|
+
except Exception as e:
|
387
|
+
console.print(f"[bold red]Error exporting to CSV: {str(e)}[/]")
|
388
|
+
return None
|
389
|
+
|
390
|
+
|
391
|
+
def export_to_json(data: List[ProfileData], filename: str, output_dir: Optional[str] = None) -> Optional[str]:
|
392
|
+
"""Export dashboard data to a JSON file."""
|
393
|
+
try:
|
394
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
|
395
|
+
base_filename = f"{filename}_{timestamp}.json"
|
396
|
+
|
397
|
+
if output_dir:
|
398
|
+
os.makedirs(output_dir, exist_ok=True)
|
399
|
+
output_filename = os.path.join(output_dir, base_filename)
|
400
|
+
else:
|
401
|
+
output_filename = base_filename
|
402
|
+
|
403
|
+
with open(output_filename, "w") as jsonfile:
|
404
|
+
json.dump(data, jsonfile, indent=4)
|
405
|
+
|
406
|
+
console.print(f"[bright_green]Exported dashboard data to {os.path.abspath(output_filename)}[/]")
|
407
|
+
return os.path.abspath(output_filename)
|
408
|
+
except Exception as e:
|
409
|
+
console.print(f"[bold red]Error exporting to JSON: {str(e)}[/]")
|
410
|
+
return None
|