runbooks 1.1.2__py3-none-any.whl → 1.1.4__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 (94) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
  3. runbooks/cfat/assessment/compliance.py +7 -7
  4. runbooks/cfat/models.py +6 -2
  5. runbooks/cfat/tests/__init__.py +6 -1
  6. runbooks/cli/__init__.py +13 -0
  7. runbooks/cli/commands/cfat.py +233 -0
  8. runbooks/cli/commands/finops.py +213 -0
  9. runbooks/cli/commands/inventory.py +276 -0
  10. runbooks/cli/commands/operate.py +266 -0
  11. runbooks/cli/commands/security.py +224 -0
  12. runbooks/cli/commands/validation.py +411 -0
  13. runbooks/cli/commands/vpc.py +246 -0
  14. runbooks/cli/registry.py +95 -0
  15. runbooks/cloudops/__init__.py +3 -3
  16. runbooks/cloudops/cost_optimizer.py +164 -28
  17. runbooks/cloudops/interfaces.py +2 -2
  18. runbooks/cloudops/mcp_cost_validation.py +3 -3
  19. runbooks/cloudops/notebook_framework.py +2 -2
  20. runbooks/common/aws_profile_manager.py +337 -0
  21. runbooks/common/aws_utils.py +1 -1
  22. runbooks/common/business_logic.py +3 -3
  23. runbooks/common/comprehensive_cost_explorer_integration.py +1 -1
  24. runbooks/common/cross_account_manager.py +1 -1
  25. runbooks/common/decorators.py +225 -0
  26. runbooks/common/mcp_cost_explorer_integration.py +2 -2
  27. runbooks/common/organizations_client.py +1 -1
  28. runbooks/common/patterns.py +206 -0
  29. runbooks/common/profile_utils.py +149 -14
  30. runbooks/common/rich_utils.py +507 -16
  31. runbooks/finops/README.md +11 -11
  32. runbooks/finops/__init__.py +4 -4
  33. runbooks/finops/business_cases.py +3 -3
  34. runbooks/finops/cli.py +169 -103
  35. runbooks/finops/cost_optimizer.py +4 -4
  36. runbooks/finops/dashboard_router.py +2 -2
  37. runbooks/finops/ebs_cost_optimizer.py +4 -4
  38. runbooks/finops/ebs_optimizer.py +19 -2
  39. runbooks/finops/embedded_mcp_validator.py +101 -23
  40. runbooks/finops/enhanced_progress.py +8 -8
  41. runbooks/finops/enterprise_wrappers.py +7 -7
  42. runbooks/finops/finops_scenarios.py +101 -27
  43. runbooks/finops/legacy_migration.py +8 -8
  44. runbooks/finops/markdown_exporter.py +2 -2
  45. runbooks/finops/multi_dashboard.py +1 -1
  46. runbooks/finops/nat_gateway_optimizer.py +1 -1
  47. runbooks/finops/optimizer.py +6 -6
  48. runbooks/finops/rds_snapshot_optimizer.py +1389 -0
  49. runbooks/finops/scenario_cli_integration.py +13 -13
  50. runbooks/finops/scenarios.py +16 -16
  51. runbooks/finops/single_dashboard.py +10 -10
  52. runbooks/finops/tests/test_finops_dashboard.py +3 -3
  53. runbooks/finops/tests/test_reference_images_validation.py +2 -2
  54. runbooks/finops/tests/test_single_account_features.py +17 -17
  55. runbooks/finops/tests/validate_test_suite.py +1 -1
  56. runbooks/finops/validation_framework.py +5 -5
  57. runbooks/finops/vpc_cleanup_exporter.py +3 -3
  58. runbooks/finops/vpc_cleanup_optimizer.py +3 -3
  59. runbooks/finops/workspaces_analyzer.py +31 -13
  60. runbooks/hitl/enhanced_workflow_engine.py +1 -1
  61. runbooks/inventory/README.md +3 -3
  62. runbooks/inventory/Tests/common_test_data.py +30 -30
  63. runbooks/inventory/collectors/aws_comprehensive.py +28 -11
  64. runbooks/inventory/collectors/aws_networking.py +2 -2
  65. runbooks/inventory/discovery.md +2 -2
  66. runbooks/inventory/find_ec2_security_groups.py +1 -1
  67. runbooks/inventory/list_rds_snapshots_aggregator.py +745 -0
  68. runbooks/inventory/organizations_discovery.py +1 -1
  69. runbooks/inventory/vpc_analyzer.py +1 -1
  70. runbooks/inventory/vpc_flow_analyzer.py +2 -2
  71. runbooks/main.py +143 -8882
  72. runbooks/metrics/dora_metrics_engine.py +2 -2
  73. runbooks/operate/mcp_integration.py +1 -1
  74. runbooks/operate/networking_cost_heatmap.py +4 -2
  75. runbooks/operate/privatelink_operations.py +1 -1
  76. runbooks/operate/vpc_endpoints.py +1 -1
  77. runbooks/operate/vpc_operations.py +2 -2
  78. runbooks/remediation/commvault_ec2_analysis.py +1 -1
  79. runbooks/remediation/rds_snapshot_list.py +5 -5
  80. runbooks/remediation/workspaces_list.py +5 -5
  81. runbooks/security/integration_test_enterprise_security.py +5 -3
  82. runbooks/security/run_script.py +1 -1
  83. runbooks/sre/mcp_reliability_engine.py +6 -6
  84. runbooks/utils/version_validator.py +1 -1
  85. runbooks/validation/comprehensive_2way_validator.py +9 -4
  86. runbooks/vpc/heatmap_engine.py +7 -4
  87. runbooks/vpc/mcp_no_eni_validator.py +1 -1
  88. runbooks/vpc/unified_scenarios.py +7 -7
  89. {runbooks-1.1.2.dist-info → runbooks-1.1.4.dist-info}/METADATA +53 -52
  90. {runbooks-1.1.2.dist-info → runbooks-1.1.4.dist-info}/RECORD +94 -80
  91. {runbooks-1.1.2.dist-info → runbooks-1.1.4.dist-info}/WHEEL +0 -0
  92. {runbooks-1.1.2.dist-info → runbooks-1.1.4.dist-info}/entry_points.txt +0 -0
  93. {runbooks-1.1.2.dist-info → runbooks-1.1.4.dist-info}/licenses/LICENSE +0 -0
  94. {runbooks-1.1.2.dist-info → runbooks-1.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,206 @@
1
+ """
2
+ DRY Pattern Manager - Eliminate Pattern Duplication
3
+
4
+ Following Claude Code best practices for memory efficiency and enterprise
5
+ architecture patterns for systematic deduplication.
6
+
7
+ Official Claude Code Limitation: Context window optimization requires eliminating
8
+ redundant patterns to maximize available context for complex operations.
9
+
10
+ Enterprise Best Practice: Single source of truth for all reusable patterns
11
+ prevents inconsistencies and reduces maintenance overhead.
12
+ """
13
+
14
+ from typing import Any, Dict, Optional, Callable
15
+ from functools import lru_cache
16
+ import click
17
+ from rich.console import Console
18
+ from rich.markup import escape
19
+
20
+
21
+ class DRYPatternManager:
22
+ """
23
+ Don't Repeat Yourself - Load patterns once, reference everywhere.
24
+
25
+ Following Claude Code optimization principles:
26
+ - Single pattern registry (no duplicates)
27
+ - Reference-based access (@ notation concept)
28
+ - Memory efficiency through lazy loading
29
+ - Consistent patterns across all modules
30
+
31
+ Enterprise Architecture:
32
+ - Centralized pattern management
33
+ - Type-safe pattern access
34
+ - Cached pattern instances
35
+ - Extensible pattern registry
36
+ """
37
+
38
+ _patterns: Dict[str, Any] = {}
39
+ _console: Optional[Console] = None
40
+ _loaded: bool = False
41
+
42
+ @classmethod
43
+ @lru_cache(maxsize=None)
44
+ def get_console(cls) -> Console:
45
+ """
46
+ Single console instance for all modules.
47
+
48
+ Claude Code Best Practice: Reuse console objects to reduce memory overhead
49
+ and ensure consistent formatting across all CLI operations.
50
+ """
51
+ if cls._console is None:
52
+ cls._console = Console()
53
+ return cls._console
54
+
55
+ @classmethod
56
+ def get_pattern(cls, name: str) -> Any:
57
+ """
58
+ Get pattern by name with lazy loading.
59
+
60
+ Args:
61
+ name: Pattern identifier (e.g., 'error_handlers', 'click_group')
62
+
63
+ Returns:
64
+ Cached pattern instance
65
+
66
+ Raises:
67
+ KeyError: If pattern not found
68
+ """
69
+ if not cls._loaded:
70
+ cls._load_all_patterns()
71
+
72
+ if name not in cls._patterns:
73
+ raise KeyError(f"Pattern '{name}' not found. Available: {list(cls._patterns.keys())}")
74
+
75
+ return cls._patterns[name]
76
+
77
+ @classmethod
78
+ def _load_all_patterns(cls):
79
+ """Load all patterns once - DRY principle implementation."""
80
+ if cls._loaded:
81
+ return
82
+
83
+ # Common import patterns
84
+ cls._patterns['click'] = click
85
+ cls._patterns['console'] = cls.get_console()
86
+
87
+ # Error handling patterns
88
+ cls._patterns['error_handlers'] = cls._create_error_handlers()
89
+
90
+ # Click group patterns
91
+ cls._patterns['click_group'] = cls._create_click_group_pattern()
92
+
93
+ # Common CLI decorators reference
94
+ cls._patterns['common_decorators'] = cls._get_common_decorators()
95
+
96
+ cls._loaded = True
97
+
98
+ @classmethod
99
+ def _create_error_handlers(cls) -> Dict[str, Callable]:
100
+ """
101
+ Centralized error handling patterns.
102
+
103
+ Eliminates 19 instances of duplicated error messages across CLI modules.
104
+ """
105
+ console = cls.get_console()
106
+
107
+ def module_not_available_error(module_name: str, error: Exception):
108
+ """Standardized 'module not available' error handler."""
109
+ console.print(f"[red]❌ {module_name} module not available: {error}[/red]")
110
+
111
+ def operation_failed_error(operation_name: str, error: Exception):
112
+ """Standardized 'operation failed' error handler."""
113
+ console.print(f"[red]❌ {operation_name} failed: {error}[/red]")
114
+
115
+ def success_message(message: str, details: Optional[str] = None):
116
+ """Standardized success message."""
117
+ console.print(f"[green]✅ {message}[/green]")
118
+ if details:
119
+ console.print(f"[dim]{details}[/dim]")
120
+
121
+ return {
122
+ 'module_not_available': module_not_available_error,
123
+ 'operation_failed': operation_failed_error,
124
+ 'success': success_message
125
+ }
126
+
127
+ @classmethod
128
+ def _create_click_group_pattern(cls) -> Callable:
129
+ """
130
+ Standardized Click group creation pattern.
131
+
132
+ Eliminates 6 instances of identical @click.group patterns.
133
+ """
134
+ def create_group(name: str, help_text: str, invoke_without_command: bool = True):
135
+ """Create standardized Click group with common options."""
136
+
137
+ @click.group(invoke_without_command=invoke_without_command)
138
+ @click.pass_context
139
+ def group(ctx):
140
+ if ctx.invoked_subcommand is None:
141
+ click.echo(f"{name.title()} Commands:")
142
+ click.echo(help_text)
143
+
144
+ # Apply consistent group naming
145
+ group.name = name
146
+ group.__doc__ = help_text
147
+
148
+ return group
149
+
150
+ return create_group
151
+
152
+ @classmethod
153
+ def _get_common_decorators(cls):
154
+ """Reference to common decorators - avoid importing in every module."""
155
+ try:
156
+ from runbooks.common.decorators import (
157
+ common_aws_options,
158
+ common_output_options,
159
+ common_filter_options
160
+ )
161
+ return {
162
+ 'aws_options': common_aws_options,
163
+ 'output_options': common_output_options,
164
+ 'filter_options': common_filter_options
165
+ }
166
+ except ImportError:
167
+ # Graceful degradation if decorators not available
168
+ return {}
169
+
170
+
171
+ # Convenience functions for direct pattern access
172
+ def get_console() -> Console:
173
+ """Get shared console instance - replaces individual console = Console() calls."""
174
+ return DRYPatternManager.get_console()
175
+
176
+
177
+ def get_error_handlers() -> Dict[str, Callable]:
178
+ """Get standardized error handlers."""
179
+ return DRYPatternManager.get_pattern('error_handlers')
180
+
181
+
182
+ def get_click_group_creator() -> Callable:
183
+ """Get standardized Click group creator."""
184
+ return DRYPatternManager.get_pattern('click_group')
185
+
186
+
187
+ def get_common_decorators() -> Dict[str, Any]:
188
+ """Get common CLI decorators."""
189
+ return DRYPatternManager.get_pattern('common_decorators')
190
+
191
+
192
+ # Pattern registry status for monitoring
193
+ def get_pattern_registry_status() -> Dict[str, Any]:
194
+ """
195
+ Get DRY pattern registry status for monitoring and optimization.
196
+
197
+ Returns:
198
+ Dictionary containing registry status, loaded patterns, and memory efficiency metrics
199
+ """
200
+ return {
201
+ 'loaded': DRYPatternManager._loaded,
202
+ 'pattern_count': len(DRYPatternManager._patterns),
203
+ 'available_patterns': list(DRYPatternManager._patterns.keys()),
204
+ 'console_shared': DRYPatternManager._console is not None,
205
+ 'memory_efficiency': 'Single instance sharing active'
206
+ }
@@ -5,6 +5,7 @@ import boto3
5
5
  from typing import Optional, List, Dict, Any
6
6
  from runbooks.common.rich_utils import console
7
7
  from datetime import datetime
8
+ from botocore.config import Config
8
9
 
9
10
  # Enhanced caching system for enterprise performance
10
11
  _profile_cache: Dict[str, str] = {}
@@ -14,6 +15,16 @@ _cache_ttl: int = 300 # 5 minutes TTL for enterprise session management
14
15
  _session_id: Optional[str] = None # Track session consistency
15
16
  _session_cache: Dict[str, boto3.Session] = {} # Cache AWS sessions
16
17
 
18
+ # Timeout configuration for AWS API calls (prevents execution flow hangs)
19
+ _AWS_CLIENT_CONFIG = Config(
20
+ connect_timeout=30, # Connection timeout: 30 seconds
21
+ read_timeout=60, # Read timeout: 60 seconds
22
+ retries={
23
+ 'max_attempts': 3,
24
+ 'mode': 'adaptive'
25
+ }
26
+ )
27
+
17
28
  def _get_session_id() -> str:
18
29
  """Generate consistent session ID for cache scoping"""
19
30
  global _session_id
@@ -169,45 +180,147 @@ def get_account_id_from_profile(profile_name: str) -> Optional[str]:
169
180
 
170
181
  def create_cost_session(profile_name: Optional[str] = None) -> boto3.Session:
171
182
  """
172
- Create AWS session optimized for cost operations (Cost Explorer).
183
+ Create AWS session optimized for cost operations (Cost Explorer) with token error handling.
173
184
 
174
185
  Args:
175
186
  profile_name: AWS profile name for cost operations
176
187
 
177
188
  Returns:
178
189
  Configured boto3 Session for cost operations
190
+
191
+ Raises:
192
+ SystemExit: When authentication fails with clear user guidance
179
193
  """
194
+ from botocore.exceptions import TokenRetrievalError, NoCredentialsError
195
+ from .rich_utils import console, print_error, print_info
196
+
180
197
  cost_profile = get_profile_for_operation("billing", profile_name)
181
198
 
182
- # Use cached session if available
199
+ # Use cached session if available and validate it's still working
183
200
  session_key = f"cost:{cost_profile}"
184
201
  if session_key in _session_cache:
185
- return _session_cache[session_key]
202
+ cached_session = _session_cache[session_key]
203
+ # Quick validation that the cached session still works
204
+ try:
205
+ # Test with a minimal STS call to check if credentials are valid (with timeout)
206
+ sts_client = cached_session.client('sts', config=_AWS_CLIENT_CONFIG)
207
+ sts_client.get_caller_identity()
208
+ return cached_session
209
+ except (TokenRetrievalError, NoCredentialsError):
210
+ # Remove invalid cached session
211
+ del _session_cache[session_key]
212
+ console.log("[yellow]⚠️ Cached session expired, creating new session[/]")
186
213
 
187
- session = boto3.Session(profile_name=cost_profile)
188
- _session_cache[session_key] = session
189
- return session
214
+ try:
215
+ session = boto3.Session(profile_name=cost_profile)
216
+ # Test the session to ensure credentials are valid (with timeout)
217
+ sts_client = session.client('sts', config=_AWS_CLIENT_CONFIG)
218
+ sts_client.get_caller_identity()
219
+
220
+ # Cache only if session works
221
+ _session_cache[session_key] = session
222
+ return session
223
+
224
+ except TokenRetrievalError as e:
225
+ print_error("🔐 AWS SSO token has expired")
226
+ print_info("💡 To fix this issue:")
227
+ print_info(f" 1. Run: [cyan]aws sso login --profile {cost_profile}[/]")
228
+ print_info(" 2. Or try: [cyan]aws sso login[/] (if using default profile)")
229
+ print_info(" 3. Verify your internet connection")
230
+ print_info(" 4. Check if your AWS SSO session has expired")
231
+ console.log(f"[dim]Profile used: {cost_profile}[/]")
232
+ console.log(f"[dim]Error details: {str(e)}[/]")
233
+ raise SystemExit(1)
234
+
235
+ except NoCredentialsError as e:
236
+ print_error("🔐 No AWS credentials configured")
237
+ print_info("💡 To fix this issue:")
238
+ print_info(" 1. Configure AWS CLI: [cyan]aws configure[/]")
239
+ print_info(" 2. Or setup SSO: [cyan]aws configure sso[/]")
240
+ print_info(f" 3. Or set profile: [cyan]export AWS_PROFILE={cost_profile}[/]")
241
+ console.log(f"[dim]Profile attempted: {cost_profile}[/]")
242
+ raise SystemExit(1)
243
+
244
+ except Exception as e:
245
+ print_error(f"🔐 Authentication failed for profile: {cost_profile}")
246
+ print_info("💡 To fix this issue:")
247
+ print_info(" 1. Verify the profile exists: [cyan]aws configure list-profiles[/]")
248
+ print_info(" 2. Check profile permissions for cost analysis")
249
+ print_info(" 3. Ensure profile has Cost Explorer access")
250
+ console.log(f"[dim]Error details: {str(e)}[/]")
251
+ raise SystemExit(1)
190
252
 
191
253
  def create_management_session(profile_name: Optional[str] = None) -> boto3.Session:
192
254
  """
193
- Create AWS session optimized for management operations (Organizations).
255
+ Create AWS session optimized for management operations (Organizations) with token error handling.
194
256
 
195
257
  Args:
196
258
  profile_name: AWS profile name for management operations
197
259
 
198
260
  Returns:
199
261
  Configured boto3 Session for management operations
262
+
263
+ Raises:
264
+ SystemExit: When authentication fails with clear user guidance
200
265
  """
266
+ from botocore.exceptions import TokenRetrievalError, NoCredentialsError
267
+ from .rich_utils import console, print_error, print_info
268
+
201
269
  mgmt_profile = get_profile_for_operation("management", profile_name)
202
270
 
203
- # Use cached session if available
271
+ # Use cached session if available and validate it's still working
204
272
  session_key = f"management:{mgmt_profile}"
205
273
  if session_key in _session_cache:
206
- return _session_cache[session_key]
274
+ cached_session = _session_cache[session_key]
275
+ # Quick validation that the cached session still works
276
+ try:
277
+ # Test with a minimal STS call to check if credentials are valid (with timeout)
278
+ sts_client = cached_session.client('sts', config=_AWS_CLIENT_CONFIG)
279
+ sts_client.get_caller_identity()
280
+ return cached_session
281
+ except (TokenRetrievalError, NoCredentialsError):
282
+ # Remove invalid cached session
283
+ del _session_cache[session_key]
284
+ console.log("[yellow]⚠️ Cached session expired, creating new session[/]")
207
285
 
208
- session = boto3.Session(profile_name=mgmt_profile)
209
- _session_cache[session_key] = session
210
- return session
286
+ try:
287
+ session = boto3.Session(profile_name=mgmt_profile)
288
+ # Test the session to ensure credentials are valid (with timeout)
289
+ sts_client = session.client('sts', config=_AWS_CLIENT_CONFIG)
290
+ sts_client.get_caller_identity()
291
+
292
+ # Cache only if session works
293
+ _session_cache[session_key] = session
294
+ return session
295
+
296
+ except TokenRetrievalError as e:
297
+ print_error("🔐 AWS SSO token has expired")
298
+ print_info("💡 To fix this issue:")
299
+ print_info(f" 1. Run: [cyan]aws sso login --profile {mgmt_profile}[/]")
300
+ print_info(" 2. Or try: [cyan]aws sso login[/] (if using default profile)")
301
+ print_info(" 3. Verify your internet connection")
302
+ print_info(" 4. Check if your AWS SSO session has expired")
303
+ console.log(f"[dim]Profile used: {mgmt_profile}[/]")
304
+ console.log(f"[dim]Error details: {str(e)}[/]")
305
+ raise SystemExit(1)
306
+
307
+ except NoCredentialsError as e:
308
+ print_error("🔐 No AWS credentials configured")
309
+ print_info("💡 To fix this issue:")
310
+ print_info(" 1. Configure AWS CLI: [cyan]aws configure[/]")
311
+ print_info(" 2. Or setup SSO: [cyan]aws configure sso[/]")
312
+ print_info(f" 3. Or set profile: [cyan]export AWS_PROFILE={mgmt_profile}[/]")
313
+ console.log(f"[dim]Profile attempted: {mgmt_profile}[/]")
314
+ raise SystemExit(1)
315
+
316
+ except Exception as e:
317
+ print_error(f"🔐 Authentication failed for profile: {mgmt_profile}")
318
+ print_info("💡 To fix this issue:")
319
+ print_info(" 1. Verify the profile exists: [cyan]aws configure list-profiles[/]")
320
+ print_info(" 2. Check profile permissions for management operations")
321
+ print_info(" 3. Ensure profile has Organizations access")
322
+ console.log(f"[dim]Error details: {str(e)}[/]")
323
+ raise SystemExit(1)
211
324
 
212
325
  def create_operational_session(profile_name: Optional[str] = None) -> boto3.Session:
213
326
  """
@@ -242,7 +355,7 @@ def get_current_profile_info(profile_name: Optional[str] = None) -> Dict[str, An
242
355
  """
243
356
  try:
244
357
  session = boto3.Session(profile_name=profile_name)
245
- sts_client = session.client('sts')
358
+ sts_client = session.client('sts', config=_AWS_CLIENT_CONFIG)
246
359
  identity = sts_client.get_caller_identity()
247
360
 
248
361
  return {
@@ -307,4 +420,26 @@ def clear_profile_cache() -> None:
307
420
  _validation_cache.clear()
308
421
  _session_cache.clear()
309
422
  _cache_timestamp = None
310
- _session_id = None
423
+ _session_id = None
424
+
425
+ def create_timeout_protected_client(session: boto3.Session, service_name: str, region_name: Optional[str] = None):
426
+ """
427
+ Create AWS service client with timeout protection to prevent execution flow hangs.
428
+
429
+ This function should be used by all FinOps modules to create AWS clients with
430
+ enterprise-grade timeout protection and retry configuration.
431
+
432
+ Args:
433
+ session: boto3 Session to use for client creation
434
+ service_name: AWS service name (e.g., 'ec2', 'ce', 'workspaces', 'rds')
435
+ region_name: AWS region name (optional)
436
+
437
+ Returns:
438
+ AWS service client with timeout protection
439
+
440
+ Example:
441
+ session = create_cost_session()
442
+ ce_client = create_timeout_protected_client(session, 'ce', 'us-east-1')
443
+ ec2_client = create_timeout_protected_client(session, 'ec2', region_name)
444
+ """
445
+ return session.client(service_name, region_name=region_name, config=_AWS_CLIENT_CONFIG)