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,303 @@
1
+ """
2
+ Inventory collector for AWS resources.
3
+
4
+ This module provides the main inventory collection orchestration,
5
+ leveraging existing inventory scripts and extending them with
6
+ cloud foundations best practices.
7
+ """
8
+
9
+ import asyncio
10
+ from concurrent.futures import ThreadPoolExecutor, as_completed
11
+ from datetime import datetime
12
+ from typing import Any, Dict, List, Optional, Set
13
+
14
+ from loguru import logger
15
+
16
+ from runbooks.base import CloudFoundationsBase, ProgressTracker
17
+ from runbooks.config import RunbooksConfig
18
+
19
+
20
+ class InventoryCollector(CloudFoundationsBase):
21
+ """
22
+ Main inventory collector for AWS resources.
23
+
24
+ Orchestrates resource discovery across multiple accounts and regions,
25
+ providing comprehensive inventory capabilities.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ profile: Optional[str] = None,
31
+ region: Optional[str] = None,
32
+ config: Optional[RunbooksConfig] = None,
33
+ parallel: bool = True,
34
+ ):
35
+ """Initialize inventory collector."""
36
+ super().__init__(profile, region, config)
37
+ self.parallel = parallel
38
+ self._resource_collectors = self._initialize_collectors()
39
+
40
+ def _initialize_collectors(self) -> Dict[str, str]:
41
+ """Initialize available resource collectors."""
42
+ # Map resource types to their collector modules
43
+ collectors = {
44
+ "ec2": "EC2Collector",
45
+ "rds": "RDSCollector",
46
+ "s3": "S3Collector",
47
+ "lambda": "LambdaCollector",
48
+ "iam": "IAMCollector",
49
+ "vpc": "VPCCollector",
50
+ "cloudformation": "CloudFormationCollector",
51
+ "costs": "CostCollector",
52
+ }
53
+
54
+ logger.debug(f"Initialized {len(collectors)} resource collectors")
55
+ return collectors
56
+
57
+ def get_all_resource_types(self) -> List[str]:
58
+ """Get list of all available resource types."""
59
+ return list(self._resource_collectors.keys())
60
+
61
+ def get_organization_accounts(self) -> List[str]:
62
+ """Get list of accounts in AWS Organization."""
63
+ try:
64
+ organizations_client = self.get_client("organizations")
65
+ response = self._make_aws_call(organizations_client.list_accounts)
66
+
67
+ accounts = []
68
+ for account in response.get("Accounts", []):
69
+ if account["Status"] == "ACTIVE":
70
+ accounts.append(account["Id"])
71
+
72
+ logger.info(f"Found {len(accounts)} active accounts in organization")
73
+ return accounts
74
+
75
+ except Exception as e:
76
+ logger.warning(f"Could not list organization accounts: {e}")
77
+ # Fallback to current account
78
+ return [self.get_account_id()]
79
+
80
+ def get_current_account_id(self) -> str:
81
+ """Get current AWS account ID."""
82
+ return self.get_account_id()
83
+
84
+ def collect_inventory(
85
+ self, resource_types: List[str], account_ids: List[str], include_costs: bool = False
86
+ ) -> Dict[str, Any]:
87
+ """
88
+ Collect inventory across specified resources and accounts.
89
+
90
+ Args:
91
+ resource_types: List of resource types to collect
92
+ account_ids: List of account IDs to scan
93
+ include_costs: Whether to include cost information
94
+
95
+ Returns:
96
+ Dictionary containing inventory results
97
+ """
98
+ logger.info(
99
+ f"Starting inventory collection for {len(resource_types)} resource types across {len(account_ids)} accounts"
100
+ )
101
+
102
+ start_time = datetime.now()
103
+ results = {
104
+ "metadata": {
105
+ "collection_time": start_time.isoformat(),
106
+ "account_ids": account_ids,
107
+ "resource_types": resource_types,
108
+ "include_costs": include_costs,
109
+ "collector_profile": self.profile,
110
+ "collector_region": self.region,
111
+ },
112
+ "resources": {},
113
+ "summary": {},
114
+ "errors": [],
115
+ }
116
+
117
+ try:
118
+ if self.parallel:
119
+ resource_data = self._collect_parallel(resource_types, account_ids, include_costs)
120
+ else:
121
+ resource_data = self._collect_sequential(resource_types, account_ids, include_costs)
122
+
123
+ results["resources"] = resource_data
124
+ results["summary"] = self._generate_summary(resource_data)
125
+
126
+ end_time = datetime.now()
127
+ duration = (end_time - start_time).total_seconds()
128
+ results["metadata"]["duration_seconds"] = duration
129
+
130
+ logger.info(f"Inventory collection completed in {duration:.1f}s")
131
+ return results
132
+
133
+ except Exception as e:
134
+ logger.error(f"Inventory collection failed: {e}")
135
+ results["errors"].append(str(e))
136
+ return results
137
+
138
+ def _collect_parallel(
139
+ self, resource_types: List[str], account_ids: List[str], include_costs: bool
140
+ ) -> Dict[str, Any]:
141
+ """Collect inventory in parallel."""
142
+ results = {}
143
+ total_tasks = len(resource_types) * len(account_ids)
144
+ progress = ProgressTracker(total_tasks, "Collecting inventory")
145
+
146
+ with ThreadPoolExecutor(max_workers=10) as executor:
147
+ # Submit collection tasks
148
+ future_to_params = {}
149
+
150
+ for resource_type in resource_types:
151
+ for account_id in account_ids:
152
+ future = executor.submit(
153
+ self._collect_resource_for_account, resource_type, account_id, include_costs
154
+ )
155
+ future_to_params[future] = (resource_type, account_id)
156
+
157
+ # Collect results
158
+ for future in as_completed(future_to_params):
159
+ resource_type, account_id = future_to_params[future]
160
+ try:
161
+ resource_data = future.result()
162
+
163
+ if resource_type not in results:
164
+ results[resource_type] = {}
165
+
166
+ results[resource_type][account_id] = resource_data
167
+ progress.update(status=f"Completed {resource_type} for {account_id}")
168
+
169
+ except Exception as e:
170
+ logger.error(f"Failed to collect {resource_type} for account {account_id}: {e}")
171
+ progress.update(status=f"Failed {resource_type} for {account_id}")
172
+
173
+ progress.complete()
174
+ return results
175
+
176
+ def _collect_sequential(
177
+ self, resource_types: List[str], account_ids: List[str], include_costs: bool
178
+ ) -> Dict[str, Any]:
179
+ """Collect inventory sequentially."""
180
+ results = {}
181
+ total_tasks = len(resource_types) * len(account_ids)
182
+ progress = ProgressTracker(total_tasks, "Collecting inventory")
183
+
184
+ for resource_type in resource_types:
185
+ results[resource_type] = {}
186
+
187
+ for account_id in account_ids:
188
+ try:
189
+ resource_data = self._collect_resource_for_account(resource_type, account_id, include_costs)
190
+ results[resource_type][account_id] = resource_data
191
+ progress.update(status=f"Completed {resource_type} for {account_id}")
192
+
193
+ except Exception as e:
194
+ logger.error(f"Failed to collect {resource_type} for account {account_id}: {e}")
195
+ results[resource_type][account_id] = {"error": str(e)}
196
+ progress.update(status=f"Failed {resource_type} for {account_id}")
197
+
198
+ progress.complete()
199
+ return results
200
+
201
+ def _collect_resource_for_account(self, resource_type: str, account_id: str, include_costs: bool) -> Dict[str, Any]:
202
+ """
203
+ Collect specific resource type for an account.
204
+
205
+ This is a mock implementation. In a full implementation,
206
+ this would delegate to specific resource collectors.
207
+ """
208
+ # Mock implementation - replace with actual collectors
209
+ import random
210
+ import time
211
+
212
+ # Simulate collection time
213
+ time.sleep(random.uniform(0.1, 0.5))
214
+
215
+ # Generate mock data based on resource type
216
+ if resource_type == "ec2":
217
+ return {
218
+ "instances": [
219
+ {
220
+ "instance_id": f"i-{random.randint(100000000000, 999999999999):012x}",
221
+ "instance_type": random.choice(["t3.micro", "t3.small", "m5.large"]),
222
+ "state": random.choice(["running", "stopped"]),
223
+ "region": self.region or "us-east-1",
224
+ "account_id": account_id,
225
+ "tags": {"Environment": random.choice(["dev", "staging", "prod"])},
226
+ }
227
+ for _ in range(random.randint(0, 5))
228
+ ],
229
+ "count": random.randint(0, 5),
230
+ }
231
+ elif resource_type == "rds":
232
+ return {
233
+ "instances": [
234
+ {
235
+ "db_instance_identifier": f"db-{random.randint(1000, 9999)}",
236
+ "engine": random.choice(["mysql", "postgres", "aurora"]),
237
+ "instance_class": random.choice(["db.t3.micro", "db.t3.small"]),
238
+ "status": "available",
239
+ "account_id": account_id,
240
+ }
241
+ for _ in range(random.randint(0, 3))
242
+ ],
243
+ "count": random.randint(0, 3),
244
+ }
245
+ elif resource_type == "s3":
246
+ return {
247
+ "buckets": [
248
+ {
249
+ "name": f"bucket-{account_id}-{random.randint(1000, 9999)}",
250
+ "creation_date": datetime.now().isoformat(),
251
+ "region": self.region or "us-east-1",
252
+ "account_id": account_id,
253
+ }
254
+ for _ in range(random.randint(1, 10))
255
+ ],
256
+ "count": random.randint(1, 10),
257
+ }
258
+ else:
259
+ return {"resources": [], "count": 0, "resource_type": resource_type, "account_id": account_id}
260
+
261
+ def _generate_summary(self, resource_data: Dict[str, Any]) -> Dict[str, Any]:
262
+ """Generate summary statistics from collected data."""
263
+ summary = {
264
+ "total_resources": 0,
265
+ "resources_by_type": {},
266
+ "resources_by_account": {},
267
+ "collection_status": "completed",
268
+ }
269
+
270
+ for resource_type, accounts_data in resource_data.items():
271
+ type_count = 0
272
+
273
+ for account_id, account_data in accounts_data.items():
274
+ if "error" in account_data:
275
+ continue
276
+
277
+ # Count resources based on type
278
+ if resource_type == "ec2":
279
+ account_count = account_data.get("count", 0)
280
+ elif resource_type == "rds":
281
+ account_count = account_data.get("count", 0)
282
+ elif resource_type == "s3":
283
+ account_count = account_data.get("count", 0)
284
+ else:
285
+ account_count = account_data.get("count", 0)
286
+
287
+ type_count += account_count
288
+
289
+ if account_id not in summary["resources_by_account"]:
290
+ summary["resources_by_account"][account_id] = 0
291
+ summary["resources_by_account"][account_id] += account_count
292
+
293
+ summary["resources_by_type"][resource_type] = type_count
294
+ summary["total_resources"] += type_count
295
+
296
+ return summary
297
+
298
+ def run(self):
299
+ """Implementation of abstract base method."""
300
+ # Default inventory collection
301
+ resource_types = ["ec2", "rds", "s3"]
302
+ account_ids = [self.get_current_account_id()]
303
+ return self.collect_inventory(resource_types, account_ids)
@@ -0,0 +1,296 @@
1
+ """
2
+ Inventory formatter for various output formats.
3
+
4
+ This module provides formatting capabilities for inventory data
5
+ including CSV, JSON, Excel, and console table formats.
6
+ """
7
+
8
+ import json
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Union
12
+
13
+ try:
14
+ import pandas as pd
15
+
16
+ _HAS_PANDAS = True
17
+ except ImportError:
18
+ _HAS_PANDAS = False
19
+ pd = None
20
+
21
+ from loguru import logger
22
+
23
+ from runbooks.base import CloudFoundationsFormatter
24
+
25
+
26
+ class InventoryFormatter(CloudFoundationsFormatter):
27
+ """Formatter for inventory data with multiple output formats."""
28
+
29
+ def __init__(self, inventory_data: Dict[str, Any]):
30
+ """Initialize formatter with inventory data."""
31
+ super().__init__(inventory_data)
32
+ self.inventory_data = inventory_data
33
+
34
+ def to_csv(self, file_path: Union[str, Path]) -> None:
35
+ """Save inventory data as CSV files (one per resource type)."""
36
+ if not _HAS_PANDAS:
37
+ logger.error("pandas is required for CSV export. Install with: pip install pandas")
38
+ return
39
+
40
+ output_path = Path(file_path)
41
+ output_dir = output_path.parent / output_path.stem
42
+ output_dir.mkdir(parents=True, exist_ok=True)
43
+
44
+ resources = self.inventory_data.get("resources", {})
45
+
46
+ for resource_type, accounts_data in resources.items():
47
+ # Flatten data for CSV format
48
+ rows = []
49
+
50
+ for account_id, account_data in accounts_data.items():
51
+ if "error" in account_data:
52
+ rows.append(
53
+ {
54
+ "account_id": account_id,
55
+ "resource_type": resource_type,
56
+ "status": "error",
57
+ "error_message": account_data["error"],
58
+ }
59
+ )
60
+ continue
61
+
62
+ # Extract resources based on type
63
+ if resource_type == "ec2" and "instances" in account_data:
64
+ for instance in account_data["instances"]:
65
+ row = {
66
+ "account_id": account_id,
67
+ "resource_type": resource_type,
68
+ "resource_id": instance.get("instance_id"),
69
+ "resource_name": instance.get("instance_id"),
70
+ "instance_type": instance.get("instance_type"),
71
+ "state": instance.get("state"),
72
+ "region": instance.get("region"),
73
+ "tags": json.dumps(instance.get("tags", {})),
74
+ }
75
+ rows.append(row)
76
+
77
+ elif resource_type == "rds" and "instances" in account_data:
78
+ for instance in account_data["instances"]:
79
+ row = {
80
+ "account_id": account_id,
81
+ "resource_type": resource_type,
82
+ "resource_id": instance.get("db_instance_identifier"),
83
+ "resource_name": instance.get("db_instance_identifier"),
84
+ "engine": instance.get("engine"),
85
+ "instance_class": instance.get("instance_class"),
86
+ "status": instance.get("status"),
87
+ "region": self.inventory_data["metadata"].get("collector_region"),
88
+ }
89
+ rows.append(row)
90
+
91
+ elif resource_type == "s3" and "buckets" in account_data:
92
+ for bucket in account_data["buckets"]:
93
+ row = {
94
+ "account_id": account_id,
95
+ "resource_type": resource_type,
96
+ "resource_id": bucket.get("name"),
97
+ "resource_name": bucket.get("name"),
98
+ "creation_date": bucket.get("creation_date"),
99
+ "region": bucket.get("region"),
100
+ }
101
+ rows.append(row)
102
+
103
+ if rows:
104
+ df = pd.DataFrame(rows)
105
+ csv_file = output_dir / f"{resource_type}.csv"
106
+ df.to_csv(csv_file, index=False)
107
+ logger.info(f"Saved {resource_type} data to: {csv_file}")
108
+
109
+ # Save summary
110
+ summary_file = output_dir / "summary.csv"
111
+ summary_data = []
112
+
113
+ summary = self.inventory_data.get("summary", {})
114
+ for resource_type, count in summary.get("resources_by_type", {}).items():
115
+ summary_data.append({"resource_type": resource_type, "total_count": count})
116
+
117
+ if summary_data:
118
+ df_summary = pd.DataFrame(summary_data)
119
+ df_summary.to_csv(summary_file, index=False)
120
+ logger.info(f"Saved summary to: {summary_file}")
121
+
122
+ def to_excel(self, file_path: Union[str, Path]) -> None:
123
+ """Save inventory data as Excel file with multiple sheets."""
124
+ if not _HAS_PANDAS:
125
+ logger.error("pandas is required for Excel export. Install with: pip install pandas")
126
+ return
127
+
128
+ output_path = Path(file_path)
129
+ output_path.parent.mkdir(parents=True, exist_ok=True)
130
+
131
+ with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
132
+ # Summary sheet
133
+ summary = self.inventory_data.get("summary", {})
134
+ summary_data = []
135
+
136
+ for resource_type, count in summary.get("resources_by_type", {}).items():
137
+ summary_data.append({"Resource Type": resource_type.upper(), "Total Count": count})
138
+
139
+ if summary_data:
140
+ df_summary = pd.DataFrame(summary_data)
141
+ df_summary.to_excel(writer, sheet_name="Summary", index=False)
142
+
143
+ # Resource sheets
144
+ resources = self.inventory_data.get("resources", {})
145
+
146
+ for resource_type, accounts_data in resources.items():
147
+ rows = []
148
+
149
+ for account_id, account_data in accounts_data.items():
150
+ if "error" in account_data:
151
+ continue
152
+
153
+ # Extract resources based on type
154
+ if resource_type == "ec2" and "instances" in account_data:
155
+ for instance in account_data["instances"]:
156
+ row = {
157
+ "Account ID": account_id,
158
+ "Instance ID": instance.get("instance_id"),
159
+ "Instance Type": instance.get("instance_type"),
160
+ "State": instance.get("state"),
161
+ "Region": instance.get("region"),
162
+ "Environment": instance.get("tags", {}).get("Environment", "N/A"),
163
+ }
164
+ rows.append(row)
165
+
166
+ elif resource_type == "rds" and "instances" in account_data:
167
+ for instance in account_data["instances"]:
168
+ row = {
169
+ "Account ID": account_id,
170
+ "DB Instance ID": instance.get("db_instance_identifier"),
171
+ "Engine": instance.get("engine"),
172
+ "Instance Class": instance.get("instance_class"),
173
+ "Status": instance.get("status"),
174
+ }
175
+ rows.append(row)
176
+
177
+ elif resource_type == "s3" and "buckets" in account_data:
178
+ for bucket in account_data["buckets"]:
179
+ row = {
180
+ "Account ID": account_id,
181
+ "Bucket Name": bucket.get("name"),
182
+ "Creation Date": bucket.get("creation_date"),
183
+ "Region": bucket.get("region"),
184
+ }
185
+ rows.append(row)
186
+
187
+ if rows:
188
+ df = pd.DataFrame(rows)
189
+ sheet_name = resource_type.upper()[:31] # Excel sheet name limit
190
+ df.to_excel(writer, sheet_name=sheet_name, index=False)
191
+
192
+ logger.info(f"Excel inventory saved to: {output_path}")
193
+
194
+ def to_json(self, file_path: Union[str, Path]) -> None:
195
+ """Save inventory data as JSON file."""
196
+ output_path = Path(file_path)
197
+ output_path.parent.mkdir(parents=True, exist_ok=True)
198
+
199
+ with open(output_path, "w", encoding="utf-8") as f:
200
+ json.dump(self.inventory_data, f, indent=2, default=str)
201
+
202
+ logger.info(f"JSON inventory saved to: {output_path}")
203
+
204
+ def format_console_table(self) -> str:
205
+ """Format inventory data for console display."""
206
+ try:
207
+ from rich.console import Console
208
+ from rich.table import Table
209
+ except ImportError:
210
+ # Fallback to simple text output
211
+ return self._format_simple_text_table()
212
+
213
+ console = Console()
214
+
215
+ # Summary table
216
+ summary_table = Table(title="Inventory Summary")
217
+ summary_table.add_column("Resource Type", style="cyan")
218
+ summary_table.add_column("Total Count", style="bold")
219
+
220
+ summary = self.inventory_data.get("summary", {})
221
+ for resource_type, count in summary.get("resources_by_type", {}).items():
222
+ summary_table.add_row(resource_type.upper(), str(count))
223
+
224
+ # Account breakdown table
225
+ account_table = Table(title="Resources by Account")
226
+ account_table.add_column("Account ID", style="cyan")
227
+ account_table.add_column("Total Resources", style="bold")
228
+
229
+ for account_id, count in summary.get("resources_by_account", {}).items():
230
+ account_table.add_row(account_id, str(count))
231
+
232
+ # Capture console output
233
+ with console.capture() as capture:
234
+ console.print(summary_table)
235
+ console.print()
236
+ console.print(account_table)
237
+
238
+ # Metadata
239
+ metadata = self.inventory_data.get("metadata", {})
240
+ console.print(f"\n[bold]Collection Details:[/bold]")
241
+ console.print(f"Collection Time: {metadata.get('collection_time', 'N/A')}")
242
+ console.print(f"Duration: {metadata.get('duration_seconds', 0):.1f}s")
243
+ console.print(f"Profile: {metadata.get('collector_profile', 'N/A')}")
244
+ console.print(f"Region: {metadata.get('collector_region', 'N/A')}")
245
+
246
+ return capture.get()
247
+
248
+ def _format_simple_text_table(self) -> str:
249
+ """Fallback text formatting when rich is not available."""
250
+ output = "Inventory Summary\n" + "=" * 50 + "\n"
251
+
252
+ summary = self.inventory_data.get("summary", {})
253
+
254
+ # Resource summary
255
+ output += "Resources by Type:\n"
256
+ for resource_type, count in summary.get("resources_by_type", {}).items():
257
+ output += f" {resource_type.upper()}: {count}\n"
258
+
259
+ # Account summary
260
+ output += "\nResources by Account:\n"
261
+ for account_id, count in summary.get("resources_by_account", {}).items():
262
+ output += f" {account_id}: {count}\n"
263
+
264
+ # Metadata
265
+ metadata = self.inventory_data.get("metadata", {})
266
+ output += f"\nCollection Details:\n"
267
+ output += f" Collection Time: {metadata.get('collection_time', 'N/A')}\n"
268
+ output += f" Duration: {metadata.get('duration_seconds', 0):.1f}s\n"
269
+ output += f" Profile: {metadata.get('collector_profile', 'N/A')}\n"
270
+ output += f" Region: {metadata.get('collector_region', 'N/A')}\n"
271
+
272
+ return output
273
+
274
+ def get_resource_counts(self) -> Dict[str, int]:
275
+ """Get resource counts by type."""
276
+ summary = self.inventory_data.get("summary", {})
277
+ return summary.get("resources_by_type", {})
278
+
279
+ def get_account_counts(self) -> Dict[str, int]:
280
+ """Get resource counts by account."""
281
+ summary = self.inventory_data.get("summary", {})
282
+ return summary.get("resources_by_account", {})
283
+
284
+ def get_total_resources(self) -> int:
285
+ """Get total resource count."""
286
+ summary = self.inventory_data.get("summary", {})
287
+ return summary.get("total_resources", 0)
288
+
289
+ def has_errors(self) -> bool:
290
+ """Check if inventory collection had errors."""
291
+ errors = self.inventory_data.get("errors", [])
292
+ return len(errors) > 0
293
+
294
+ def get_errors(self) -> List[str]:
295
+ """Get list of collection errors."""
296
+ return self.inventory_data.get("errors", [])