runbooks 0.9.9__py3-none-any.whl → 1.0.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.
- runbooks/cfat/cloud_foundations_assessment.py +626 -0
- runbooks/cloudops/cost_optimizer.py +95 -33
- runbooks/common/aws_pricing.py +388 -0
- runbooks/common/aws_pricing_api.py +205 -0
- runbooks/common/aws_utils.py +2 -2
- runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
- runbooks/common/cross_account_manager.py +606 -0
- runbooks/common/enhanced_exception_handler.py +4 -0
- runbooks/common/env_utils.py +96 -0
- runbooks/common/mcp_integration.py +49 -2
- runbooks/common/organizations_client.py +579 -0
- runbooks/common/profile_utils.py +96 -2
- runbooks/finops/cost_optimizer.py +2 -1
- runbooks/finops/elastic_ip_optimizer.py +13 -9
- runbooks/finops/embedded_mcp_validator.py +31 -0
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/markdown_exporter.py +217 -2
- runbooks/finops/nat_gateway_optimizer.py +57 -20
- runbooks/finops/vpc_cleanup_exporter.py +28 -26
- runbooks/finops/vpc_cleanup_optimizer.py +370 -16
- runbooks/inventory/__init__.py +10 -1
- runbooks/inventory/cloud_foundations_integration.py +409 -0
- runbooks/inventory/core/collector.py +1148 -88
- runbooks/inventory/discovery.md +389 -0
- runbooks/inventory/drift_detection_cli.py +327 -0
- runbooks/inventory/inventory_mcp_cli.py +171 -0
- runbooks/inventory/inventory_modules.py +4 -7
- runbooks/inventory/mcp_inventory_validator.py +2149 -0
- runbooks/inventory/mcp_vpc_validator.py +23 -6
- runbooks/inventory/organizations_discovery.py +91 -1
- runbooks/inventory/rich_inventory_display.py +129 -1
- runbooks/inventory/unified_validation_engine.py +1292 -0
- runbooks/inventory/verify_ec2_security_groups.py +3 -1
- runbooks/inventory/vpc_analyzer.py +825 -7
- runbooks/inventory/vpc_flow_analyzer.py +36 -42
- runbooks/main.py +654 -35
- runbooks/monitoring/performance_monitor.py +11 -7
- runbooks/operate/dynamodb_operations.py +6 -5
- runbooks/operate/ec2_operations.py +3 -2
- runbooks/operate/networking_cost_heatmap.py +4 -3
- runbooks/operate/s3_operations.py +13 -12
- runbooks/operate/vpc_operations.py +49 -1
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commvault_ec2_analysis.py +6 -1
- runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
- runbooks/remediation/rds_snapshot_list.py +5 -3
- runbooks/validation/__init__.py +21 -1
- runbooks/validation/comprehensive_2way_validator.py +1996 -0
- runbooks/validation/mcp_validator.py +904 -94
- runbooks/validation/terraform_citations_validator.py +363 -0
- runbooks/validation/terraform_drift_detector.py +1098 -0
- runbooks/vpc/cleanup_wrapper.py +231 -10
- runbooks/vpc/config.py +310 -62
- runbooks/vpc/cross_account_session.py +308 -0
- runbooks/vpc/heatmap_engine.py +96 -29
- runbooks/vpc/manager_interface.py +9 -9
- runbooks/vpc/mcp_no_eni_validator.py +1551 -0
- runbooks/vpc/networking_wrapper.py +14 -8
- runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/vpc/runbooks.security.report_generator.log +0 -0
- runbooks/vpc/runbooks.security.run_script.log +0 -0
- runbooks/vpc/runbooks.security.security_export.log +0 -0
- runbooks/vpc/tests/test_cost_engine.py +1 -1
- runbooks/vpc/unified_scenarios.py +73 -3
- runbooks/vpc/vpc_cleanup_integration.py +512 -78
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/METADATA +94 -52
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/RECORD +71 -49
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/WHEEL +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,409 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Cloud Foundations Integration Module
|
4
|
+
Integrates proven patterns into runbooks inventory module using enterprise finops architecture
|
5
|
+
|
6
|
+
This module extracts and integrates valuable patterns while maintaining runbooks architecture:
|
7
|
+
- Enhanced multi-account session management
|
8
|
+
- Specialized service discovery capabilities
|
9
|
+
- Enterprise-grade error handling and performance
|
10
|
+
- Rich CLI integration with runbooks standards
|
11
|
+
|
12
|
+
Strategic Alignment:
|
13
|
+
- "Do one thing and do it well" - Enhance existing inventory, don't duplicate
|
14
|
+
- "Move Fast, But Not So Fast We Crash" - Proven patterns with quality validation
|
15
|
+
- Maintain 3 strategic objectives alignment
|
16
|
+
"""
|
17
|
+
|
18
|
+
from typing import Dict, List, Optional, Any, Tuple
|
19
|
+
import asyncio
|
20
|
+
from concurrent.futures import ThreadPoolExecutor
|
21
|
+
import time
|
22
|
+
import logging
|
23
|
+
from dataclasses import dataclass, field
|
24
|
+
from pathlib import Path
|
25
|
+
|
26
|
+
import boto3
|
27
|
+
from botocore.exceptions import ClientError, ProfileNotFound
|
28
|
+
|
29
|
+
# Import runbooks enterprise standards
|
30
|
+
from runbooks.common.rich_utils import (
|
31
|
+
console, print_header, print_success, print_warning, print_error,
|
32
|
+
create_table, create_progress_bar, create_panel
|
33
|
+
)
|
34
|
+
from runbooks.common.profile_utils import get_profile_for_operation
|
35
|
+
|
36
|
+
__version__ = "1.0.0"
|
37
|
+
|
38
|
+
|
39
|
+
@dataclass
|
40
|
+
class EnhancedAccountInfo:
|
41
|
+
"""
|
42
|
+
Enhanced account information based on runbooks inventory account patterns
|
43
|
+
Supports enterprise multi-account operations with session management
|
44
|
+
"""
|
45
|
+
account_id: str
|
46
|
+
account_name: str
|
47
|
+
account_status: str
|
48
|
+
account_email: str
|
49
|
+
joined_method: str
|
50
|
+
organizational_unit: Optional[str] = None
|
51
|
+
account_type: str = "member" # management, member, suspended
|
52
|
+
session_cache: Dict[str, Any] = field(default_factory=dict)
|
53
|
+
last_accessed: Optional[float] = None
|
54
|
+
|
55
|
+
def __post_init__(self):
|
56
|
+
"""Initialize session cache with TTL management"""
|
57
|
+
self.last_accessed = time.time()
|
58
|
+
|
59
|
+
@property
|
60
|
+
def is_session_expired(self) -> bool:
|
61
|
+
"""Check if session cache is expired (4-hour TTL)"""
|
62
|
+
if not self.last_accessed:
|
63
|
+
return True
|
64
|
+
return (time.time() - self.last_accessed) > (4 * 3600) # 4 hours TTL
|
65
|
+
|
66
|
+
|
67
|
+
class CloudFoundationsAccountManager:
|
68
|
+
"""
|
69
|
+
Enhanced Account Manager integrating proven runbooks inventory patterns
|
70
|
+
|
71
|
+
Key Features:
|
72
|
+
- Multi-account organization discovery with filtering
|
73
|
+
- 4-hour TTL session management for 60+ account operations
|
74
|
+
- Enhanced error handling and graceful degradation
|
75
|
+
- Rich CLI integration with runbooks standards
|
76
|
+
- Cross-account role assumption with caching
|
77
|
+
|
78
|
+
Enhanced from: runbooks.inventory patterns with proven finops integration
|
79
|
+
"""
|
80
|
+
|
81
|
+
def __init__(self, profile: Optional[str] = None):
|
82
|
+
"""Initialize account manager with profile management"""
|
83
|
+
self.profile = get_profile_for_operation("management", profile)
|
84
|
+
self.accounts: Dict[str, EnhancedAccountInfo] = {}
|
85
|
+
self.session_cache: Dict[str, Dict[str, Any]] = {}
|
86
|
+
|
87
|
+
# Initialize base session
|
88
|
+
try:
|
89
|
+
self.base_session = boto3.Session(profile_name=self.profile)
|
90
|
+
print_success(f"Initialized Cloud Foundations Account Manager with profile: {self.profile}")
|
91
|
+
except ProfileNotFound as e:
|
92
|
+
print_error(f"Profile not found: {self.profile}")
|
93
|
+
raise
|
94
|
+
|
95
|
+
async def discover_organization_structure(self) -> Dict[str, List[EnhancedAccountInfo]]:
|
96
|
+
"""
|
97
|
+
Enhanced organization discovery with structure analysis
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
Dictionary organized by organizational units with account lists
|
101
|
+
|
102
|
+
Enhanced from: runbooks inventory organization discovery patterns with improved filtering and structure
|
103
|
+
"""
|
104
|
+
print_header("Organization Discovery", __version__)
|
105
|
+
|
106
|
+
try:
|
107
|
+
with create_progress_bar() as progress:
|
108
|
+
discovery_task = progress.add_task("Discovering organization structure...", total=100)
|
109
|
+
|
110
|
+
# Step 1: Discover organization accounts (40%)
|
111
|
+
accounts = await self._discover_accounts()
|
112
|
+
progress.update(discovery_task, advance=40)
|
113
|
+
|
114
|
+
# Step 2: Get organizational unit structure (30%)
|
115
|
+
ou_structure = await self._discover_organizational_units()
|
116
|
+
progress.update(discovery_task, advance=30)
|
117
|
+
|
118
|
+
# Step 3: Map accounts to OUs (30%)
|
119
|
+
structured_accounts = await self._map_accounts_to_ous(accounts, ou_structure)
|
120
|
+
progress.update(discovery_task, advance=30)
|
121
|
+
|
122
|
+
print_success(f"Discovered {len(accounts)} accounts across {len(structured_accounts)} organizational units")
|
123
|
+
return structured_accounts
|
124
|
+
|
125
|
+
except ClientError as e:
|
126
|
+
print_error(f"Organization discovery failed: {e}")
|
127
|
+
raise
|
128
|
+
|
129
|
+
async def _discover_accounts(self) -> List[EnhancedAccountInfo]:
|
130
|
+
"""
|
131
|
+
Discover all organization accounts with enhanced filtering
|
132
|
+
Based on proven runbooks inventory patterns
|
133
|
+
"""
|
134
|
+
try:
|
135
|
+
orgs_client = self.base_session.client('organizations')
|
136
|
+
accounts = []
|
137
|
+
|
138
|
+
# Use paginator for large organizations (60+ accounts)
|
139
|
+
paginator = orgs_client.get_paginator('list_accounts')
|
140
|
+
|
141
|
+
for page in paginator.paginate():
|
142
|
+
for account_data in page['Accounts']:
|
143
|
+
# Filter active accounts only (decommissioning filter)
|
144
|
+
if account_data['Status'] == 'ACTIVE':
|
145
|
+
account_info = EnhancedAccountInfo(
|
146
|
+
account_id=account_data['Id'],
|
147
|
+
account_name=account_data['Name'],
|
148
|
+
account_status=account_data['Status'],
|
149
|
+
account_email=account_data['Email'],
|
150
|
+
joined_method=account_data['JoinedMethod'],
|
151
|
+
account_type='management' if account_data['Id'] == self._get_management_account_id() else 'member'
|
152
|
+
)
|
153
|
+
accounts.append(account_info)
|
154
|
+
self.accounts[account_data['Id']] = account_info
|
155
|
+
|
156
|
+
return accounts
|
157
|
+
|
158
|
+
except ClientError as e:
|
159
|
+
if e.response['Error']['Code'] == 'AWSOrganizationsNotInUseException':
|
160
|
+
print_warning("Account is not part of an AWS Organization")
|
161
|
+
return []
|
162
|
+
raise
|
163
|
+
|
164
|
+
async def _discover_organizational_units(self) -> Dict[str, Dict[str, Any]]:
|
165
|
+
"""Discover organizational unit structure"""
|
166
|
+
try:
|
167
|
+
orgs_client = self.base_session.client('organizations')
|
168
|
+
|
169
|
+
# Get root and traverse OU structure
|
170
|
+
roots = orgs_client.list_roots()['Roots']
|
171
|
+
ou_structure = {}
|
172
|
+
|
173
|
+
for root in roots:
|
174
|
+
root_id = root['Id']
|
175
|
+
ou_structure[root_id] = {
|
176
|
+
'Name': root['Name'],
|
177
|
+
'Type': 'ROOT',
|
178
|
+
'Children': await self._get_ou_children(orgs_client, root_id)
|
179
|
+
}
|
180
|
+
|
181
|
+
return ou_structure
|
182
|
+
|
183
|
+
except ClientError as e:
|
184
|
+
print_warning(f"Could not discover OU structure: {e}")
|
185
|
+
return {}
|
186
|
+
|
187
|
+
async def _get_ou_children(self, orgs_client, parent_id: str) -> List[Dict[str, Any]]:
|
188
|
+
"""Recursively get OU children"""
|
189
|
+
children = []
|
190
|
+
|
191
|
+
try:
|
192
|
+
paginator = orgs_client.get_paginator('list_organizational_units_for_parent')
|
193
|
+
for page in paginator.paginate(ParentId=parent_id):
|
194
|
+
for ou in page['OrganizationalUnits']:
|
195
|
+
child_info = {
|
196
|
+
'Id': ou['Id'],
|
197
|
+
'Name': ou['Name'],
|
198
|
+
'Type': 'ORGANIZATIONAL_UNIT',
|
199
|
+
'Children': await self._get_ou_children(orgs_client, ou['Id'])
|
200
|
+
}
|
201
|
+
children.append(child_info)
|
202
|
+
|
203
|
+
except ClientError as e:
|
204
|
+
print_warning(f"Could not get children for {parent_id}: {e}")
|
205
|
+
|
206
|
+
return children
|
207
|
+
|
208
|
+
async def _map_accounts_to_ous(self,
|
209
|
+
accounts: List[EnhancedAccountInfo],
|
210
|
+
ou_structure: Dict[str, Dict[str, Any]]) -> Dict[str, List[EnhancedAccountInfo]]:
|
211
|
+
"""Map accounts to their organizational units"""
|
212
|
+
mapped_accounts = {}
|
213
|
+
|
214
|
+
try:
|
215
|
+
orgs_client = self.base_session.client('organizations')
|
216
|
+
|
217
|
+
for account in accounts:
|
218
|
+
try:
|
219
|
+
# Find which OU this account belongs to
|
220
|
+
parents = orgs_client.list_parents(ChildId=account.account_id)['Parents']
|
221
|
+
|
222
|
+
for parent in parents:
|
223
|
+
parent_id = parent['Id']
|
224
|
+
parent_type = parent['Type']
|
225
|
+
|
226
|
+
# Create OU key for grouping
|
227
|
+
ou_key = f"{parent_type}:{parent_id}"
|
228
|
+
if ou_key not in mapped_accounts:
|
229
|
+
mapped_accounts[ou_key] = []
|
230
|
+
|
231
|
+
account.organizational_unit = parent_id
|
232
|
+
mapped_accounts[ou_key].append(account)
|
233
|
+
|
234
|
+
except ClientError as e:
|
235
|
+
print_warning(f"Could not map account {account.account_id} to OU: {e}")
|
236
|
+
# Add to ungrouped accounts
|
237
|
+
if 'ungrouped' not in mapped_accounts:
|
238
|
+
mapped_accounts['ungrouped'] = []
|
239
|
+
mapped_accounts['ungrouped'].append(account)
|
240
|
+
|
241
|
+
except Exception as e:
|
242
|
+
print_warning(f"Account to OU mapping encountered issues: {e}")
|
243
|
+
|
244
|
+
return mapped_accounts
|
245
|
+
|
246
|
+
def get_cross_account_session(self,
|
247
|
+
target_account_id: str,
|
248
|
+
role_name: str = "OrganizationAccountAccessRole") -> Optional[boto3.Session]:
|
249
|
+
"""
|
250
|
+
Get cross-account session with enhanced caching and TTL management
|
251
|
+
|
252
|
+
Based on runbooks inventory patterns with 4-hour TTL optimization
|
253
|
+
"""
|
254
|
+
session_key = f"{target_account_id}_{role_name}"
|
255
|
+
|
256
|
+
# Check session cache with TTL validation
|
257
|
+
if (session_key in self.session_cache and
|
258
|
+
not self._is_session_expired(self.session_cache[session_key])):
|
259
|
+
return self.session_cache[session_key]['session']
|
260
|
+
|
261
|
+
# Create new cross-account session
|
262
|
+
try:
|
263
|
+
sts_client = self.base_session.client('sts')
|
264
|
+
role_arn = f"arn:aws:iam::{target_account_id}:role/{role_name}"
|
265
|
+
|
266
|
+
response = sts_client.assume_role(
|
267
|
+
RoleArn=role_arn,
|
268
|
+
RoleSessionName=f"CloudOpsRunbooks-CF-{target_account_id}",
|
269
|
+
DurationSeconds=14400 # 4 hours maximum
|
270
|
+
)
|
271
|
+
|
272
|
+
credentials = response['Credentials']
|
273
|
+
cross_account_session = boto3.Session(
|
274
|
+
aws_access_key_id=credentials['AccessKeyId'],
|
275
|
+
aws_secret_access_key=credentials['SecretAccessKey'],
|
276
|
+
aws_session_token=credentials['SessionToken']
|
277
|
+
)
|
278
|
+
|
279
|
+
# Cache session with TTL
|
280
|
+
self.session_cache[session_key] = {
|
281
|
+
'session': cross_account_session,
|
282
|
+
'expires_at': credentials['Expiration'],
|
283
|
+
'created_at': time.time()
|
284
|
+
}
|
285
|
+
|
286
|
+
return cross_account_session
|
287
|
+
|
288
|
+
except ClientError as e:
|
289
|
+
if 'AccessDenied' in str(e):
|
290
|
+
print_warning(f"Cross-account access denied for {target_account_id} (role: {role_name})")
|
291
|
+
else:
|
292
|
+
print_warning(f"Cross-account session creation failed for {target_account_id}: {e}")
|
293
|
+
return None
|
294
|
+
|
295
|
+
def _is_session_expired(self, session_info: Dict[str, Any]) -> bool:
|
296
|
+
"""Check if cached session is expired"""
|
297
|
+
if 'expires_at' in session_info:
|
298
|
+
return time.time() >= session_info['expires_at'].timestamp()
|
299
|
+
return True
|
300
|
+
|
301
|
+
def _get_management_account_id(self) -> Optional[str]:
|
302
|
+
"""Get the management account ID"""
|
303
|
+
try:
|
304
|
+
orgs_client = self.base_session.client('organizations')
|
305
|
+
org_info = orgs_client.describe_organization()
|
306
|
+
return org_info['Organization']['MasterAccountId']
|
307
|
+
except ClientError:
|
308
|
+
return None
|
309
|
+
|
310
|
+
def display_organization_summary(self, structured_accounts: Dict[str, List[EnhancedAccountInfo]]):
|
311
|
+
"""
|
312
|
+
Display organization summary with Rich CLI formatting
|
313
|
+
Enterprise-ready visualization of multi-account structure
|
314
|
+
"""
|
315
|
+
print_header("Organization Structure Summary", __version__)
|
316
|
+
|
317
|
+
# Create summary table
|
318
|
+
table = create_table(
|
319
|
+
title="Multi-Account Organization Structure",
|
320
|
+
caption=f"Discovered via profile: {self.profile}"
|
321
|
+
)
|
322
|
+
|
323
|
+
table.add_column("Organizational Unit", style="cyan", no_wrap=True)
|
324
|
+
table.add_column("Account Count", justify="right", style="green")
|
325
|
+
table.add_column("Account Types", style="blue")
|
326
|
+
table.add_column("Status", style="yellow")
|
327
|
+
|
328
|
+
total_accounts = 0
|
329
|
+
management_accounts = 0
|
330
|
+
member_accounts = 0
|
331
|
+
|
332
|
+
for ou_key, accounts in structured_accounts.items():
|
333
|
+
account_types = []
|
334
|
+
active_count = 0
|
335
|
+
|
336
|
+
for account in accounts:
|
337
|
+
total_accounts += 1
|
338
|
+
if account.account_type == 'management':
|
339
|
+
management_accounts += 1
|
340
|
+
account_types.append('Management')
|
341
|
+
else:
|
342
|
+
member_accounts += 1
|
343
|
+
account_types.append('Member')
|
344
|
+
|
345
|
+
if account.account_status == 'ACTIVE':
|
346
|
+
active_count += 1
|
347
|
+
|
348
|
+
ou_display = ou_key.replace('ORGANIZATIONAL_UNIT:', 'OU: ').replace('ROOT:', 'Root: ')
|
349
|
+
table.add_row(
|
350
|
+
ou_display,
|
351
|
+
str(len(accounts)),
|
352
|
+
', '.join(set(account_types)),
|
353
|
+
f"{active_count}/{len(accounts)} Active"
|
354
|
+
)
|
355
|
+
|
356
|
+
console.print(table)
|
357
|
+
|
358
|
+
# Summary panel
|
359
|
+
summary_text = f"""
|
360
|
+
Total Accounts: {total_accounts}
|
361
|
+
Management Accounts: {management_accounts}
|
362
|
+
Member Accounts: {member_accounts}
|
363
|
+
Organizational Units: {len(structured_accounts)}
|
364
|
+
Session Cache: {len(self.session_cache)} active sessions
|
365
|
+
"""
|
366
|
+
|
367
|
+
summary_panel = create_panel(
|
368
|
+
summary_text,
|
369
|
+
title="Organization Summary",
|
370
|
+
style="green"
|
371
|
+
)
|
372
|
+
console.print(summary_panel)
|
373
|
+
|
374
|
+
|
375
|
+
async def main():
|
376
|
+
"""
|
377
|
+
Demonstration of Cloud Foundations integration
|
378
|
+
Shows enhanced multi-account discovery capabilities
|
379
|
+
"""
|
380
|
+
import argparse
|
381
|
+
|
382
|
+
parser = argparse.ArgumentParser(
|
383
|
+
description="Cloud Foundations Integration - Enhanced Multi-Account Discovery"
|
384
|
+
)
|
385
|
+
parser.add_argument(
|
386
|
+
'--profile',
|
387
|
+
help='AWS profile to use (defaults to management profile detection)'
|
388
|
+
)
|
389
|
+
args = parser.parse_args()
|
390
|
+
|
391
|
+
# Initialize enhanced account manager
|
392
|
+
try:
|
393
|
+
account_manager = CloudFoundationsAccountManager(profile=args.profile)
|
394
|
+
|
395
|
+
# Discover organization structure
|
396
|
+
structured_accounts = await account_manager.discover_organization_structure()
|
397
|
+
|
398
|
+
# Display results with Rich formatting
|
399
|
+
account_manager.display_organization_summary(structured_accounts)
|
400
|
+
|
401
|
+
print_success("Cloud Foundations integration demonstration completed successfully")
|
402
|
+
|
403
|
+
except Exception as e:
|
404
|
+
print_error(f"Integration demonstration failed: {e}")
|
405
|
+
raise
|
406
|
+
|
407
|
+
|
408
|
+
if __name__ == "__main__":
|
409
|
+
asyncio.run(main())
|