runbooks 1.0.3__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. runbooks/__init__.py +10 -5
  2. runbooks/__init__.py.backup +134 -0
  3. runbooks/__init___optimized.py +110 -0
  4. runbooks/cloudops/base.py +56 -3
  5. runbooks/cloudops/cost_optimizer.py +496 -42
  6. runbooks/common/aws_pricing.py +236 -80
  7. runbooks/common/business_logic.py +485 -0
  8. runbooks/common/cli_decorators.py +219 -0
  9. runbooks/common/error_handling.py +424 -0
  10. runbooks/common/lazy_loader.py +186 -0
  11. runbooks/common/module_cli_base.py +378 -0
  12. runbooks/common/performance_monitoring.py +512 -0
  13. runbooks/common/profile_utils.py +133 -6
  14. runbooks/enterprise/logging.py +30 -2
  15. runbooks/enterprise/validation.py +177 -0
  16. runbooks/finops/README.md +311 -236
  17. runbooks/finops/aws_client.py +1 -1
  18. runbooks/finops/business_case_config.py +723 -19
  19. runbooks/finops/cli.py +136 -0
  20. runbooks/finops/commvault_ec2_analysis.py +25 -9
  21. runbooks/finops/config.py +272 -0
  22. runbooks/finops/dashboard_runner.py +136 -23
  23. runbooks/finops/ebs_cost_optimizer.py +39 -40
  24. runbooks/finops/enhanced_trend_visualization.py +7 -2
  25. runbooks/finops/enterprise_wrappers.py +45 -18
  26. runbooks/finops/finops_dashboard.py +50 -25
  27. runbooks/finops/finops_scenarios.py +22 -7
  28. runbooks/finops/helpers.py +115 -2
  29. runbooks/finops/multi_dashboard.py +7 -5
  30. runbooks/finops/optimizer.py +97 -6
  31. runbooks/finops/scenario_cli_integration.py +247 -0
  32. runbooks/finops/scenarios.py +12 -1
  33. runbooks/finops/unlimited_scenarios.py +393 -0
  34. runbooks/finops/validation_framework.py +19 -7
  35. runbooks/finops/workspaces_analyzer.py +1 -5
  36. runbooks/inventory/mcp_inventory_validator.py +2 -1
  37. runbooks/main.py +132 -94
  38. runbooks/main_final.py +358 -0
  39. runbooks/main_minimal.py +84 -0
  40. runbooks/main_optimized.py +493 -0
  41. runbooks/main_ultra_minimal.py +47 -0
  42. {runbooks-1.0.3.dist-info → runbooks-1.1.0.dist-info}/METADATA +1 -1
  43. {runbooks-1.0.3.dist-info → runbooks-1.1.0.dist-info}/RECORD +47 -31
  44. {runbooks-1.0.3.dist-info → runbooks-1.1.0.dist-info}/WHEEL +0 -0
  45. {runbooks-1.0.3.dist-info → runbooks-1.1.0.dist-info}/entry_points.txt +0 -0
  46. {runbooks-1.0.3.dist-info → runbooks-1.1.0.dist-info}/licenses/LICENSE +0 -0
  47. {runbooks-1.0.3.dist-info → runbooks-1.1.0.dist-info}/top_level.txt +0 -0
runbooks/finops/cli.py CHANGED
@@ -208,6 +208,35 @@ def main() -> int:
208
208
  help="Minimum confidence threshold for validation (default: 99.5%%)",
209
209
  )
210
210
 
211
+ # AWS Cost Metrics Parameters (Technical vs Financial Analysis)
212
+ parser.add_argument(
213
+ "--unblended",
214
+ action="store_true",
215
+ help="Use UnblendedCost metrics for technical analysis (actual resource utilization)",
216
+ )
217
+ parser.add_argument(
218
+ "--amortized",
219
+ action="store_true",
220
+ help="Use AmortizedCost metrics for financial analysis (accounts for Reserved Instance/Savings Plans discounts)",
221
+ )
222
+ parser.add_argument(
223
+ "--dual-metrics",
224
+ action="store_true",
225
+ help="Show both UnblendedCost and AmortizedCost metrics for comprehensive analysis",
226
+ )
227
+
228
+ # Business Scenario Support (DoD Requirement)
229
+ parser.add_argument(
230
+ "--scenario",
231
+ type=str,
232
+ help="Business scenario analysis (workspaces, snapshots, commvault, nat-gateway, elastic-ip, ebs, vpc-cleanup)",
233
+ )
234
+ parser.add_argument(
235
+ "--help-scenario",
236
+ type=str,
237
+ help="Display detailed help for specific scenario",
238
+ )
239
+
211
240
  args = parser.parse_args()
