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.
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 +7 -7
  199. runbooks/security_baseline/report_template_jp.html +7 -7
  200. runbooks/security_baseline/report_template_kr.html +12 -12
  201. runbooks/security_baseline/report_template_vn.html +7 -7
  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.5.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.5.dist-info/METADATA +0 -439
  219. runbooks-0.2.5.dist-info/RECORD +0 -61
  220. runbooks-0.2.5.dist-info/entry_points.txt +0 -3
  221. 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