runbooks 0.2.5__py3-none-any.whl → 0.7.0__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 (249) hide show
  1. conftest.py +26 -0
  2. jupyter-agent/.env +2 -0
  3. jupyter-agent/.env.template +2 -0
  4. jupyter-agent/.gitattributes +35 -0
  5. jupyter-agent/.gradio/certificate.pem +31 -0
  6. jupyter-agent/README.md +16 -0
  7. jupyter-agent/__main__.log +8 -0
  8. jupyter-agent/app.py +256 -0
  9. jupyter-agent/cloudops-agent.png +0 -0
  10. jupyter-agent/ds-system-prompt.txt +154 -0
  11. jupyter-agent/jupyter-agent.png +0 -0
  12. jupyter-agent/llama3_template.jinja +123 -0
  13. jupyter-agent/requirements.txt +9 -0
  14. jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +68 -0
  15. jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +91 -0
  16. jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +91 -0
  17. jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +57 -0
  18. jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +53 -0
  19. jupyter-agent/tmp/jupyter-agent.ipynb +27 -0
  20. jupyter-agent/utils.py +409 -0
  21. runbooks/__init__.py +71 -3
  22. runbooks/__main__.py +13 -0
  23. runbooks/aws/ec2_describe_instances.py +1 -1
  24. runbooks/aws/ec2_run_instances.py +8 -2
  25. runbooks/aws/ec2_start_stop_instances.py +17 -4
  26. runbooks/aws/ec2_unused_volumes.py +5 -1
  27. runbooks/aws/s3_create_bucket.py +4 -2
  28. runbooks/aws/s3_list_objects.py +6 -1
  29. runbooks/aws/tagging_lambda_handler.py +13 -2
  30. runbooks/aws/tags.json +12 -0
  31. runbooks/base.py +353 -0
  32. runbooks/cfat/README.md +49 -0
  33. runbooks/cfat/__init__.py +74 -0
  34. runbooks/cfat/app.ts +644 -0
  35. runbooks/cfat/assessment/__init__.py +40 -0
  36. runbooks/cfat/assessment/asana-import.csv +39 -0
  37. runbooks/cfat/assessment/cfat-checks.csv +31 -0
  38. runbooks/cfat/assessment/cfat.txt +520 -0
  39. runbooks/cfat/assessment/collectors.py +200 -0
  40. runbooks/cfat/assessment/jira-import.csv +39 -0
  41. runbooks/cfat/assessment/runner.py +387 -0
  42. runbooks/cfat/assessment/validators.py +290 -0
  43. runbooks/cfat/cli.py +103 -0
  44. runbooks/cfat/docs/asana-import.csv +24 -0
  45. runbooks/cfat/docs/cfat-checks.csv +31 -0
  46. runbooks/cfat/docs/cfat.txt +335 -0
  47. runbooks/cfat/docs/checks-output.png +0 -0
  48. runbooks/cfat/docs/cloudshell-console-run.png +0 -0
  49. runbooks/cfat/docs/cloudshell-download.png +0 -0
  50. runbooks/cfat/docs/cloudshell-output.png +0 -0
  51. runbooks/cfat/docs/downloadfile.png +0 -0
  52. runbooks/cfat/docs/jira-import.csv +24 -0
  53. runbooks/cfat/docs/open-cloudshell.png +0 -0
  54. runbooks/cfat/docs/report-header.png +0 -0
  55. runbooks/cfat/models.py +1026 -0
  56. runbooks/cfat/package-lock.json +5116 -0
  57. runbooks/cfat/package.json +38 -0
  58. runbooks/cfat/report.py +496 -0
  59. runbooks/cfat/reporting/__init__.py +46 -0
  60. runbooks/cfat/reporting/exporters.py +337 -0
  61. runbooks/cfat/reporting/formatters.py +496 -0
  62. runbooks/cfat/reporting/templates.py +135 -0
  63. runbooks/cfat/run-assessment.sh +23 -0
  64. runbooks/cfat/runner.py +69 -0
  65. runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
  66. runbooks/cfat/src/actions/check-config-existence.ts +37 -0
  67. runbooks/cfat/src/actions/check-control-tower.ts +37 -0
  68. runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
  69. runbooks/cfat/src/actions/check-iam-users.ts +50 -0
  70. runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
  71. runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
  72. runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
  73. runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
  74. runbooks/cfat/src/actions/create-backlog.ts +372 -0
  75. runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
  76. runbooks/cfat/src/actions/create-report.ts +616 -0
  77. runbooks/cfat/src/actions/define-account-type.ts +51 -0
  78. runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
  79. runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
  80. runbooks/cfat/src/actions/get-idc-info.ts +34 -0
  81. runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
  82. runbooks/cfat/src/actions/get-org-details.ts +35 -0
  83. runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
  84. runbooks/cfat/src/actions/get-org-ous.ts +35 -0
  85. runbooks/cfat/src/actions/get-regions.ts +22 -0
  86. runbooks/cfat/src/actions/zip-assessment.ts +27 -0
  87. runbooks/cfat/src/types/index.d.ts +147 -0
  88. runbooks/cfat/tests/__init__.py +141 -0
  89. runbooks/cfat/tests/test_cli.py +340 -0
  90. runbooks/cfat/tests/test_integration.py +290 -0
  91. runbooks/cfat/tests/test_models.py +505 -0
  92. runbooks/cfat/tests/test_reporting.py +354 -0
  93. runbooks/cfat/tsconfig.json +16 -0
  94. runbooks/cfat/webpack.config.cjs +27 -0
  95. runbooks/config.py +260 -0
  96. runbooks/finops/README.md +337 -0
  97. runbooks/finops/__init__.py +86 -0
  98. runbooks/finops/aws_client.py +245 -0
  99. runbooks/finops/cli.py +151 -0
  100. runbooks/finops/cost_processor.py +410 -0
  101. runbooks/finops/dashboard_runner.py +448 -0
  102. runbooks/finops/helpers.py +355 -0
  103. runbooks/finops/main.py +14 -0
  104. runbooks/finops/profile_processor.py +174 -0
  105. runbooks/finops/types.py +66 -0
  106. runbooks/finops/visualisations.py +80 -0
  107. runbooks/inventory/.gitignore +354 -0
  108. runbooks/inventory/ArgumentsClass.py +261 -0
  109. runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +619 -0
  110. runbooks/inventory/Inventory_Modules.py +6130 -0
  111. runbooks/inventory/LandingZone/delete_lz.py +1075 -0
  112. runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +738 -0
  113. runbooks/inventory/README.md +1320 -0
  114. runbooks/inventory/__init__.py +62 -0
  115. runbooks/inventory/account_class.py +532 -0
  116. runbooks/inventory/all_my_instances_wrapper.py +123 -0
  117. runbooks/inventory/aws_decorators.py +201 -0
  118. runbooks/inventory/aws_organization.png +0 -0
  119. runbooks/inventory/cfn_move_stack_instances.py +1526 -0
  120. runbooks/inventory/check_cloudtrail_compliance.py +614 -0
  121. runbooks/inventory/check_controltower_readiness.py +1107 -0
  122. runbooks/inventory/check_landingzone_readiness.py +711 -0
  123. runbooks/inventory/cloudtrail.md +727 -0
  124. runbooks/inventory/collectors/__init__.py +20 -0
  125. runbooks/inventory/collectors/aws_compute.py +518 -0
  126. runbooks/inventory/collectors/aws_networking.py +275 -0
  127. runbooks/inventory/collectors/base.py +222 -0
  128. runbooks/inventory/core/__init__.py +19 -0
  129. runbooks/inventory/core/collector.py +303 -0
  130. runbooks/inventory/core/formatter.py +296 -0
  131. runbooks/inventory/delete_s3_buckets_objects.py +169 -0
  132. runbooks/inventory/discovery.md +81 -0
  133. runbooks/inventory/draw_org_structure.py +748 -0
  134. runbooks/inventory/ec2_vpc_utils.py +341 -0
  135. runbooks/inventory/find_cfn_drift_detection.py +272 -0
  136. runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
  137. runbooks/inventory/find_cfn_stackset_drift.py +733 -0
  138. runbooks/inventory/find_ec2_security_groups.py +669 -0
  139. runbooks/inventory/find_landingzone_versions.py +201 -0
  140. runbooks/inventory/find_vpc_flow_logs.py +1221 -0
  141. runbooks/inventory/inventory.sh +659 -0
  142. runbooks/inventory/list_cfn_stacks.py +558 -0
  143. runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
  144. runbooks/inventory/list_cfn_stackset_operations.py +734 -0
  145. runbooks/inventory/list_cfn_stacksets.py +453 -0
  146. runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
  147. runbooks/inventory/list_ds_directories.py +354 -0
  148. runbooks/inventory/list_ec2_availability_zones.py +286 -0
  149. runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
  150. runbooks/inventory/list_ec2_instances.py +425 -0
  151. runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
  152. runbooks/inventory/list_elbs_load_balancers.py +411 -0
  153. runbooks/inventory/list_enis_network_interfaces.py +526 -0
  154. runbooks/inventory/list_guardduty_detectors.py +568 -0
  155. runbooks/inventory/list_iam_policies.py +404 -0
  156. runbooks/inventory/list_iam_roles.py +518 -0
  157. runbooks/inventory/list_iam_saml_providers.py +359 -0
  158. runbooks/inventory/list_lambda_functions.py +882 -0
  159. runbooks/inventory/list_org_accounts.py +446 -0
  160. runbooks/inventory/list_org_accounts_users.py +354 -0
  161. runbooks/inventory/list_rds_db_instances.py +406 -0
  162. runbooks/inventory/list_route53_hosted_zones.py +318 -0
  163. runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
  164. runbooks/inventory/list_sns_topics.py +360 -0
  165. runbooks/inventory/list_ssm_parameters.py +402 -0
  166. runbooks/inventory/list_vpc_subnets.py +433 -0
  167. runbooks/inventory/list_vpcs.py +422 -0
  168. runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
  169. runbooks/inventory/models/__init__.py +24 -0
  170. runbooks/inventory/models/account.py +192 -0
  171. runbooks/inventory/models/inventory.py +309 -0
  172. runbooks/inventory/models/resource.py +247 -0
  173. runbooks/inventory/recover_cfn_stack_ids.py +205 -0
  174. runbooks/inventory/requirements.txt +12 -0
  175. runbooks/inventory/run_on_multi_accounts.py +211 -0
  176. runbooks/inventory/tests/common_test_data.py +3661 -0
  177. runbooks/inventory/tests/common_test_functions.py +204 -0
  178. runbooks/inventory/tests/setup.py +24 -0
  179. runbooks/inventory/tests/src.py +18 -0
  180. runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
  181. runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
  182. runbooks/inventory/tests/test_inventory_modules.py +55 -0
  183. runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
  184. runbooks/inventory/tests/test_moto_integration_example.py +273 -0
  185. runbooks/inventory/tests/test_org_list_accounts.py +49 -0
  186. runbooks/inventory/update_aws_actions.py +173 -0
  187. runbooks/inventory/update_cfn_stacksets.py +1215 -0
  188. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
  189. runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
  190. runbooks/inventory/update_s3_public_access_block.py +539 -0
  191. runbooks/inventory/utils/__init__.py +23 -0
  192. runbooks/inventory/utils/aws_helpers.py +510 -0
  193. runbooks/inventory/utils/threading_utils.py +493 -0
  194. runbooks/inventory/utils/validation.py +682 -0
  195. runbooks/inventory/verify_ec2_security_groups.py +1430 -0
  196. runbooks/main.py +1004 -0
  197. runbooks/organizations/__init__.py +12 -0
  198. runbooks/organizations/manager.py +374 -0
  199. runbooks/security/README.md +447 -0
  200. runbooks/security/__init__.py +71 -0
  201. runbooks/{security_baseline → security}/checklist/alternate_contacts.py +8 -1
  202. runbooks/{security_baseline → security}/checklist/bucket_public_access.py +4 -1
  203. runbooks/{security_baseline → security}/checklist/cloudwatch_alarm_configuration.py +9 -2
  204. runbooks/{security_baseline → security}/checklist/guardduty_enabled.py +9 -2
  205. runbooks/{security_baseline → security}/checklist/multi_region_instance_usage.py +5 -1
  206. runbooks/{security_baseline → security}/checklist/root_access_key.py +6 -1
  207. runbooks/{security_baseline → security}/config-origin.json +1 -1
  208. runbooks/{security_baseline → security}/config.json +1 -1
  209. runbooks/{security_baseline → security}/permission.json +1 -1
  210. runbooks/{security_baseline → security}/report_generator.py +10 -2
  211. runbooks/{security_baseline → security}/report_template_en.html +7 -7
  212. runbooks/{security_baseline → security}/report_template_jp.html +7 -7
  213. runbooks/{security_baseline → security}/report_template_kr.html +12 -12
  214. runbooks/{security_baseline → security}/report_template_vn.html +7 -7
  215. runbooks/{security_baseline → security}/run_script.py +8 -2
  216. runbooks/{security_baseline → security}/security_baseline_tester.py +12 -4
  217. runbooks/{security_baseline → security}/utils/common.py +5 -1
  218. runbooks/utils/__init__.py +204 -0
  219. runbooks-0.7.0.dist-info/METADATA +375 -0
  220. runbooks-0.7.0.dist-info/RECORD +249 -0
  221. {runbooks-0.2.5.dist-info → runbooks-0.7.0.dist-info}/WHEEL +1 -1
  222. runbooks-0.7.0.dist-info/entry_points.txt +7 -0
  223. runbooks-0.7.0.dist-info/licenses/LICENSE +201 -0
  224. runbooks-0.7.0.dist-info/top_level.txt +3 -0
  225. runbooks/python101/calculator.py +0 -34
  226. runbooks/python101/config.py +0 -1
  227. runbooks/python101/exceptions.py +0 -16
  228. runbooks/python101/file_manager.py +0 -218
  229. runbooks/python101/toolkit.py +0 -153
  230. runbooks-0.2.5.dist-info/METADATA +0 -439
  231. runbooks-0.2.5.dist-info/RECORD +0 -61
  232. runbooks-0.2.5.dist-info/entry_points.txt +0 -3
  233. runbooks-0.2.5.dist-info/top_level.txt +0 -1
  234. /runbooks/{security_baseline/__init__.py → inventory/tests/script_test_data.py} +0 -0
  235. /runbooks/{security_baseline → security}/checklist/__init__.py +0 -0
  236. /runbooks/{security_baseline → security}/checklist/account_level_bucket_public_access.py +0 -0
  237. /runbooks/{security_baseline → security}/checklist/direct_attached_policy.py +0 -0
  238. /runbooks/{security_baseline → security}/checklist/iam_password_policy.py +0 -0
  239. /runbooks/{security_baseline → security}/checklist/iam_user_mfa.py +0 -0
  240. /runbooks/{security_baseline → security}/checklist/multi_region_trail.py +0 -0
  241. /runbooks/{security_baseline → security}/checklist/root_mfa.py +0 -0
  242. /runbooks/{security_baseline → security}/checklist/root_usage.py +0 -0
  243. /runbooks/{security_baseline → security}/checklist/trail_enabled.py +0 -0
  244. /runbooks/{security_baseline → security}/checklist/trusted_advisor.py +0 -0
  245. /runbooks/{security_baseline → security}/utils/__init__.py +0 -0
  246. /runbooks/{security_baseline → security}/utils/enums.py +0 -0
  247. /runbooks/{security_baseline → security}/utils/language.py +0 -0
  248. /runbooks/{security_baseline → security}/utils/level_const.py +0 -0
  249. /runbooks/{security_baseline → security}/utils/permission_list.py +0 -0
