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.
Files changed (221) hide show
  1. conftest.py +26 -0
  2. jupyter-agent/.env.template +2 -0
  3. jupyter-agent/.gitattributes +35 -0
  4. jupyter-agent/README.md +16 -0
  5. jupyter-agent/app.py +256 -0
  6. jupyter-agent/cloudops-agent.png +0 -0
  7. jupyter-agent/ds-system-prompt.txt +154 -0
  8. jupyter-agent/jupyter-agent.png +0 -0
  9. jupyter-agent/llama3_template.jinja +123 -0
  10. jupyter-agent/requirements.txt +9 -0
  11. jupyter-agent/utils.py +409 -0
  12. runbooks/__init__.py +71 -3
  13. runbooks/__main__.py +13 -0
  14. runbooks/aws/ec2_describe_instances.py +1 -1
  15. runbooks/aws/ec2_run_instances.py +8 -2
  16. runbooks/aws/ec2_start_stop_instances.py +17 -4
  17. runbooks/aws/ec2_unused_volumes.py +5 -1
  18. runbooks/aws/s3_create_bucket.py +4 -2
  19. runbooks/aws/s3_list_objects.py +6 -1
  20. runbooks/aws/tagging_lambda_handler.py +13 -2
  21. runbooks/aws/tags.json +12 -0
  22. runbooks/base.py +353 -0
  23. runbooks/cfat/README.md +49 -0
  24. runbooks/cfat/__init__.py +74 -0
  25. runbooks/cfat/app.ts +644 -0
  26. runbooks/cfat/assessment/__init__.py +40 -0
  27. runbooks/cfat/assessment/asana-import.csv +39 -0
  28. runbooks/cfat/assessment/cfat-checks.csv +31 -0
  29. runbooks/cfat/assessment/cfat.txt +520 -0
  30. runbooks/cfat/assessment/collectors.py +200 -0
  31. runbooks/cfat/assessment/jira-import.csv +39 -0
  32. runbooks/cfat/assessment/runner.py +387 -0
  33. runbooks/cfat/assessment/validators.py +290 -0
  34. runbooks/cfat/cli.py +103 -0
  35. runbooks/cfat/docs/asana-import.csv +24 -0
  36. runbooks/cfat/docs/cfat-checks.csv +31 -0
  37. runbooks/cfat/docs/cfat.txt +335 -0
  38. runbooks/cfat/docs/checks-output.png +0 -0
  39. runbooks/cfat/docs/cloudshell-console-run.png +0 -0
  40. runbooks/cfat/docs/cloudshell-download.png +0 -0
  41. runbooks/cfat/docs/cloudshell-output.png +0 -0
  42. runbooks/cfat/docs/downloadfile.png +0 -0
  43. runbooks/cfat/docs/jira-import.csv +24 -0
  44. runbooks/cfat/docs/open-cloudshell.png +0 -0
  45. runbooks/cfat/docs/report-header.png +0 -0
  46. runbooks/cfat/models.py +1026 -0
  47. runbooks/cfat/package-lock.json +5116 -0
  48. runbooks/cfat/package.json +38 -0
  49. runbooks/cfat/report.py +496 -0
  50. runbooks/cfat/reporting/__init__.py +46 -0
  51. runbooks/cfat/reporting/exporters.py +337 -0
  52. runbooks/cfat/reporting/formatters.py +496 -0
  53. runbooks/cfat/reporting/templates.py +135 -0
  54. runbooks/cfat/run-assessment.sh +23 -0
  55. runbooks/cfat/runner.py +69 -0
  56. runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
  57. runbooks/cfat/src/actions/check-config-existence.ts +37 -0
  58. runbooks/cfat/src/actions/check-control-tower.ts +37 -0
  59. runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
  60. runbooks/cfat/src/actions/check-iam-users.ts +50 -0
  61. runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
  62. runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
  63. runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
  64. runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
  65. runbooks/cfat/src/actions/create-backlog.ts +372 -0
  66. runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
  67. runbooks/cfat/src/actions/create-report.ts +616 -0
  68. runbooks/cfat/src/actions/define-account-type.ts +51 -0
  69. runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
  70. runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
  71. runbooks/cfat/src/actions/get-idc-info.ts +34 -0
  72. runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
  73. runbooks/cfat/src/actions/get-org-details.ts +35 -0
  74. runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
  75. runbooks/cfat/src/actions/get-org-ous.ts +35 -0
  76. runbooks/cfat/src/actions/get-regions.ts +22 -0
  77. runbooks/cfat/src/actions/zip-assessment.ts +27 -0
  78. runbooks/cfat/src/types/index.d.ts +147 -0
  79. runbooks/cfat/tests/__init__.py +141 -0
  80. runbooks/cfat/tests/test_cli.py +340 -0
  81. runbooks/cfat/tests/test_integration.py +290 -0
  82. runbooks/cfat/tests/test_models.py +505 -0
  83. runbooks/cfat/tests/test_reporting.py +354 -0
  84. runbooks/cfat/tsconfig.json +16 -0
  85. runbooks/cfat/webpack.config.cjs +27 -0
  86. runbooks/config.py +260 -0
  87. runbooks/finops/__init__.py +88 -0
  88. runbooks/finops/aws_client.py +245 -0
  89. runbooks/finops/cli.py +151 -0
  90. runbooks/finops/cost_processor.py +410 -0
  91. runbooks/finops/dashboard_runner.py +448 -0
  92. runbooks/finops/helpers.py +355 -0
  93. runbooks/finops/main.py +14 -0
  94. runbooks/finops/profile_processor.py +174 -0
  95. runbooks/finops/types.py +66 -0
  96. runbooks/finops/visualisations.py +80 -0
  97. runbooks/inventory/.gitignore +354 -0
  98. runbooks/inventory/ArgumentsClass.py +261 -0
  99. runbooks/inventory/Inventory_Modules.py +6130 -0
  100. runbooks/inventory/LandingZone/delete_lz.py +1075 -0
  101. runbooks/inventory/README.md +1320 -0
  102. runbooks/inventory/__init__.py +62 -0
  103. runbooks/inventory/account_class.py +532 -0
  104. runbooks/inventory/all_my_instances_wrapper.py +123 -0
  105. runbooks/inventory/aws_decorators.py +201 -0
  106. runbooks/inventory/cfn_move_stack_instances.py +1526 -0
  107. runbooks/inventory/check_cloudtrail_compliance.py +614 -0
  108. runbooks/inventory/check_controltower_readiness.py +1107 -0
  109. runbooks/inventory/check_landingzone_readiness.py +711 -0
  110. runbooks/inventory/cloudtrail.md +727 -0
  111. runbooks/inventory/collectors/__init__.py +20 -0
  112. runbooks/inventory/collectors/aws_compute.py +518 -0
  113. runbooks/inventory/collectors/aws_networking.py +275 -0
  114. runbooks/inventory/collectors/base.py +222 -0
  115. runbooks/inventory/core/__init__.py +19 -0
  116. runbooks/inventory/core/collector.py +303 -0
  117. runbooks/inventory/core/formatter.py +296 -0
  118. runbooks/inventory/delete_s3_buckets_objects.py +169 -0
  119. runbooks/inventory/discovery.md +81 -0
  120. runbooks/inventory/draw_org_structure.py +748 -0
  121. runbooks/inventory/ec2_vpc_utils.py +341 -0
  122. runbooks/inventory/find_cfn_drift_detection.py +272 -0
  123. runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
  124. runbooks/inventory/find_cfn_stackset_drift.py +733 -0
  125. runbooks/inventory/find_ec2_security_groups.py +669 -0
  126. runbooks/inventory/find_landingzone_versions.py +201 -0
  127. runbooks/inventory/find_vpc_flow_logs.py +1221 -0
  128. runbooks/inventory/inventory.sh +659 -0
  129. runbooks/inventory/list_cfn_stacks.py +558 -0
  130. runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
  131. runbooks/inventory/list_cfn_stackset_operations.py +734 -0
  132. runbooks/inventory/list_cfn_stacksets.py +453 -0
  133. runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
  134. runbooks/inventory/list_ds_directories.py +354 -0
  135. runbooks/inventory/list_ec2_availability_zones.py +286 -0
  136. runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
  137. runbooks/inventory/list_ec2_instances.py +425 -0
  138. runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
  139. runbooks/inventory/list_elbs_load_balancers.py +411 -0
  140. runbooks/inventory/list_enis_network_interfaces.py +526 -0
  141. runbooks/inventory/list_guardduty_detectors.py +568 -0
  142. runbooks/inventory/list_iam_policies.py +404 -0
  143. runbooks/inventory/list_iam_roles.py +518 -0
  144. runbooks/inventory/list_iam_saml_providers.py +359 -0
  145. runbooks/inventory/list_lambda_functions.py +882 -0
  146. runbooks/inventory/list_org_accounts.py +446 -0
  147. runbooks/inventory/list_org_accounts_users.py +354 -0
  148. runbooks/inventory/list_rds_db_instances.py +406 -0
  149. runbooks/inventory/list_route53_hosted_zones.py +318 -0
  150. runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
  151. runbooks/inventory/list_sns_topics.py +360 -0
  152. runbooks/inventory/list_ssm_parameters.py +402 -0
  153. runbooks/inventory/list_vpc_subnets.py +433 -0
  154. runbooks/inventory/list_vpcs.py +422 -0
  155. runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
  156. runbooks/inventory/models/__init__.py +24 -0
  157. runbooks/inventory/models/account.py +192 -0
  158. runbooks/inventory/models/inventory.py +309 -0
  159. runbooks/inventory/models/resource.py +247 -0
  160. runbooks/inventory/recover_cfn_stack_ids.py +205 -0
  161. runbooks/inventory/requirements.txt +12 -0
  162. runbooks/inventory/run_on_multi_accounts.py +211 -0
  163. runbooks/inventory/tests/common_test_data.py +3661 -0
  164. runbooks/inventory/tests/common_test_functions.py +204 -0
  165. runbooks/inventory/tests/script_test_data.py +0 -0
  166. runbooks/inventory/tests/setup.py +24 -0
  167. runbooks/inventory/tests/src.py +18 -0
  168. runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
  169. runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
  170. runbooks/inventory/tests/test_inventory_modules.py +55 -0
  171. runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
  172. runbooks/inventory/tests/test_moto_integration_example.py +273 -0
  173. runbooks/inventory/tests/test_org_list_accounts.py +49 -0
  174. runbooks/inventory/update_aws_actions.py +173 -0
  175. runbooks/inventory/update_cfn_stacksets.py +1215 -0
  176. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
  177. runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
  178. runbooks/inventory/update_s3_public_access_block.py +539 -0
  179. runbooks/inventory/utils/__init__.py +23 -0
  180. runbooks/inventory/utils/aws_helpers.py +510 -0
  181. runbooks/inventory/utils/threading_utils.py +493 -0
  182. runbooks/inventory/utils/validation.py +682 -0
  183. runbooks/inventory/verify_ec2_security_groups.py +1430 -0
  184. runbooks/main.py +785 -0
  185. runbooks/organizations/__init__.py +12 -0
  186. runbooks/organizations/manager.py +374 -0
  187. runbooks/security_baseline/README.md +324 -0
  188. runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
  189. runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
  190. runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
  191. runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
  192. runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
  193. runbooks/security_baseline/checklist/root_access_key.py +6 -1
  194. runbooks/security_baseline/config-origin.json +1 -1
  195. runbooks/security_baseline/config.json +1 -1
  196. runbooks/security_baseline/permission.json +1 -1
  197. runbooks/security_baseline/report_generator.py +10 -2
  198. runbooks/security_baseline/report_template_en.html +8 -8
  199. runbooks/security_baseline/report_template_jp.html +8 -8
  200. runbooks/security_baseline/report_template_kr.html +13 -13
  201. runbooks/security_baseline/report_template_vn.html +8 -8
  202. runbooks/security_baseline/requirements.txt +7 -0
  203. runbooks/security_baseline/run_script.py +8 -2
  204. runbooks/security_baseline/security_baseline_tester.py +10 -2
  205. runbooks/security_baseline/utils/common.py +5 -1
  206. runbooks/utils/__init__.py +204 -0
  207. runbooks-0.6.1.dist-info/METADATA +373 -0
  208. runbooks-0.6.1.dist-info/RECORD +237 -0
  209. {runbooks-0.2.3.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
  210. runbooks-0.6.1.dist-info/entry_points.txt +7 -0
  211. runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
  212. runbooks-0.6.1.dist-info/top_level.txt +3 -0
  213. runbooks/python101/calculator.py +0 -34
  214. runbooks/python101/config.py +0 -1
  215. runbooks/python101/exceptions.py +0 -16
  216. runbooks/python101/file_manager.py +0 -218
  217. runbooks/python101/toolkit.py +0 -153
  218. runbooks-0.2.3.dist-info/METADATA +0 -435
  219. runbooks-0.2.3.dist-info/RECORD +0 -61
  220. runbooks-0.2.3.dist-info/entry_points.txt +0 -3
  221. 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