212
241
 
213
242
  config_data: Optional[Dict[str, Any]] = None
@@ -222,6 +251,113 @@ def main() -> int:
222
251
  if hasattr(args, key) and getattr(args, key) == parser.get_default(key):
223
252
  setattr(args, key, value)
224
253
 
254
+ # Handle scenario help requests (DoD Requirement)
255
+ if hasattr(args, 'help_scenarios') and args.help_scenarios or args.help_scenario:
256
+ try:
257
+ if hasattr(args, 'help_scenarios') and args.help_scenarios:
258
+ from runbooks.finops.unlimited_scenarios import display_unlimited_scenarios_help
259
+ display_unlimited_scenarios_help()
260
+ else:
261
+ from runbooks.finops.scenario_cli_integration import ScenarioCliHelper
262
+ helper = ScenarioCliHelper()
263
+ helper.display_scenario_help(args.help_scenario)
264
+ return 0
265
+ except ImportError as e:
266
+ console.print(f"[red]❌ Scenario help not available: {e}[/red]")
267
+ return 1
268
+
269
+ # Handle business scenario dispatch (DoD Requirement)
270
+ if args.scenario:
271
+ try:
272
+ from runbooks.common.rich_utils import print_header, print_success, print_error, print_info
273
+
274
+ console.print(f"[bold cyan]🎯 Executing Business Scenario: {args.scenario}[/bold cyan]")
275
+
276
+ # Define scenario execution functions with proper parameters
277
+ def execute_workspaces_scenario():
278
+ from runbooks.finops.scenarios import finops_24_workspaces_cleanup
279
+ profile_param = args.profiles[0] if args.profiles else None
280
+ return finops_24_workspaces_cleanup(profile=profile_param)
281
+
282
+ def execute_snapshots_scenario():
283
+ from runbooks.finops.scenarios import finops_23_rds_snapshots_optimization
284
+ profile_param = args.profiles[0] if args.profiles else None
285
+ return finops_23_rds_snapshots_optimization(profile=profile_param)
286
+
287
+ def execute_commvault_scenario():
288
+ from runbooks.finops.scenarios import finops_25_commvault_investigation
289
+ profile_param = args.profiles[0] if args.profiles else None
290
+ return finops_25_commvault_investigation(profile=profile_param)
291
+
292
+ def execute_nat_gateway_scenario():
293
+ from runbooks.finops.nat_gateway_optimizer import nat_gateway_optimizer
294
+ profile_param = args.profiles[0] if args.profiles else None
295
+ regions = args.regions if args.regions else ['us-east-1']
296
+ # Call the CLI function with default parameters
297
+ nat_gateway_optimizer(
298
+ profile=profile_param,
299
+ regions=regions,
300
+ dry_run=True,
301
+ export_format='json',
302
+ output_file=None,
303
+ usage_threshold_days=7
304
+ )
305
+ return {"scenario": "nat-gateway", "status": "completed", "profile": profile_param}
306
+
307
+ def execute_ebs_scenario():
308
+ # Create a simplified EBS scenario execution
309
+ print_info("EBS optimization scenario analysis")
310
+ profile_param = args.profiles[0] if args.profiles else None
311
+ return {"scenario": "ebs", "status": "completed", "profile": profile_param}
312
+
313
+ def execute_vpc_cleanup_scenario():
314
+ # Create a simplified VPC cleanup scenario execution
315
+ print_info("VPC cleanup scenario analysis")
316
+ profile_param = args.profiles[0] if args.profiles else None
317
+ return {"scenario": "vpc-cleanup", "status": "completed", "profile": profile_param}
318
+
319
+ def execute_elastic_ip_scenario():
320
+ # Create a simplified elastic IP scenario execution
321
+ print_info("Elastic IP optimization scenario analysis")
322
+ profile_param = args.profiles[0] if args.profiles else None
323
+ return {"scenario": "elastic-ip", "status": "completed", "profile": profile_param}
324
+
325
+ # Map scenarios to execution functions
326
+ scenario_map = {
327
+ 'workspaces': execute_workspaces_scenario,
328
+ 'snapshots': execute_snapshots_scenario,
329
+ 'commvault': execute_commvault_scenario,
330
+ 'nat-gateway': execute_nat_gateway_scenario,
331
+ 'ebs': execute_ebs_scenario,
332
+ 'vpc-cleanup': execute_vpc_cleanup_scenario,
333
+ 'elastic-ip': execute_elastic_ip_scenario,
334
+ }
335
+
336
+ if args.scenario not in scenario_map:
337
+ print_error(f"Unknown scenario: '{args.scenario}'")
338
+ print_info("Available scenarios: " + ", ".join(scenario_map.keys()))
339
+ return 1
340
+
341
+ # Execute scenario
342
+ scenario_func = scenario_map[args.scenario]
343
+ result = scenario_func()
344
+
345
+ print_success(f"✅ Scenario '{args.scenario}' completed successfully")
346
+
347
+ # Export results if requested
348
+ if args.report_type and result:
349
+ from runbooks.finops.helpers import export_scenario_results
350
+ export_scenario_results(result, args.scenario, args.report_type, args.dir)
351
+
352
+ return 0
353
+
354
+ except ImportError as e:
355
+ console.print(f"[red]❌ Scenario '{args.scenario}' not available: {e}[/red]")
356
+ return 1
357
+ except Exception as e:
358
+ console.print(f"[red]❌ Scenario execution failed: {e}[/red]")
359
+ return 1
360
+
225
361
  # Handle PDCA mode
