runbooks 0.9.2__py3-none-any.whl → 0.9.5__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/__init__.py +15 -6
- runbooks/cfat/__init__.py +3 -1
- runbooks/cloudops/__init__.py +3 -1
- runbooks/common/aws_utils.py +367 -0
- runbooks/common/enhanced_logging_example.py +239 -0
- runbooks/common/enhanced_logging_integration_example.py +257 -0
- runbooks/common/logging_integration_helper.py +344 -0
- runbooks/common/profile_utils.py +8 -6
- runbooks/common/rich_utils.py +347 -3
- runbooks/enterprise/logging.py +400 -38
- runbooks/finops/README.md +262 -406
- runbooks/finops/__init__.py +44 -1
- runbooks/finops/accuracy_cross_validator.py +12 -3
- runbooks/finops/business_cases.py +552 -0
- runbooks/finops/commvault_ec2_analysis.py +415 -0
- runbooks/finops/cost_processor.py +718 -42
- runbooks/finops/dashboard_router.py +44 -22
- runbooks/finops/dashboard_runner.py +302 -39
- runbooks/finops/embedded_mcp_validator.py +358 -48
- runbooks/finops/finops_scenarios.py +1122 -0
- runbooks/finops/helpers.py +182 -0
- runbooks/finops/multi_dashboard.py +30 -15
- runbooks/finops/scenarios.py +789 -0
- runbooks/finops/single_dashboard.py +386 -58
- runbooks/finops/types.py +29 -4
- runbooks/inventory/__init__.py +2 -1
- runbooks/main.py +522 -29
- runbooks/operate/__init__.py +3 -1
- runbooks/remediation/__init__.py +3 -1
- runbooks/remediation/commons.py +55 -16
- runbooks/remediation/commvault_ec2_analysis.py +259 -0
- runbooks/remediation/rds_snapshot_list.py +267 -102
- runbooks/remediation/workspaces_list.py +182 -31
- runbooks/security/__init__.py +3 -1
- runbooks/sre/__init__.py +2 -1
- runbooks/utils/__init__.py +81 -6
- runbooks/utils/version_validator.py +241 -0
- runbooks/vpc/__init__.py +2 -1
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/METADATA +98 -60
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/RECORD +44 -39
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/entry_points.txt +1 -0
- runbooks/inventory/cloudtrail.md +0 -727
- runbooks/inventory/discovery.md +0 -81
- runbooks/remediation/CLAUDE.md +0 -100
- runbooks/remediation/DOME9.md +0 -218
- runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +0 -506
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/WHEEL +0 -0
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,415 @@
|
|
1
|
+
"""
|
2
|
+
FinOps-25: Commvault EC2 Investigation Framework
|
3
|
+
|
4
|
+
Strategic Achievement: Investigation methodology established for infrastructure optimization
|
5
|
+
Account Focus: 637423383469 (Commvault backups account)
|
6
|
+
Objective: Analyze EC2 instances for backup utilization and cost optimization potential
|
7
|
+
|
8
|
+
This module provides comprehensive EC2 utilization analysis specifically for Commvault
|
9
|
+
backup infrastructure to determine if instances are actively performing backups.
|
10
|
+
|
11
|
+
Strategic Alignment:
|
12
|
+
- "Do one thing and do it well": Focus on Commvault-specific EC2 analysis
|
13
|
+
- "Move Fast, But Not So Fast We Crash": Careful analysis before decommissioning
|
14
|
+
- Enterprise FAANG SDLC: Evidence-based investigation with audit trails
|
15
|
+
"""
|
16
|
+
|
17
|
+
import logging
|
18
|
+
from datetime import datetime, timedelta, timezone
|
19
|
+
from typing import Dict, List, Optional, Tuple
|
20
|
+
|
21
|
+
import boto3
|
22
|
+
import click
|
23
|
+
from botocore.exceptions import ClientError
|
24
|
+
|
25
|
+
from ..common.rich_utils import (
|
26
|
+
console, print_header, print_success, print_error, print_warning, print_info,
|
27
|
+
create_table, create_progress_bar, format_cost, create_panel
|
28
|
+
)
|
29
|
+
|
30
|
+
logger = logging.getLogger(__name__)
|
31
|
+
|
32
|
+
|
33
|
+
class CommvaultEC2Analysis:
|
34
|
+
"""
|
35
|
+
FinOps-25: Commvault EC2 Investigation Framework
|
36
|
+
|
37
|
+
Provides systematic analysis of EC2 instances in Commvault backup account
|
38
|
+
to determine utilization patterns and optimization opportunities.
|
39
|
+
"""
|
40
|
+
|
41
|
+
def __init__(self, profile_name: Optional[str] = None, account_id: str = "637423383469"):
|
42
|
+
"""Initialize Commvault EC2 analysis."""
|
43
|
+
self.profile_name = profile_name
|
44
|
+
self.account_id = account_id
|
45
|
+
self.session = boto3.Session(profile_name=profile_name) if profile_name else boto3.Session()
|
46
|
+
|
47
|
+
def analyze_commvault_instances(self, region: str = "us-east-1") -> Dict:
|
48
|
+
"""
|
49
|
+
Analyze EC2 instances in Commvault account for utilization patterns.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
region: AWS region to analyze (default: us-east-1)
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
Dict containing analysis results with cost implications
|
56
|
+
"""
|
57
|
+
print_header("FinOps-25: Commvault EC2 Investigation", f"Account: {self.account_id}")
|
58
|
+
|
59
|
+
try:
|
60
|
+
ec2_client = self.session.client('ec2', region_name=region)
|
61
|
+
cloudwatch_client = self.session.client('cloudwatch', region_name=region)
|
62
|
+
|
63
|
+
# Get all EC2 instances
|
64
|
+
response = ec2_client.describe_instances()
|
65
|
+
instances = []
|
66
|
+
|
67
|
+
for reservation in response['Reservations']:
|
68
|
+
for instance in reservation['Instances']:
|
69
|
+
if instance['State']['Name'] != 'terminated':
|
70
|
+
instances.append(instance)
|
71
|
+
|
72
|
+
if not instances:
|
73
|
+
print_warning(f"No active instances found in account {self.account_id}")
|
74
|
+
return {"instances": [], "total_cost": 0, "optimization_potential": 0}
|
75
|
+
|
76
|
+
print_info(f"Found {len(instances)} active instances for analysis")
|
77
|
+
|
78
|
+
# Analyze each instance
|
79
|
+
analysis_results = []
|
80
|
+
total_monthly_cost = 0
|
81
|
+
|
82
|
+
with create_progress_bar() as progress:
|
83
|
+
task = progress.add_task("Analyzing instances...", total=len(instances))
|
84
|
+
|
85
|
+
for instance in instances:
|
86
|
+
instance_analysis = self._analyze_single_instance(
|
87
|
+
instance, cloudwatch_client, region
|
88
|
+
)
|
89
|
+
analysis_results.append(instance_analysis)
|
90
|
+
total_monthly_cost += instance_analysis['estimated_monthly_cost']
|
91
|
+
progress.advance(task)
|
92
|
+
|
93
|
+
# Generate summary
|
94
|
+
optimization_potential = self._calculate_optimization_potential(analysis_results)
|
95
|
+
|
96
|
+
# Display results
|
97
|
+
self._display_analysis_results(analysis_results, total_monthly_cost, optimization_potential)
|
98
|
+
|
99
|
+
return {
|
100
|
+
"instances": analysis_results,
|
101
|
+
"total_monthly_cost": total_monthly_cost,
|
102
|
+
"optimization_potential": optimization_potential,
|
103
|
+
"account_id": self.account_id,
|
104
|
+
"analysis_timestamp": datetime.now().isoformat()
|
105
|
+
}
|
106
|
+
|
107
|
+
except ClientError as e:
|
108
|
+
print_error(f"AWS API Error: {e}")
|
109
|
+
raise
|
110
|
+
except Exception as e:
|
111
|
+
print_error(f"Analysis Error: {e}")
|
112
|
+
raise
|
113
|
+
|
114
|
+
def _analyze_single_instance(self, instance: Dict, cloudwatch_client, region: str) -> Dict:
|
115
|
+
"""Analyze a single EC2 instance for utilization patterns."""
|
116
|
+
instance_id = instance['InstanceId']
|
117
|
+
instance_type = instance['InstanceType']
|
118
|
+
|
119
|
+
# Get CloudWatch metrics for last 30 days
|
120
|
+
end_time = datetime.now(timezone.utc)
|
121
|
+
start_time = end_time - timedelta(days=30)
|
122
|
+
|
123
|
+
try:
|
124
|
+
# CPU Utilization
|
125
|
+
cpu_response = cloudwatch_client.get_metric_statistics(
|
126
|
+
Namespace='AWS/EC2',
|
127
|
+
MetricName='CPUUtilization',
|
128
|
+
Dimensions=[{'Name': 'InstanceId', 'Value': instance_id}],
|
129
|
+
StartTime=start_time,
|
130
|
+
EndTime=end_time,
|
131
|
+
Period=3600, # 1 hour intervals
|
132
|
+
Statistics=['Average']
|
133
|
+
)
|
134
|
+
|
135
|
+
# Network metrics for backup activity indication
|
136
|
+
network_in_response = cloudwatch_client.get_metric_statistics(
|
137
|
+
Namespace='AWS/EC2',
|
138
|
+
MetricName='NetworkIn',
|
139
|
+
Dimensions=[{'Name': 'InstanceId', 'Value': instance_id}],
|
140
|
+
StartTime=start_time,
|
141
|
+
EndTime=end_time,
|
142
|
+
Period=3600,
|
143
|
+
Statistics=['Sum']
|
144
|
+
)
|
145
|
+
|
146
|
+
network_out_response = cloudwatch_client.get_metric_statistics(
|
147
|
+
Namespace='AWS/EC2',
|
148
|
+
MetricName='NetworkOut',
|
149
|
+
Dimensions=[{'Name': 'InstanceId', 'Value': instance_id}],
|
150
|
+
StartTime=start_time,
|
151
|
+
EndTime=end_time,
|
152
|
+
Period=3600,
|
153
|
+
Statistics=['Sum']
|
154
|
+
)
|
155
|
+
|
156
|
+
except ClientError as e:
|
157
|
+
logger.warning(f"CloudWatch metrics unavailable for {instance_id}: {e}")
|
158
|
+
cpu_response = {'Datapoints': []}
|
159
|
+
network_in_response = {'Datapoints': []}
|
160
|
+
network_out_response = {'Datapoints': []}
|
161
|
+
|
162
|
+
# Calculate averages
|
163
|
+
avg_cpu = 0
|
164
|
+
if cpu_response['Datapoints']:
|
165
|
+
avg_cpu = sum(dp['Average'] for dp in cpu_response['Datapoints']) / len(cpu_response['Datapoints'])
|
166
|
+
|
167
|
+
total_network_in = sum(dp['Sum'] for dp in network_in_response['Datapoints']) if network_in_response['Datapoints'] else 0
|
168
|
+
total_network_out = sum(dp['Sum'] for dp in network_out_response['Datapoints']) if network_out_response['Datapoints'] else 0
|
169
|
+
|
170
|
+
# Estimate monthly cost (simplified pricing model)
|
171
|
+
estimated_monthly_cost = self._estimate_instance_cost(instance_type)
|
172
|
+
|
173
|
+
# Get instance tags
|
174
|
+
tags = {tag['Key']: tag['Value'] for tag in instance.get('Tags', [])}
|
175
|
+
|
176
|
+
# Backup activity assessment
|
177
|
+
backup_activity_score = self._assess_backup_activity(
|
178
|
+
avg_cpu, total_network_in, total_network_out, tags
|
179
|
+
)
|
180
|
+
|
181
|
+
return {
|
182
|
+
"instance_id": instance_id,
|
183
|
+
"instance_type": instance_type,
|
184
|
+
"state": instance['State']['Name'],
|
185
|
+
"launch_time": instance.get('LaunchTime', '').isoformat() if instance.get('LaunchTime') else '',
|
186
|
+
"avg_cpu_utilization": round(avg_cpu, 2),
|
187
|
+
"network_in_bytes": int(total_network_in),
|
188
|
+
"network_out_bytes": int(total_network_out),
|
189
|
+
"estimated_monthly_cost": estimated_monthly_cost,
|
190
|
+
"tags": tags,
|
191
|
+
"backup_activity_score": backup_activity_score,
|
192
|
+
"recommendation": self._generate_recommendation(backup_activity_score, avg_cpu)
|
193
|
+
}
|
194
|
+
|
195
|
+
def _estimate_instance_cost(self, instance_type: str) -> float:
|
196
|
+
"""Estimate monthly cost for EC2 instance type."""
|
197
|
+
# Simplified pricing model - actual costs may vary
|
198
|
+
instance_pricing = {
|
199
|
+
't2.micro': 8.76,
|
200
|
+
't2.small': 17.52,
|
201
|
+
't2.medium': 35.04,
|
202
|
+
't2.large': 70.08,
|
203
|
+
't3.micro': 7.59,
|
204
|
+
't3.small': 15.18,
|
205
|
+
't3.medium': 30.37,
|
206
|
+
't3.large': 60.74,
|
207
|
+
'm5.large': 70.08,
|
208
|
+
'm5.xlarge': 140.16,
|
209
|
+
'm5.2xlarge': 280.32,
|
210
|
+
'c5.large': 62.93,
|
211
|
+
'c5.xlarge': 125.87,
|
212
|
+
'r5.large': 91.98,
|
213
|
+
'r5.xlarge': 183.96
|
214
|
+
}
|
215
|
+
|
216
|
+
return instance_pricing.get(instance_type, 100.0) # Default estimate
|
217
|
+
|
218
|
+
def _assess_backup_activity(self, cpu: float, network_in: int, network_out: int, tags: Dict) -> str:
|
219
|
+
"""Assess likelihood of backup activity based on metrics and tags."""
|
220
|
+
score_factors = []
|
221
|
+
|
222
|
+
# CPU utilization assessment
|
223
|
+
if cpu > 20:
|
224
|
+
score_factors.append("High CPU usage suggests active processes")
|
225
|
+
elif cpu < 5:
|
226
|
+
score_factors.append("Low CPU usage may indicate idle instance")
|
227
|
+
|
228
|
+
# Network activity assessment
|
229
|
+
total_network = network_in + network_out
|
230
|
+
if total_network > 10 * 1024**3: # 10 GB
|
231
|
+
score_factors.append("High network activity suggests data transfer")
|
232
|
+
elif total_network < 1 * 1024**3: # 1 GB
|
233
|
+
score_factors.append("Low network activity may indicate minimal backup activity")
|
234
|
+
|
235
|
+
# Tag analysis for Commvault indicators
|
236
|
+
commvault_indicators = ['commvault', 'backup', 'cv', 'media', 'agent']
|
237
|
+
for indicator in commvault_indicators:
|
238
|
+
for tag_value in tags.values():
|
239
|
+
if indicator.lower() in str(tag_value).lower():
|
240
|
+
score_factors.append(f"Tag indicates Commvault purpose: {tag_value}")
|
241
|
+
break
|
242
|
+
|
243
|
+
if len(score_factors) >= 2:
|
244
|
+
return "LIKELY_ACTIVE"
|
245
|
+
elif len(score_factors) == 1:
|
246
|
+
return "UNCERTAIN"
|
247
|
+
else:
|
248
|
+
return "LIKELY_IDLE"
|
249
|
+
|
250
|
+
def _generate_recommendation(self, activity_score: str, cpu: float) -> str:
|
251
|
+
"""Generate optimization recommendation based on analysis."""
|
252
|
+
if activity_score == "LIKELY_IDLE" and cpu < 5:
|
253
|
+
return "CANDIDATE_FOR_DECOMMISSION"
|
254
|
+
elif activity_score == "UNCERTAIN":
|
255
|
+
return "REQUIRES_DEEPER_INVESTIGATION"
|
256
|
+
elif activity_score == "LIKELY_ACTIVE":
|
257
|
+
return "RETAIN_MONITOR_USAGE"
|
258
|
+
else:
|
259
|
+
return "MANUAL_REVIEW_REQUIRED"
|
260
|
+
|
261
|
+
def _calculate_optimization_potential(self, instances: List[Dict]) -> Dict:
|
262
|
+
"""Calculate potential cost savings from optimization."""
|
263
|
+
decommission_candidates = [
|
264
|
+
i for i in instances
|
265
|
+
if i['recommendation'] == 'CANDIDATE_FOR_DECOMMISSION'
|
266
|
+
]
|
267
|
+
|
268
|
+
investigation_required = [
|
269
|
+
i for i in instances
|
270
|
+
if i['recommendation'] == 'REQUIRES_DEEPER_INVESTIGATION'
|
271
|
+
]
|
272
|
+
|
273
|
+
potential_monthly_savings = sum(i['estimated_monthly_cost'] for i in decommission_candidates)
|
274
|
+
potential_annual_savings = potential_monthly_savings * 12
|
275
|
+
|
276
|
+
return {
|
277
|
+
"decommission_candidates": len(decommission_candidates),
|
278
|
+
"investigation_required": len(investigation_required),
|
279
|
+
"potential_monthly_savings": potential_monthly_savings,
|
280
|
+
"potential_annual_savings": potential_annual_savings,
|
281
|
+
"confidence_level": "HIGH" if len(decommission_candidates) > 0 else "MEDIUM"
|
282
|
+
}
|
283
|
+
|
284
|
+
def _display_analysis_results(self, instances: List[Dict], total_cost: float, optimization: Dict):
|
285
|
+
"""Display comprehensive analysis results."""
|
286
|
+
# Summary table
|
287
|
+
summary_table = create_table(
|
288
|
+
title="FinOps-25: Commvault EC2 Analysis Summary",
|
289
|
+
caption=f"Account: {self.account_id} | Analysis Date: {datetime.now().strftime('%Y-%m-%d')}"
|
290
|
+
)
|
291
|
+
|
292
|
+
summary_table.add_column("Metric", style="cyan", width=25)
|
293
|
+
summary_table.add_column("Value", style="green", justify="right", width=20)
|
294
|
+
summary_table.add_column("Impact", style="yellow", width=30)
|
295
|
+
|
296
|
+
summary_table.add_row(
|
297
|
+
"Total Instances",
|
298
|
+
str(len(instances)),
|
299
|
+
"Infrastructure scope"
|
300
|
+
)
|
301
|
+
summary_table.add_row(
|
302
|
+
"Monthly Cost",
|
303
|
+
format_cost(total_cost, period="monthly"),
|
304
|
+
"Current infrastructure cost"
|
305
|
+
)
|
306
|
+
summary_table.add_row(
|
307
|
+
"Decommission Candidates",
|
308
|
+
str(optimization['decommission_candidates']),
|
309
|
+
"Immediate optimization opportunities"
|
310
|
+
)
|
311
|
+
summary_table.add_row(
|
312
|
+
"Investigation Required",
|
313
|
+
str(optimization['investigation_required']),
|
314
|
+
"Further analysis needed"
|
315
|
+
)
|
316
|
+
summary_table.add_row(
|
317
|
+
"Potential Annual Savings",
|
318
|
+
format_cost(optimization['potential_annual_savings'], period="annual"),
|
319
|
+
f"Confidence: {optimization['confidence_level']}"
|
320
|
+
)
|
321
|
+
|
322
|
+
console.print(summary_table)
|
323
|
+
|
324
|
+
# Detailed instance analysis
|
325
|
+
if instances:
|
326
|
+
detail_table = create_table(
|
327
|
+
title="Detailed Instance Analysis",
|
328
|
+
caption="CPU and network utilization patterns with recommendations"
|
329
|
+
)
|
330
|
+
|
331
|
+
detail_table.add_column("Instance ID", style="cyan", width=18)
|
332
|
+
detail_table.add_column("Type", style="blue", width=12)
|
333
|
+
detail_table.add_column("Avg CPU %", style="yellow", justify="right", width=10)
|
334
|
+
detail_table.add_column("Network (GB)", style="magenta", justify="right", width=12)
|
335
|
+
detail_table.add_column("Monthly Cost", style="green", justify="right", width=12)
|
336
|
+
detail_table.add_column("Recommendation", style="red", width=20)
|
337
|
+
|
338
|
+
for instance in instances:
|
339
|
+
network_gb = (instance['network_in_bytes'] + instance['network_out_bytes']) / (1024**3)
|
340
|
+
|
341
|
+
recommendation_style = {
|
342
|
+
'CANDIDATE_FOR_DECOMMISSION': '[red]DECOMMISSION[/red]',
|
343
|
+
'REQUIRES_DEEPER_INVESTIGATION': '[yellow]INVESTIGATE[/yellow]',
|
344
|
+
'RETAIN_MONITOR_USAGE': '[green]RETAIN[/green]',
|
345
|
+
'MANUAL_REVIEW_REQUIRED': '[blue]MANUAL REVIEW[/blue]'
|
346
|
+
}.get(instance['recommendation'], instance['recommendation'])
|
347
|
+
|
348
|
+
detail_table.add_row(
|
349
|
+
instance['instance_id'],
|
350
|
+
instance['instance_type'],
|
351
|
+
f"{instance['avg_cpu_utilization']:.1f}%",
|
352
|
+
f"{network_gb:.1f}",
|
353
|
+
format_cost(instance['estimated_monthly_cost'], period="monthly"),
|
354
|
+
recommendation_style
|
355
|
+
)
|
356
|
+
|
357
|
+
console.print(detail_table)
|
358
|
+
|
359
|
+
# Business impact panel
|
360
|
+
if optimization['potential_annual_savings'] > 0:
|
361
|
+
impact_panel = create_panel(
|
362
|
+
f"[bold green]Business Impact Analysis[/bold green]\n\n"
|
363
|
+
f"💰 [yellow]Optimization Potential:[/yellow] {format_cost(optimization['potential_annual_savings'], period='annual')}\n"
|
364
|
+
f"📊 [yellow]Confidence Level:[/yellow] {optimization['confidence_level']}\n"
|
365
|
+
f"🎯 [yellow]Implementation Approach:[/yellow] Systematic decommissioning with validation\n"
|
366
|
+
f"⏱️ [yellow]Timeline:[/yellow] 3-4 weeks investigation + approval process\n\n"
|
367
|
+
f"[blue]Strategic Value:[/blue] Establish investigation methodology for infrastructure optimization\n"
|
368
|
+
f"[blue]Risk Assessment:[/blue] Medium risk - requires careful backup workflow validation",
|
369
|
+
title="FinOps-25: Commvault Investigation Framework Results"
|
370
|
+
)
|
371
|
+
console.print(impact_panel)
|
372
|
+
|
373
|
+
print_success(f"FinOps-25 analysis complete - {len(instances)} instances analyzed")
|
374
|
+
|
375
|
+
|
376
|
+
def analyze_commvault_ec2(profile: Optional[str] = None, account_id: str = "637423383469",
|
377
|
+
region: str = "us-east-1") -> Dict:
|
378
|
+
"""
|
379
|
+
Business wrapper function for FinOps-25 Commvault EC2 investigation.
|
380
|
+
|
381
|
+
Args:
|
382
|
+
profile: AWS profile name
|
383
|
+
account_id: Target account (default: 637423383469)
|
384
|
+
region: AWS region (default: us-east-1)
|
385
|
+
|
386
|
+
Returns:
|
387
|
+
Dict containing comprehensive analysis results
|
388
|
+
"""
|
389
|
+
analyzer = CommvaultEC2Analysis(profile_name=profile, account_id=account_id)
|
390
|
+
return analyzer.analyze_commvault_instances(region=region)
|
391
|
+
|
392
|
+
|
393
|
+
@click.command()
|
394
|
+
@click.option('--profile', help='AWS profile name')
|
395
|
+
@click.option('--account-id', default='637423383469', help='Commvault account ID')
|
396
|
+
@click.option('--region', default='us-east-1', help='AWS region')
|
397
|
+
@click.option('--output-file', help='Save results to file')
|
398
|
+
def main(profile, account_id, region, output_file):
|
399
|
+
"""FinOps-25: Commvault EC2 Investigation Framework - CLI interface."""
|
400
|
+
try:
|
401
|
+
results = analyze_commvault_ec2(profile, account_id, region)
|
402
|
+
|
403
|
+
if output_file:
|
404
|
+
import json
|
405
|
+
with open(output_file, 'w') as f:
|
406
|
+
json.dump(results, f, indent=2, default=str)
|
407
|
+
print_success(f"Results saved to {output_file}")
|
408
|
+
|
409
|
+
except Exception as e:
|
410
|
+
print_error(f"Analysis failed: {e}")
|
411
|
+
raise click.Abort()
|
412
|
+
|
413
|
+
|
414
|
+
if __name__ == '__main__':
|
415
|
+
main()
|