runbooks 0.2.3__py3-none-any.whl → 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conftest.py +26 -0
- jupyter-agent/.env.template +2 -0
- jupyter-agent/.gitattributes +35 -0
- jupyter-agent/README.md +16 -0
- jupyter-agent/app.py +256 -0
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +154 -0
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +123 -0
- jupyter-agent/requirements.txt +9 -0
- jupyter-agent/utils.py +409 -0
- runbooks/__init__.py +71 -3
- runbooks/__main__.py +13 -0
- runbooks/aws/ec2_describe_instances.py +1 -1
- runbooks/aws/ec2_run_instances.py +8 -2
- runbooks/aws/ec2_start_stop_instances.py +17 -4
- runbooks/aws/ec2_unused_volumes.py +5 -1
- runbooks/aws/s3_create_bucket.py +4 -2
- runbooks/aws/s3_list_objects.py +6 -1
- runbooks/aws/tagging_lambda_handler.py +13 -2
- runbooks/aws/tags.json +12 -0
- runbooks/base.py +353 -0
- runbooks/cfat/README.md +49 -0
- runbooks/cfat/__init__.py +74 -0
- runbooks/cfat/app.ts +644 -0
- runbooks/cfat/assessment/__init__.py +40 -0
- runbooks/cfat/assessment/asana-import.csv +39 -0
- runbooks/cfat/assessment/cfat-checks.csv +31 -0
- runbooks/cfat/assessment/cfat.txt +520 -0
- runbooks/cfat/assessment/collectors.py +200 -0
- runbooks/cfat/assessment/jira-import.csv +39 -0
- runbooks/cfat/assessment/runner.py +387 -0
- runbooks/cfat/assessment/validators.py +290 -0
- runbooks/cfat/cli.py +103 -0
- runbooks/cfat/docs/asana-import.csv +24 -0
- runbooks/cfat/docs/cfat-checks.csv +31 -0
- runbooks/cfat/docs/cfat.txt +335 -0
- runbooks/cfat/docs/checks-output.png +0 -0
- runbooks/cfat/docs/cloudshell-console-run.png +0 -0
- runbooks/cfat/docs/cloudshell-download.png +0 -0
- runbooks/cfat/docs/cloudshell-output.png +0 -0
- runbooks/cfat/docs/downloadfile.png +0 -0
- runbooks/cfat/docs/jira-import.csv +24 -0
- runbooks/cfat/docs/open-cloudshell.png +0 -0
- runbooks/cfat/docs/report-header.png +0 -0
- runbooks/cfat/models.py +1026 -0
- runbooks/cfat/package-lock.json +5116 -0
- runbooks/cfat/package.json +38 -0
- runbooks/cfat/report.py +496 -0
- runbooks/cfat/reporting/__init__.py +46 -0
- runbooks/cfat/reporting/exporters.py +337 -0
- runbooks/cfat/reporting/formatters.py +496 -0
- runbooks/cfat/reporting/templates.py +135 -0
- runbooks/cfat/run-assessment.sh +23 -0
- runbooks/cfat/runner.py +69 -0
- runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
- runbooks/cfat/src/actions/check-config-existence.ts +37 -0
- runbooks/cfat/src/actions/check-control-tower.ts +37 -0
- runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
- runbooks/cfat/src/actions/check-iam-users.ts +50 -0
- runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
- runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
- runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
- runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
- runbooks/cfat/src/actions/create-backlog.ts +372 -0
- runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
- runbooks/cfat/src/actions/create-report.ts +616 -0
- runbooks/cfat/src/actions/define-account-type.ts +51 -0
- runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
- runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
- runbooks/cfat/src/actions/get-idc-info.ts +34 -0
- runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
- runbooks/cfat/src/actions/get-org-details.ts +35 -0
- runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
- runbooks/cfat/src/actions/get-org-ous.ts +35 -0
- runbooks/cfat/src/actions/get-regions.ts +22 -0
- runbooks/cfat/src/actions/zip-assessment.ts +27 -0
- runbooks/cfat/src/types/index.d.ts +147 -0
- runbooks/cfat/tests/__init__.py +141 -0
- runbooks/cfat/tests/test_cli.py +340 -0
- runbooks/cfat/tests/test_integration.py +290 -0
- runbooks/cfat/tests/test_models.py +505 -0
- runbooks/cfat/tests/test_reporting.py +354 -0
- runbooks/cfat/tsconfig.json +16 -0
- runbooks/cfat/webpack.config.cjs +27 -0
- runbooks/config.py +260 -0
- runbooks/finops/__init__.py +88 -0
- runbooks/finops/aws_client.py +245 -0
- runbooks/finops/cli.py +151 -0
- runbooks/finops/cost_processor.py +410 -0
- runbooks/finops/dashboard_runner.py +448 -0
- runbooks/finops/helpers.py +355 -0
- runbooks/finops/main.py +14 -0
- runbooks/finops/profile_processor.py +174 -0
- runbooks/finops/types.py +66 -0
- runbooks/finops/visualisations.py +80 -0
- runbooks/inventory/.gitignore +354 -0
- runbooks/inventory/ArgumentsClass.py +261 -0
- runbooks/inventory/Inventory_Modules.py +6130 -0
- runbooks/inventory/LandingZone/delete_lz.py +1075 -0
- runbooks/inventory/README.md +1320 -0
- runbooks/inventory/__init__.py +62 -0
- runbooks/inventory/account_class.py +532 -0
- runbooks/inventory/all_my_instances_wrapper.py +123 -0
- runbooks/inventory/aws_decorators.py +201 -0
- runbooks/inventory/cfn_move_stack_instances.py +1526 -0
- runbooks/inventory/check_cloudtrail_compliance.py +614 -0
- runbooks/inventory/check_controltower_readiness.py +1107 -0
- runbooks/inventory/check_landingzone_readiness.py +711 -0
- runbooks/inventory/cloudtrail.md +727 -0
- runbooks/inventory/collectors/__init__.py +20 -0
- runbooks/inventory/collectors/aws_compute.py +518 -0
- runbooks/inventory/collectors/aws_networking.py +275 -0
- runbooks/inventory/collectors/base.py +222 -0
- runbooks/inventory/core/__init__.py +19 -0
- runbooks/inventory/core/collector.py +303 -0
- runbooks/inventory/core/formatter.py +296 -0
- runbooks/inventory/delete_s3_buckets_objects.py +169 -0
- runbooks/inventory/discovery.md +81 -0
- runbooks/inventory/draw_org_structure.py +748 -0
- runbooks/inventory/ec2_vpc_utils.py +341 -0
- runbooks/inventory/find_cfn_drift_detection.py +272 -0
- runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
- runbooks/inventory/find_cfn_stackset_drift.py +733 -0
- runbooks/inventory/find_ec2_security_groups.py +669 -0
- runbooks/inventory/find_landingzone_versions.py +201 -0
- runbooks/inventory/find_vpc_flow_logs.py +1221 -0
- runbooks/inventory/inventory.sh +659 -0
- runbooks/inventory/list_cfn_stacks.py +558 -0
- runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
- runbooks/inventory/list_cfn_stackset_operations.py +734 -0
- runbooks/inventory/list_cfn_stacksets.py +453 -0
- runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
- runbooks/inventory/list_ds_directories.py +354 -0
- runbooks/inventory/list_ec2_availability_zones.py +286 -0
- runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
- runbooks/inventory/list_ec2_instances.py +425 -0
- runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
- runbooks/inventory/list_elbs_load_balancers.py +411 -0
- runbooks/inventory/list_enis_network_interfaces.py +526 -0
- runbooks/inventory/list_guardduty_detectors.py +568 -0
- runbooks/inventory/list_iam_policies.py +404 -0
- runbooks/inventory/list_iam_roles.py +518 -0
- runbooks/inventory/list_iam_saml_providers.py +359 -0
- runbooks/inventory/list_lambda_functions.py +882 -0
- runbooks/inventory/list_org_accounts.py +446 -0
- runbooks/inventory/list_org_accounts_users.py +354 -0
- runbooks/inventory/list_rds_db_instances.py +406 -0
- runbooks/inventory/list_route53_hosted_zones.py +318 -0
- runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
- runbooks/inventory/list_sns_topics.py +360 -0
- runbooks/inventory/list_ssm_parameters.py +402 -0
- runbooks/inventory/list_vpc_subnets.py +433 -0
- runbooks/inventory/list_vpcs.py +422 -0
- runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
- runbooks/inventory/models/__init__.py +24 -0
- runbooks/inventory/models/account.py +192 -0
- runbooks/inventory/models/inventory.py +309 -0
- runbooks/inventory/models/resource.py +247 -0
- runbooks/inventory/recover_cfn_stack_ids.py +205 -0
- runbooks/inventory/requirements.txt +12 -0
- runbooks/inventory/run_on_multi_accounts.py +211 -0
- runbooks/inventory/tests/common_test_data.py +3661 -0
- runbooks/inventory/tests/common_test_functions.py +204 -0
- runbooks/inventory/tests/script_test_data.py +0 -0
- runbooks/inventory/tests/setup.py +24 -0
- runbooks/inventory/tests/src.py +18 -0
- runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
- runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
- runbooks/inventory/tests/test_inventory_modules.py +55 -0
- runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
- runbooks/inventory/tests/test_moto_integration_example.py +273 -0
- runbooks/inventory/tests/test_org_list_accounts.py +49 -0
- runbooks/inventory/update_aws_actions.py +173 -0
- runbooks/inventory/update_cfn_stacksets.py +1215 -0
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
- runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
- runbooks/inventory/update_s3_public_access_block.py +539 -0
- runbooks/inventory/utils/__init__.py +23 -0
- runbooks/inventory/utils/aws_helpers.py +510 -0
- runbooks/inventory/utils/threading_utils.py +493 -0
- runbooks/inventory/utils/validation.py +682 -0
- runbooks/inventory/verify_ec2_security_groups.py +1430 -0
- runbooks/main.py +785 -0
- runbooks/organizations/__init__.py +12 -0
- runbooks/organizations/manager.py +374 -0
- runbooks/security_baseline/README.md +324 -0
- runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
- runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
- runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
- runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
- runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
- runbooks/security_baseline/checklist/root_access_key.py +6 -1
- runbooks/security_baseline/config-origin.json +1 -1
- runbooks/security_baseline/config.json +1 -1
- runbooks/security_baseline/permission.json +1 -1
- runbooks/security_baseline/report_generator.py +10 -2
- runbooks/security_baseline/report_template_en.html +8 -8
- runbooks/security_baseline/report_template_jp.html +8 -8
- runbooks/security_baseline/report_template_kr.html +13 -13
- runbooks/security_baseline/report_template_vn.html +8 -8
- runbooks/security_baseline/requirements.txt +7 -0
- runbooks/security_baseline/run_script.py +8 -2
- runbooks/security_baseline/security_baseline_tester.py +10 -2
- runbooks/security_baseline/utils/common.py +5 -1
- runbooks/utils/__init__.py +204 -0
- runbooks-0.6.1.dist-info/METADATA +373 -0
- runbooks-0.6.1.dist-info/RECORD +237 -0
- {runbooks-0.2.3.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
- runbooks-0.6.1.dist-info/entry_points.txt +7 -0
- runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
- runbooks-0.6.1.dist-info/top_level.txt +3 -0
- runbooks/python101/calculator.py +0 -34
- runbooks/python101/config.py +0 -1
- runbooks/python101/exceptions.py +0 -16
- runbooks/python101/file_manager.py +0 -218
- runbooks/python101/toolkit.py +0 -153
- runbooks-0.2.3.dist-info/METADATA +0 -435
- runbooks-0.2.3.dist-info/RECORD +0 -61
- runbooks-0.2.3.dist-info/entry_points.txt +0 -3
- runbooks-0.2.3.dist-info/top_level.txt +0 -1
@@ -0,0 +1,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", [])
|