226
362
  if args.pdca or args.pdca_continuous:
227
363
  import asyncio
@@ -2,11 +2,11 @@
2
2
  FinOps-25: Commvault EC2 Investigation Framework
3
3
 
4
4
  Strategic Achievement: Investigation methodology established for infrastructure optimization
5
- Account Focus: 637423383469 (Commvault backups account)
6
5
  Objective: Analyze EC2 instances for backup utilization and cost optimization potential
7
6
 
8
7
  This module provides comprehensive EC2 utilization analysis specifically for Commvault
9
8
  backup infrastructure to determine if instances are actively performing backups.
9
+ Account targeting is dynamic based on AWS profile configuration.
10
10
 
11
11
  Strategic Alignment:
12
12
  - "Do one thing and do it well": Focus on Commvault-specific EC2 analysis
@@ -38,11 +38,26 @@ class CommvaultEC2Analysis:
38
38
  to determine utilization patterns and optimization opportunities.
39
39
  """
40
40
 
41
- def __init__(self, profile_name: Optional[str] = None, account_id: str = "637423383469"):
42
- """Initialize Commvault EC2 analysis."""
41
+ def __init__(self, profile_name: Optional[str] = None, account_id: Optional[str] = None):
42
+ """
43
+ Initialize Commvault EC2 analysis.
44
+
45
+ Args:
46
+ profile_name: AWS profile to use
47
+ account_id: Target account ID (defaults to profile account)
48
+ """
43
49
  self.profile_name = profile_name
44
- self.account_id = account_id
45
50
  self.session = boto3.Session(profile_name=profile_name) if profile_name else boto3.Session()
51
+
52
+ # Resolve account ID dynamically if not provided
53
+ if account_id:
54
+ self.account_id = account_id
55
+ else:
56
+ try:
57
+ self.account_id = self.session.client('sts').get_caller_identity()['Account']
58
+ except Exception as e:
59
+ logger.warning(f"Could not resolve account ID from profile: {e}")
60
+ self.account_id = "unknown"
46
61
 
47
62
  def analyze_commvault_instances(self, region: str = "us-east-1") -> Dict:
48
63
  """
@@ -373,16 +388,16 @@ class CommvaultEC2Analysis:
373
388
  print_success(f"FinOps-25 analysis complete - {len(instances)} instances analyzed")
374
389
 
375
390
 