runbooks/finops/cli.py ADDED
@@ -0,0 +1,151 @@
1
+ import argparse
2
+ import sys
3
+ from typing import Any, Dict, Optional
4
+
5
+ import requests
6
+ from packaging import version
7
+ from rich.console import Console
8
+
9
+ from runbooks.finops.helpers import load_config_file
10
+
11
+ console = Console()
12
+
13
+ __version__ = "0.7.0"
14
+
15
+
16
+ def welcome_banner() -> None:
17
+ banner = rf"""
18
+ [bold red]
19
+ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$$$ /$$ /$$$$$$
20
+ /$$__ $$| $$ /$ | $$ /$$__ $$ | $$_____/|__/ /$$__ $$
21
+ | $$ \ $$| $$ /$$$| $$| $$ \__/ | $$ /$$ /$$$$$$$ | $$ \ $$ /$$$$$$ /$$$$$$$
22
+ | $$$$$$$$| $$/$$ $$ $$| $$$$$$ | $$$$$ | $$| $$__ $$| $$ | $$ /$$__ $$ /$$_____/
23
+ | $$__ $$| $$$$_ $$$$ \____ $$ | $$__/ | $$| $$ \ $$| $$ | $$| $$ \ $$| $$$$$$
24
+ | $$ | $$| $$$/ \ $$$ /$$ \ $$ | $$ | $$| $$ | $$| $$ | $$| $$ | $$ \____ $$
25
+ | $$ | $$| $$/ \ $$| $$$$$$/ | $$ | $$| $$ | $$| $$$$$$/| $$$$$$$/ /$$$$$$$/
26
+ |__/ |__/|__/ \__/ \______/ |__/ |__/|__/ |__/ \______/ | $$____/ |_______/
27
+ | $$
28
+ | $$
29
+ |__/
30
+ [/]
31
+ [bold bright_blue]AWS FinOps Dashboard CLI (v{__version__})[/]
32
+ """
33
+ console.print(banner)
34
+
35
+
36
+ def check_latest_version() -> None:
37
+ """Check for the latest version of the AWS FinOps Dashboard (CLI)."""
38
+ try:
39
+ response = requests.get("https://pypi.org/pypi/aws-finops-dashboard/json", timeout=3)
40
+ latest = response.json()["info"]["version"]
41
+ if version.parse(latest) > version.parse(__version__):
42
+ console.print(f"[bold red]A new version of AWS FinOps Dashboard is available: {latest}[/]")
43
+ console.print(
44
+ "[bold bright_yellow]Please update using:\npipx upgrade aws-finops-dashboard\nor\npip install --upgrade aws-finops-dashboard\n[/]"
45
+ )
46
+ except Exception:
47
+ pass
48
+
49
+
50
+ def main() -> int:
51
+ """Command-line interface entry point."""
52
+ welcome_banner()
53
+ check_latest_version()
54
+ from runbooks.finops.dashboard_runner import run_dashboard
55
+
56
+ # Create the parser instance to be accessible for get_default
57
+ parser = argparse.ArgumentParser(description="AWS FinOps Dashboard CLI")
58
+
59
+ parser.add_argument(
60
+ "--config-file",
61
+ "-C",
62
+ help="Path to a TOML, YAML, or JSON configuration file.",
63
+ type=str,
64
+ )
65
+ parser.add_argument(
66
+ "--profiles",
67
+ "-p",
68
+ nargs="+",
69
+ help="Specific AWS profiles to use (space-separated)",
70
+ type=str,
71
+ )
72
+ parser.add_argument(
73
+ "--regions",
74
+ "-r",
75
+ nargs="+",
76
+ help="AWS regions to check for EC2 instances (space-separated)",
77
+ type=str,
78
+ )
79
+ parser.add_argument("--all", "-a", action="store_true", help="Use all available AWS profiles")
80
+ parser.add_argument(
81
+ "--combine",
82
+ "-c",
83
+ action="store_true",
84
+ help="Combine profiles from the same AWS account",
85
+ )
86
+ parser.add_argument(
87
+ "--report-name",
88
+ "-n",
89
+ help="Specify the base name for the report file (without extension)",
90
+ default=None,
91
+ type=str,
92
+ )
93
+ parser.add_argument(
94
+ "--report-type",
95
+ "-y",
96
+ nargs="+",
97
+ choices=["csv", "json", "pdf"],
98
+ help="Specify one or more report types: csv and/or json and/or pdf (space-separated)",
99
+ type=str,
100
+ default=["csv"],
101
+ )
102
+ parser.add_argument(
103
+ "--dir",
104
+ "-d",
105
+ help="Directory to save the report files (default: current directory)",
106
+ type=str,
107
+ )
108
+ parser.add_argument(
109
+ "--time-range",
110
+ "-t",
111
+ help="Time range for cost data in days (default: current month). Examples: 7, 30, 90",
112
+ type=int,
113
+ )
114
+ parser.add_argument(
115
+ "--tag",
116
+ "-g",
117
+ nargs="+",
118
+ help="Cost allocation tag to filter resources, e.g., --tag Team=DevOps",
119
+ type=str,
120
+ )
121
+ parser.add_argument(
122
+ "--trend",
123
+ action="store_true",
124
+ help="Display a trend report as bars for the past 6 months time range",
125
+ )
126
+ parser.add_argument(
127
+ "--audit",
128
+ action="store_true",
129
+ help="Display an audit report with cost anomalies, stopped EC2 instances, unused EBS columes, budget alerts, and more",
130
+ )
131
+
132
+ args = parser.parse_args()
133
+
134
+ config_data: Optional[Dict[str, Any]] = None
135
+ if args.config_file:
136
+ config_data = load_config_file(args.config_file)
137
+ if config_data is None:
138
+ return 1 # Exit if config file loading failed
139
+
140
+ # Override args with config_data if present and arg is not set via CLI
141
+ if config_data:
142
+ for key, value in config_data.items():
143
+ if hasattr(args, key) and getattr(args, key) == parser.get_default(key):
144
+ setattr(args, key, value)
145
+
146
+ result = run_dashboard(args)
147
+ return 0 if result == 0 else 1
148
+
149
+
150
+ if __name__ == "__main__":
151
+ sys.exit(main())
@@ -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