runbooks 0.2.3__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 +8 -8
- runbooks/security_baseline/report_template_jp.html +8 -8
- runbooks/security_baseline/report_template_kr.html +13 -13
- runbooks/security_baseline/report_template_vn.html +8 -8
- 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.3.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.3.dist-info/METADATA +0 -435
- runbooks-0.2.3.dist-info/RECORD +0 -61
- runbooks-0.2.3.dist-info/entry_points.txt +0 -3
- runbooks-0.2.3.dist-info/top_level.txt +0 -1
@@ -0,0 +1,448 @@
|
|
1
|
+
import argparse
|
2
|
+
from collections import defaultdict
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple
|
4
|
+
|
5
|
+
import boto3
|
6
|
+
from rich import box
|
7
|
+
from rich.console import Console
|
8
|
+
from rich.progress import track
|
9
|
+
from rich.status import Status
|
10
|
+
from rich.table import Column, Table
|
11
|
+
|
12
|
+
from runbooks.finops.aws_client import (
|
13
|
+
get_accessible_regions,
|
14
|
+
get_account_id,
|
15
|
+
get_budgets,
|
16
|
+
get_stopped_instances,
|
17
|
+
get_untagged_resources,
|
18
|
+
get_unused_eips,
|
19
|
+
get_unused_volumes,
|
20
|
+
)
|
21
|
+
from runbooks.finops.cost_processor import (
|
22
|
+
export_to_csv,
|
23
|
+
export_to_json,
|
24
|
+
get_cost_data,
|
25
|
+
get_trend,
|
26
|
+
)
|
27
|
+
from runbooks.finops.helpers import (
|
28
|
+
clean_rich_tags,
|
29
|
+
export_audit_report_to_csv,
|
30
|
+
export_audit_report_to_json,
|
31
|
+
export_audit_report_to_pdf,
|
32
|
+
export_cost_dashboard_to_pdf,
|
33
|
+
export_trend_data_to_json,
|
34
|
+
)
|
35
|
+
from runbooks.finops.profile_processor import (
|
36
|
+
process_combined_profiles,
|
37
|
+
process_single_profile,
|
38
|
+
)
|
39
|
+
from runbooks.finops.types import ProfileData
|
40
|
+
from runbooks.finops.visualisations import create_trend_bars
|
41
|
+
|
42
|
+
console = Console()
|
43
|
+
|
44
|
+
|
45
|
+
def _initialize_profiles(
|
46
|
+
args: argparse.Namespace,
|
47
|
+
) -> Tuple[List[str], Optional[List[str]], Optional[int]]:
|
48
|
+
"""Initialize AWS profiles based on arguments."""
|
49
|
+
available_profiles = get_aws_profiles()
|
50
|
+
if not available_profiles:
|
51
|
+
console.log("[bold red]No AWS profiles found. Please configure AWS CLI first.[/]")
|
52
|
+
raise SystemExit(1)
|
53
|
+
|
54
|
+
profiles_to_use = []
|
55
|
+
if args.profiles:
|
56
|
+
for profile in args.profiles:
|
57
|
+
if profile in available_profiles:
|
58
|
+
profiles_to_use.append(profile)
|
59
|
+
else:
|
60
|
+
console.log(f"[yellow]Warning: Profile '{profile}' not found in AWS configuration[/]")
|
61
|
+
if not profiles_to_use:
|
62
|
+
console.log("[bold red]None of the specified profiles were found in AWS configuration.[/]")
|
63
|
+
raise SystemExit(1)
|
64
|
+
elif args.all:
|
65
|
+
profiles_to_use = available_profiles
|
66
|
+
else:
|
67
|
+
if "default" in available_profiles:
|
68
|
+
profiles_to_use = ["default"]
|
69
|
+
else:
|
70
|
+
profiles_to_use = available_profiles
|
71
|
+
console.log("[yellow]No default profile found. Using all available profiles.[/]")
|
72
|
+
|
73
|
+
return profiles_to_use, args.regions, args.time_range
|
74
|
+
|
75
|
+
|
76
|
+
def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> None:
|
77
|
+
"""Generate and export an audit report."""
|
78
|
+
console.print("[bold bright_cyan]Preparing your audit report...[/]")
|
79
|
+
table = Table(
|
80
|
+
Column("Profile", justify="center"),
|
81
|
+
Column("Account ID", justify="center"),
|
82
|
+
Column("Untagged Resources"),
|
83
|
+
Column("Stopped EC2 Instances"),
|
84
|
+
Column("Unused Volumes"),
|
85
|
+
Column("Unused EIPs"),
|
86
|
+
Column("Budget Alerts"),
|
87
|
+
title="AWS FinOps Audit Report",
|
88
|
+
show_lines=True,
|
89
|
+
box=box.ASCII_DOUBLE_HEAD,
|
90
|
+
style="bright_cyan",
|
91
|
+
)
|
92
|
+
|
93
|
+
audit_data = []
|
94
|
+
raw_audit_data = []
|
95
|
+
nl = "\n"
|
96
|
+
comma_nl = ",\n"
|
97
|
+
|
98
|
+
for profile in profiles_to_use:
|
99
|
+
session = boto3.Session(profile_name=profile)
|
100
|
+
account_id = get_account_id(session) or "Unknown"
|
101
|
+
regions = args.regions or get_accessible_regions(session)
|
102
|
+
|
103
|
+
try:
|
104
|
+
untagged = get_untagged_resources(session, regions)
|
105
|
+
anomalies = []
|
106
|
+
for service, region_map in untagged.items():
|
107
|
+
if region_map:
|
108
|
+
service_block = f"[bright_yellow]{service}[/]:\n"
|
109
|
+
for region, ids in region_map.items():
|
110
|
+
if ids:
|
111
|
+
ids_block = "\n".join(f"[orange1]{res_id}[/]" for res_id in ids)
|
112
|
+
service_block += f"\n{region}:\n{ids_block}\n"
|
113
|
+
anomalies.append(service_block)
|
114
|
+
if not any(region_map for region_map in untagged.values()):
|
115
|
+
anomalies = ["None"]
|
116
|
+
except Exception as e:
|
117
|
+
anomalies = [f"Error: {str(e)}"]
|
118
|
+
|
119
|
+
stopped = get_stopped_instances(session, regions)
|
120
|
+
stopped_list = [f"{r}:\n[gold1]{nl.join(ids)}[/]" for r, ids in stopped.items()] or ["None"]
|
121
|
+
|
122
|
+
unused_vols = get_unused_volumes(session, regions)
|
123
|
+
vols_list = [f"{r}:\n[dark_orange]{nl.join(ids)}[/]" for r, ids in unused_vols.items()] or ["None"]
|
124
|
+
|
125
|
+
unused_eips = get_unused_eips(session, regions)
|
126
|
+
eips_list = [f"{r}:\n{comma_nl.join(ids)}" for r, ids in unused_eips.items()] or ["None"]
|
127
|
+
|
128
|
+
budget_data = get_budgets(session)
|
129
|
+
alerts = []
|
130
|
+
for b in budget_data:
|
131
|
+
if b["actual"] > b["limit"]:
|
132
|
+
alerts.append(f"[red1]{b['name']}[/]: ${b['actual']:.2f} > ${b['limit']:.2f}")
|
133
|
+
if not alerts:
|
134
|
+
alerts = ["No budgets exceeded"]
|
135
|
+
|
136
|
+
audit_data.append(
|
137
|
+
{
|
138
|
+
"profile": profile,
|
139
|
+
"account_id": account_id,
|
140
|
+
"untagged_resources": clean_rich_tags("\n".join(anomalies)),
|
141
|
+
"stopped_instances": clean_rich_tags("\n".join(stopped_list)),
|
142
|
+
"unused_volumes": clean_rich_tags("\n".join(vols_list)),
|
143
|
+
"unused_eips": clean_rich_tags("\n".join(eips_list)),
|
144
|
+
"budget_alerts": clean_rich_tags("\n".join(alerts)),
|
145
|
+
}
|
146
|
+
)
|
147
|
+
|
148
|
+
# Data for JSON which includes raw audit data
|
149
|
+
raw_audit_data.append(
|
150
|
+
{
|
151
|
+
"profile": profile,
|
152
|
+
"account_id": account_id,
|
153
|
+
"untagged_resources": untagged,
|
154
|
+
"stopped_instances": stopped,
|
155
|
+
"unused_volumes": unused_vols,
|
156
|
+
"unused_eips": unused_eips,
|
157
|
+
"budget_alerts": budget_data,
|
158
|
+
}
|
159
|
+
)
|
160
|
+
|
161
|
+
table.add_row(
|
162
|
+
f"[dark_magenta]{profile}[/]",
|
163
|
+
account_id,
|
164
|
+
"\n".join(anomalies),
|
165
|
+
"\n".join(stopped_list),
|
166
|
+
"\n".join(vols_list),
|
167
|
+
"\n".join(eips_list),
|
168
|
+
"\n".join(alerts),
|
169
|
+
)
|
170
|
+
console.print(table)
|
171
|
+
console.print("[bold bright_cyan]Note: The dashboard only lists untagged EC2, RDS, Lambda, ELBv2.\n[/]")
|
172
|
+
|
173
|
+
if args.report_name: # Ensure report_name is provided for any export
|
174
|
+
if args.report_type:
|
175
|
+
for report_type in args.report_type:
|
176
|
+
if report_type == "csv":
|
177
|
+
csv_path = export_audit_report_to_csv(audit_data, args.report_name, args.dir)
|
178
|
+
if csv_path:
|
179
|
+
console.print(f"[bright_green]Successfully exported to CSV format: {csv_path}[/]")
|
180
|
+
elif report_type == "json":
|
181
|
+
json_path = export_audit_report_to_json(raw_audit_data, args.report_name, args.dir)
|
182
|
+
if json_path:
|
183
|
+
console.print(f"[bright_green]Successfully exported to JSON format: {json_path}[/]")
|
184
|
+
elif report_type == "pdf":
|
185
|
+
pdf_path = export_audit_report_to_pdf(audit_data, args.report_name, args.dir)
|
186
|
+
if pdf_path:
|
187
|
+
console.print(f"[bright_green]Successfully exported to PDF format: {pdf_path}[/]")
|
188
|
+
|
189
|
+
|
190
|
+
def _run_trend_analysis(profiles_to_use: List[str], args: argparse.Namespace) -> None:
|
191
|
+
"""Analyze and display cost trends."""
|
192
|
+
console.print("[bold bright_cyan]Analysing cost trends...[/]")
|
193
|
+
raw_trend_data = []
|
194
|
+
if args.combine:
|
195
|
+
account_profiles = defaultdict(list)
|
196
|
+
for profile in profiles_to_use:
|
197
|
+
try:
|
198
|
+
session = boto3.Session(profile_name=profile)
|
199
|
+
account_id = get_account_id(session)
|
200
|
+
if account_id:
|
201
|
+
account_profiles[account_id].append(profile)
|
202
|
+
except Exception as e:
|
203
|
+
console.print(f"[red]Error checking account ID for profile {profile}: {str(e)}[/]")
|
204
|
+
|
205
|
+
for account_id, profiles in account_profiles.items():
|
206
|
+
try:
|
207
|
+
primary_profile = profiles[0]
|
208
|
+
session = boto3.Session(profile_name=primary_profile)
|
209
|
+
cost_data = get_trend(session, args.tag)
|
210
|
+
trend_data = cost_data.get("monthly_costs")
|
211
|
+
|
212
|
+
if not trend_data:
|
213
|
+
console.print(f"[yellow]No trend data available for account {account_id}[/]")
|
214
|
+
continue
|
215
|
+
|
216
|
+
profile_list = ", ".join(profiles)
|
217
|
+
console.print(f"\n[bright_yellow]Account: {account_id} (Profiles: {profile_list})[/]")
|
218
|
+
raw_trend_data.append(cost_data)
|
219
|
+
create_trend_bars(trend_data)
|
220
|
+
except Exception as e:
|
221
|
+
console.print(f"[red]Error getting trend for account {account_id}: {str(e)}[/]")
|
222
|
+
|
223
|
+
else:
|
224
|
+
for profile in profiles_to_use:
|
225
|
+
try:
|
226
|
+
session = boto3.Session(profile_name=profile)
|
227
|
+
cost_data = get_trend(session, args.tag)
|
228
|
+
trend_data = cost_data.get("monthly_costs")
|
229
|
+
account_id = cost_data.get("account_id", "Unknown")
|
230
|
+
|
231
|
+
if not trend_data:
|
232
|
+
console.print(f"[yellow]No trend data available for profile {profile}[/]")
|
233
|
+
continue
|
234
|
+
|
235
|
+
console.print(f"\n[bright_yellow]Account: {account_id} (Profile: {profile})[/]")
|
236
|
+
raw_trend_data.append(cost_data)
|
237
|
+
create_trend_bars(trend_data)
|
238
|
+
except Exception as e:
|
239
|
+
console.print(f"[red]Error getting trend for profile {profile}: {str(e)}[/]")
|
240
|
+
|
241
|
+
if raw_trend_data and args.report_name and args.report_type:
|
242
|
+
if "json" in args.report_type:
|
243
|
+
json_path = export_trend_data_to_json(raw_trend_data, args.report_name, args.dir)
|
244
|
+
if json_path:
|
245
|
+
console.print(f"[bright_green]Successfully exported trend data to JSON format: {json_path}[/]")
|
246
|
+
|
247
|
+
|
248
|
+
def _get_display_table_period_info(profiles_to_use: List[str], time_range: Optional[int]) -> Tuple[str, str, str, str]:
|
249
|
+
"""Get period information for the display table."""
|
250
|
+
if profiles_to_use:
|
251
|
+
try:
|
252
|
+
sample_session = boto3.Session(profile_name=profiles_to_use[0])
|
253
|
+
sample_cost_data = get_cost_data(sample_session, time_range)
|
254
|
+
previous_period_name = sample_cost_data.get("previous_period_name", "Last Month Due")
|
255
|
+
current_period_name = sample_cost_data.get("current_period_name", "Current Month Cost")
|
256
|
+
previous_period_dates = (
|
257
|
+
f"{sample_cost_data['previous_period_start']} to {sample_cost_data['previous_period_end']}"
|
258
|
+
)
|
259
|
+
current_period_dates = (
|
260
|
+
f"{sample_cost_data['current_period_start']} to {sample_cost_data['current_period_end']}"
|
261
|
+
)
|
262
|
+
return (
|
263
|
+
previous_period_name,
|
264
|
+
current_period_name,
|
265
|
+
previous_period_dates,
|
266
|
+
current_period_dates,
|
267
|
+
)
|
268
|
+
except Exception:
|
269
|
+
pass # Fall through to default values
|
270
|
+
return "Last Month Due", "Current Month Cost", "N/A", "N/A"
|
271
|
+
|
272
|
+
|
273
|
+
def create_display_table(
|
274
|
+
previous_period_dates: str,
|
275
|
+
current_period_dates: str,
|
276
|
+
previous_period_name: str = "Last Month Due",
|
277
|
+
current_period_name: str = "Current Month Cost",
|
278
|
+
) -> Table:
|
279
|
+
"""Create and configure the display table with dynamic column names."""
|
280
|
+
return Table(
|
281
|
+
Column("AWS Account Profile", justify="center", vertical="middle"),
|
282
|
+
Column(
|
283
|
+
f"{previous_period_name}\n({previous_period_dates})",
|
284
|
+
justify="center",
|
285
|
+
vertical="middle",
|
286
|
+
),
|
287
|
+
Column(
|
288
|
+
f"{current_period_name}\n({current_period_dates})",
|
289
|
+
justify="center",
|
290
|
+
vertical="middle",
|
291
|
+
),
|
292
|
+
Column("Cost By Service", vertical="middle"),
|
293
|
+
Column("Budget Status", vertical="middle"),
|
294
|
+
Column("EC2 Instance Summary", justify="center", vertical="middle"),
|
295
|
+
title="AWS FinOps Dashboard",
|
296
|
+
caption="AWS FinOps Dashboard CLI",
|
297
|
+
box=box.ASCII_DOUBLE_HEAD,
|
298
|
+
show_lines=True,
|
299
|
+
style="bright_cyan",
|
300
|
+
)
|
301
|
+
|
302
|
+
|
303
|
+
def add_profile_to_table(table: Table, profile_data: ProfileData) -> None:
|
304
|
+
"""Add profile data to the display table."""
|
305
|
+
if profile_data["success"]:
|
306
|
+
percentage_change = profile_data.get("percent_change_in_total_cost")
|
307
|
+
change_text = ""
|
308
|
+
|
309
|
+
if percentage_change is not None:
|
310
|
+
if percentage_change > 0:
|
311
|
+
change_text = f"\n\n[bright_red]⬆ {percentage_change:.2f}%"
|
312
|
+
elif percentage_change < 0:
|
313
|
+
change_text = f"\n\n[bright_green]⬇ {abs(percentage_change):.2f}%"
|
314
|
+
elif percentage_change == 0:
|
315
|
+
change_text = "\n\n[bright_yellow]➡ 0.00%[/]"
|
316
|
+
|
317
|
+
current_month_with_change = f"[bold red]${profile_data['current_month']:.2f}[/]{change_text}"
|
318
|
+
|
319
|
+
table.add_row(
|
320
|
+
f"[bright_magenta]Profile: {profile_data['profile']}\nAccount: {profile_data['account_id']}[/]",
|
321
|
+
f"[bold red]${profile_data['last_month']:.2f}[/]",
|
322
|
+
current_month_with_change,
|
323
|
+
"[bright_green]" + "\n".join(profile_data["service_costs_formatted"]) + "[/]",
|
324
|
+
"[bright_yellow]" + "\n\n".join(profile_data["budget_info"]) + "[/]",
|
325
|
+
"\n".join(profile_data["ec2_summary_formatted"]),
|
326
|
+
)
|
327
|
+
else:
|
328
|
+
table.add_row(
|
329
|
+
f"[bright_magenta]{profile_data['profile']}[/]",
|
330
|
+
"[red]Error[/]",
|
331
|
+
"[red]Error[/]",
|
332
|
+
f"[red]Failed to process profile: {profile_data['error']}[/]",
|
333
|
+
"[red]N/A[/]",
|
334
|
+
"[red]N/A[/]",
|
335
|
+
)
|
336
|
+
|
337
|
+
|
338
|
+
def _generate_dashboard_data(
|
339
|
+
profiles_to_use: List[str],
|
340
|
+
user_regions: Optional[List[str]],
|
341
|
+
time_range: Optional[int],
|
342
|
+
args: argparse.Namespace,
|
343
|
+
table: Table,
|
344
|
+
) -> List[ProfileData]:
|
345
|
+
"""Fetch, process, and prepare the main dashboard data."""
|
346
|
+
export_data: List[ProfileData] = []
|
347
|
+
if args.combine:
|
348
|
+
account_profiles = defaultdict(list)
|
349
|
+
for profile in profiles_to_use:
|
350
|
+
try:
|
351
|
+
session = boto3.Session(profile_name=profile)
|
352
|
+
current_account_id = get_account_id(session) # Renamed to avoid conflict
|
353
|
+
if current_account_id:
|
354
|
+
account_profiles[current_account_id].append(profile)
|
355
|
+
else:
|
356
|
+
console.log(f"[yellow]Could not determine account ID for profile {profile}[/]")
|
357
|
+
except Exception as e:
|
358
|
+
console.log(f"[bold red]Error checking account ID for profile {profile}: {str(e)}[/]")
|
359
|
+
|
360
|
+
for account_id_key, profiles_list in track( # Renamed loop variables
|
361
|
+
account_profiles.items(), description="[bright_cyan]Fetching cost data..."
|
362
|
+
):
|
363
|
+
# account_id_key here is known to be a string because it's a key from account_profiles
|
364
|
+
# where None keys were filtered out when populating it.
|
365
|
+
if len(profiles_list) > 1:
|
366
|
+
profile_data = process_combined_profiles(
|
367
|
+
account_id_key, profiles_list, user_regions, time_range, args.tag
|
368
|
+
)
|
369
|
+
else:
|
370
|
+
profile_data = process_single_profile(profiles_list[0], user_regions, time_range, args.tag)
|
371
|
+
export_data.append(profile_data)
|
372
|
+
add_profile_to_table(table, profile_data)
|
373
|
+
else:
|
374
|
+
for profile in track(profiles_to_use, description="[bright_cyan]Fetching cost data..."):
|
375
|
+
profile_data = process_single_profile(profile, user_regions, time_range, args.tag)
|
376
|
+
export_data.append(profile_data)
|
377
|
+
add_profile_to_table(table, profile_data)
|
378
|
+
return export_data
|
379
|
+
|
380
|
+
|
381
|
+
def _export_dashboard_reports(
|
382
|
+
export_data: List[ProfileData],
|
383
|
+
args: argparse.Namespace,
|
384
|
+
previous_period_dates: str,
|
385
|
+
current_period_dates: str,
|
386
|
+
) -> None:
|
387
|
+
"""Export dashboard data to specified formats."""
|
388
|
+
if args.report_name and args.report_type:
|
389
|
+
for report_type in args.report_type:
|
390
|
+
if report_type == "csv":
|
391
|
+
csv_path = export_to_csv(
|
392
|
+
export_data,
|
393
|
+
args.report_name,
|
394
|
+
args.dir,
|
395
|
+
previous_period_dates=previous_period_dates,
|
396
|
+
current_period_dates=current_period_dates,
|
397
|
+
)
|
398
|
+
if csv_path:
|
399
|
+
console.print(f"[bright_green]Successfully exported to CSV format: {csv_path}[/]")
|
400
|
+
elif report_type == "json":
|
401
|
+
json_path = export_to_json(export_data, args.report_name, args.dir)
|
402
|
+
if json_path:
|
403
|
+
console.print(f"[bright_green]Successfully exported to JSON format: {json_path}[/]")
|
404
|
+
elif report_type == "pdf":
|
405
|
+
pdf_path = export_cost_dashboard_to_pdf(
|
406
|
+
export_data,
|
407
|
+
args.report_name,
|
408
|
+
args.dir,
|
409
|
+
previous_period_dates=previous_period_dates,
|
410
|
+
current_period_dates=current_period_dates,
|
411
|
+
)
|
412
|
+
if pdf_path:
|
413
|
+
console.print(f"[bright_green]Successfully exported to PDF format: {pdf_path}[/]")
|
414
|
+
|
415
|
+
|
416
|
+
def run_dashboard(args: argparse.Namespace) -> int:
|
417
|
+
"""Main function to run the AWS FinOps dashboard."""
|
418
|
+
with Status("[bright_cyan]Initialising...", spinner="aesthetic", speed=0.4):
|
419
|
+
profiles_to_use, user_regions, time_range = _initialize_profiles(args)
|
420
|
+
|
421
|
+
if args.audit:
|
422
|
+
_run_audit_report(profiles_to_use, args)
|
423
|
+
return 0
|
424
|
+
|
425
|
+
if args.trend:
|
426
|
+
_run_trend_analysis(profiles_to_use, args)
|
427
|
+
return 0
|
428
|
+
|
429
|
+
with Status("[bright_cyan]Initialising dashboard...", spinner="aesthetic", speed=0.4):
|
430
|
+
(
|
431
|
+
previous_period_name,
|
432
|
+
current_period_name,
|
433
|
+
previous_period_dates,
|
434
|
+
current_period_dates,
|
435
|
+
) = _get_display_table_period_info(profiles_to_use, time_range)
|
436
|
+
|
437
|
+
table = create_display_table(
|
438
|
+
previous_period_dates,
|
439
|
+
current_period_dates,
|
440
|
+
previous_period_name,
|
441
|
+
current_period_name,
|
442
|
+
)
|
443
|
+
|
444
|
+
export_data = _generate_dashboard_data(profiles_to_use, user_regions, time_range, args, table)
|
445
|
+
console.print(table)
|
446
|
+
_export_dashboard_reports(export_data, args, previous_period_dates, current_period_dates)
|
447
|
+
|
448
|
+
return 0
|