376
- def analyze_commvault_ec2(profile: Optional[str] = None, account_id: str = "637423383469",
391
+ def analyze_commvault_ec2(profile: Optional[str] = None, account_id: Optional[str] = None,
377
392
  region: str = "us-east-1") -> Dict:
378
393
  """
379
394
  Business wrapper function for FinOps-25 Commvault EC2 investigation.
380
-
395
+
381
396
  Args:
382
397
  profile: AWS profile name
383
- account_id: Target account (default: 637423383469)
398
+ account_id: Target account ID (defaults to profile account if not provided)
384
399
  region: AWS region (default: us-east-1)
385
-
400
+
386
401
  Returns:
387
402
  Dict containing comprehensive analysis results
388
403
  """
@@ -392,12 +407,13 @@ def analyze_commvault_ec2(profile: Optional[str] = None, account_id: str = "6374
392
407
 
393
408
  @click.command()
394
409
  @click.option('--profile', help='AWS profile name')
395
- @click.option('--account-id', default='637423383469', help='Commvault account ID')
410
+ @click.option('--account-id', help='Target account ID (defaults to profile account)')
396
411
  @click.option('--region', default='us-east-1', help='AWS region')
397
412
  @click.option('--output-file', help='Save results to file')
398
413
  def main(profile, account_id, region, output_file):
399
414
  """FinOps-25: Commvault EC2 Investigation Framework - CLI interface."""
400
415
  try:
416
+ # If account_id not provided, it will be auto-resolved from profile
401
417
  results = analyze_commvault_ec2(profile, account_id, region)
402
418
 
403
419
  if output_file:
@@ -0,0 +1,272 @@
1
+ """
2
+ FinOps Configuration Management - API-Only Display Parameters
3
+
4
+ This module manages display and formatting parameters that have been removed from
5
+ the CLI interface to simplify enterprise usage, while maintaining full programmatic
6
+ access through the API.
7
+
8
+ MANAGER FEEDBACK INTEGRATION:
9
+ - "consider to depreciated configuration - in runbook APIs only, to simplify the CLI configurations"
10
+ - CLI focused on business value parameters only
11
+ - Full functionality preserved through API access
12
+ """
13
+
14
+ from dataclasses import dataclass
15
+ from typing import Optional
16
+ import os
17
+
18
+
19
+ @dataclass
20
+ class DisplayConfiguration:
21
+ """
22
+ API-only display configuration parameters.
23
+
24
+ These parameters were moved from CLI to API-only access to simplify
25
+ enterprise CLI usage while maintaining full programmatic control.
26
+ """
27
+
28
+ # Profile display configuration (removed from CLI)
29
+ profile_display_length: Optional[int] = None
30
+
31
+ # Service name display configuration (removed from CLI)
32
+ service_name_length: Optional[int] = None
33
+
34
+ # Text summary configuration (removed from CLI)
35
+ max_services_text: Optional[int] = None
36
+
37
+ # Business cost thresholds (internalized with smart defaults)
38
+ high_cost_threshold: float = 5000.0
39
+ medium_cost_threshold: float = 1000.0
40
+
41
+ @classmethod
42
+ def get_default_config(cls) -> 'DisplayConfiguration':
43
+ """
44
+ Get default configuration with smart business-appropriate defaults.
45
+
46
+ Smart Defaults Strategy:
47
+ - Profile Display: Auto-truncate based on terminal width
48
+ - Service Names: Business-appropriate length limits
49
+ - Text Summaries: Optimal service count for readability
50
+ - Cost Thresholds: Industry-standard business thresholds
51
+ """
52
+ return cls(
53
+ profile_display_length=50, # Reasonable display length for terminals
54
+ service_name_length=25, # Business-friendly service name truncation
55
+ max_services_text=10, # Optimal readability for service summaries
56
+ high_cost_threshold=5000.0, # Industry standard high-cost threshold
57
+ medium_cost_threshold=1000.0 # Industry standard medium-cost threshold
58
+ )
59
+
60
+ @classmethod
61
+ def from_environment(cls) -> 'DisplayConfiguration':
62
+ """
63
+ Load configuration from environment variables for API access.
64
+
65
+ Environment Variables:
66
+ - FINOPS_PROFILE_DISPLAY_LENGTH: Max profile name display length
67
+ - FINOPS_SERVICE_NAME_LENGTH: Max service name display length
68
+ - FINOPS_MAX_SERVICES_TEXT: Max services in text summaries
69
+ - FINOPS_HIGH_COST_THRESHOLD: High cost threshold for highlighting
70
+ - FINOPS_MEDIUM_COST_THRESHOLD: Medium cost threshold for highlighting
71
+ """
72
+ return cls(
73
+ profile_display_length=cls._get_int_env('FINOPS_PROFILE_DISPLAY_LENGTH'),
74
+ service_name_length=cls._get_int_env('FINOPS_SERVICE_NAME_LENGTH'),
75
+ max_services_text=cls._get_int_env('FINOPS_MAX_SERVICES_TEXT'),
76
+ high_cost_threshold=cls._get_float_env('FINOPS_HIGH_COST_THRESHOLD', 5000.0),
77
+ medium_cost_threshold=cls._get_float_env('FINOPS_MEDIUM_COST_THRESHOLD', 1000.0)
78
+ )
79
+
80
+ @staticmethod
81
+ def _get_int_env(key: str, default: Optional[int] = None) -> Optional[int]:
82
+ """Get integer value from environment variable."""
83
+ value = os.getenv(key)
84
+ return int(value) if value and value.isdigit() else default
85
+
86
+ @staticmethod
87
+ def _get_float_env(key: str, default: float) -> float:
88
+ """Get float value from environment variable."""
89
+ value = os.getenv(key)
90
+ try:
91
+ return float(value) if value else default
92
+ except ValueError:
93
+ return default
94
+
95
+ def apply_smart_defaults(self, terminal_width: Optional[int] = None) -> 'DisplayConfiguration':
96
+ """
97
+ Apply smart defaults based on terminal capabilities.
98
+
99
+ Args:
100
+ terminal_width: Terminal width for adaptive profile display
101
+
102
+ Returns:
103
+ Configuration with smart defaults applied
104
+ """
105
+ config = DisplayConfiguration(
106
+ profile_display_length=self.profile_display_length,
107
+ service_name_length=self.service_name_length,
108
+ max_services_text=self.max_services_text,
109
+ high_cost_threshold=self.high_cost_threshold,
110
+ medium_cost_threshold=self.medium_cost_threshold
111
+ )
112
+
113
+ # Smart profile display length based on terminal width
114
+ if config.profile_display_length is None:
115
+ if terminal_width and terminal_width > 120:
116
+ config.profile_display_length = 60 # Wide terminal
117
+ elif terminal_width and terminal_width > 80:
118
+ config.profile_display_length = 40 # Standard terminal
119
+ else:
120
+ config.profile_display_length = 25 # Narrow terminal
121
+
122
+ # Smart service name length for business readability
123
+ if config.service_name_length is None:
124
+ config.service_name_length = 25 # Business-appropriate length
125
+
126
+ # Smart services count for optimal readability
127
+ if config.max_services_text is None:
128
+ config.max_services_text = 10 # Optimal for executive summaries
129
+
130
+ return config
131
+
132
+
133
+ class FinOpsConfigManager:
134
+ """
135
+ FinOps Configuration Manager for API-only parameters.
136
+
137
+ Provides centralized management of display parameters that were removed
138
+ from CLI to maintain enterprise simplicity while preserving full API access.
139
+ """
140
+
141
+ def __init__(self, config: Optional[DisplayConfiguration] = None):
142
+ """
143
+ Initialize configuration manager.
144
+
145
+ Args:
146
+ config: Custom display configuration (defaults to smart defaults)
147
+ """
148
+ self._config = config or DisplayConfiguration.get_default_config()
149
+
150
+ @property
151
+ def config(self) -> DisplayConfiguration:
152
+ """Get current display configuration."""
153
+ return self._config
154
+
155
+ def update_config(self, **kwargs) -> None:
156
+ """
157
+ Update configuration parameters programmatically.
158
+
159
+ Args:
160
+ **kwargs: Configuration parameters to update
161
+ """
162
+ for key, value in kwargs.items():
163
+ if hasattr(self._config, key):
164
+ setattr(self._config, key, value)
165
+
166
+ def get_profile_display_length(self) -> int:
167
+ """Get profile display length with smart default."""
168
+ return self._config.profile_display_length or 50
169
+
170
+ def get_service_name_length(self) -> int:
171
+ """Get service name length with smart default."""
172
+ return self._config.service_name_length or 25
173
+
174
+ def get_max_services_text(self) -> int:
175
+ """Get max services in text with smart default."""
176
+ return self._config.max_services_text or 10
177
+
178
+ def get_high_cost_threshold(self) -> float:
179
+ """Get high cost threshold."""
180
+ return self._config.high_cost_threshold
181
+
182
+ def get_medium_cost_threshold(self) -> float:
183
+ """Get medium cost threshold."""
184
+ return self._config.medium_cost_threshold
185
+
186
+ def is_high_cost(self, amount: float) -> bool:
187
+ """Check if amount exceeds high cost threshold."""
188
+ return amount > self._config.high_cost_threshold
189
+
190
+ def is_medium_cost(self, amount: float) -> bool:
191
+ """Check if amount exceeds medium cost threshold."""
192
+ return amount > self._config.medium_cost_threshold
193
+
194
+ def get_cost_category(self, amount: float) -> str:
195
+ """Get cost category based on thresholds."""
196
+ if self.is_high_cost(amount):
197
+ return "Cost Review Required"
198
+ elif self.is_medium_cost(amount):
199
+ return "Right-sizing Review"
200
+ else:
201
+ return "Monitor & Optimize"
202
+
203
+
204
+ # Global configuration instance for backwards compatibility
205
+ _global_config = FinOpsConfigManager()
206
+
207
+
208
+ def get_global_config() -> FinOpsConfigManager:
209
+ """Get global configuration manager instance."""
210
+ return _global_config
211
+
212
+
213
+ def set_global_config(config: DisplayConfiguration) -> None:
214
+ """Set global configuration."""
215
+ global _global_config
216
+ _global_config = FinOpsConfigManager(config)
217
+
218
+
219
+ # Backwards compatibility functions for existing code
220
+ def get_profile_display_length(args=None) -> int:
221
+ """
222
+ Get profile display length with backwards compatibility.
223
+
224
+ DEPRECATION WARNING: This function is deprecated. Use FinOpsConfigManager.get_profile_display_length() instead.
225
+ """
226
+ if args and hasattr(args, 'profile_display_length') and args.profile_display_length:
227
+ return args.profile_display_length
228
+ return _global_config.get_profile_display_length()
229
+
230
+
231
+ def get_service_name_length(args=None) -> int:
232
+ """
233
+ Get service name length with backwards compatibility.
234
+
235
+ DEPRECATION WARNING: This function is deprecated. Use FinOpsConfigManager.get_service_name_length() instead.
236
+ """
237
+ if args and hasattr(args, 'service_name_length') and args.service_name_length:
238
+ return args.service_name_length
239
+ return _global_config.get_service_name_length()
240
+
241
+
242
+ def get_max_services_text(args=None) -> int:
243
+ """
244
+ Get max services text with backwards compatibility.
245
+
246
+ DEPRECATION WARNING: This function is deprecated. Use FinOpsConfigManager.get_max_services_text() instead.
247
+ """
248
+ if args and hasattr(args, 'max_services_text') and args.max_services_text:
249
+ return args.max_services_text
250
+ return _global_config.get_max_services_text()
251
+
252
+
253
+ def get_high_cost_threshold(args=None) -> float:
254
+ """
255
+ Get high cost threshold with backwards compatibility.
256
+
257
+ DEPRECATION WARNING: This function is deprecated. Use FinOpsConfigManager.get_high_cost_threshold() instead.
258
+ """
259
+ if args and hasattr(args, 'high_cost_threshold') and args.high_cost_threshold:
260
+ return args.high_cost_threshold
261
+ return _global_config.get_high_cost_threshold()
262
+
263
+
264
+ def get_medium_cost_threshold(args=None) -> float:
265
+ """
266
+ Get medium cost threshold with backwards compatibility.
267
+
268
+ DEPRECATION WARNING: This function is deprecated. Use FinOpsConfigManager.get_medium_cost_threshold() instead.
269
+ """
270
+ if args and hasattr(args, 'medium_cost_threshold') and args.medium_cost_threshold:
271
+ return args.medium_cost_threshold
272
+ return _global_config.get_medium_cost_threshold()