runbooks 0.9.1__py3-none-any.whl → 0.9.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 (47) hide show
  1. runbooks/__init__.py +15 -6
  2. runbooks/cfat/__init__.py +3 -1
  3. runbooks/cloudops/__init__.py +3 -1
  4. runbooks/common/aws_utils.py +367 -0
  5. runbooks/common/enhanced_logging_example.py +239 -0
  6. runbooks/common/enhanced_logging_integration_example.py +257 -0
  7. runbooks/common/logging_integration_helper.py +344 -0
  8. runbooks/common/profile_utils.py +8 -6
  9. runbooks/common/rich_utils.py +347 -3
  10. runbooks/enterprise/logging.py +400 -38
  11. runbooks/finops/README.md +262 -406
  12. runbooks/finops/__init__.py +2 -1
  13. runbooks/finops/accuracy_cross_validator.py +12 -3
  14. runbooks/finops/commvault_ec2_analysis.py +415 -0
  15. runbooks/finops/cost_processor.py +718 -42
  16. runbooks/finops/dashboard_router.py +44 -22
  17. runbooks/finops/dashboard_runner.py +302 -39
  18. runbooks/finops/embedded_mcp_validator.py +358 -48
  19. runbooks/finops/finops_scenarios.py +771 -0
  20. runbooks/finops/multi_dashboard.py +30 -15
  21. runbooks/finops/single_dashboard.py +386 -58
  22. runbooks/finops/types.py +29 -4
  23. runbooks/inventory/__init__.py +2 -1
  24. runbooks/main.py +522 -29
  25. runbooks/operate/__init__.py +3 -1
  26. runbooks/remediation/__init__.py +3 -1
  27. runbooks/remediation/commons.py +55 -16
  28. runbooks/remediation/commvault_ec2_analysis.py +259 -0
  29. runbooks/remediation/rds_snapshot_list.py +267 -102
  30. runbooks/remediation/workspaces_list.py +182 -31
  31. runbooks/security/__init__.py +3 -1
  32. runbooks/sre/__init__.py +2 -1
  33. runbooks/utils/__init__.py +81 -6
  34. runbooks/utils/version_validator.py +241 -0
  35. runbooks/vpc/__init__.py +2 -1
  36. runbooks-0.9.4.dist-info/METADATA +563 -0
  37. {runbooks-0.9.1.dist-info → runbooks-0.9.4.dist-info}/RECORD +41 -38
  38. {runbooks-0.9.1.dist-info → runbooks-0.9.4.dist-info}/entry_points.txt +1 -0
  39. runbooks/inventory/cloudtrail.md +0 -727
  40. runbooks/inventory/discovery.md +0 -81
  41. runbooks/remediation/CLAUDE.md +0 -100
  42. runbooks/remediation/DOME9.md +0 -218
  43. runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +0 -506
  44. runbooks-0.9.1.dist-info/METADATA +0 -308
  45. {runbooks-0.9.1.dist-info → runbooks-0.9.4.dist-info}/WHEEL +0 -0
  46. {runbooks-0.9.1.dist-info → runbooks-0.9.4.dist-info}/licenses/LICENSE +0 -0
  47. {runbooks-0.9.1.dist-info → runbooks-0.9.4.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,23 @@
1
1
  """
2
2
  RDS Snapshot Analysis - Analyze RDS snapshots for lifecycle management and cost optimization.
3
+
4
+ JIRA AWSO-23: Enhanced RDS snapshots analysis with cost calculation for $5K-24K annual savings
5
+ Accounts: 91893567291, 142964829704, 363435891329, 507583929055
6
+ Focus: 89 manual snapshots causing storage costs and operational clutter
3
7
  """
4
8
 
5
9
  import logging
6
10
  from datetime import datetime, timedelta, timezone
11
+ from typing import Dict, List, Optional
7
12
 
8
13
  import click
9
14
  from botocore.exceptions import ClientError
10
15
 
11
16
  from .commons import display_aws_account_info, get_client, write_to_csv
17
+ from ..common.rich_utils import (
18
+ console, print_header, print_success, print_error, print_warning,
19
+ create_table, create_progress_bar, format_cost
20
+ )
12
21
 
13
22
  logger = logging.getLogger(__name__)
14
23
 
@@ -24,18 +33,18 @@ def calculate_snapshot_age(create_time):
24
33
 
25
34
 
26
35
  def estimate_snapshot_cost(allocated_storage, storage_type="gp2", days_old=1):
27
- """Estimate monthly snapshot storage cost (simplified)."""
28
- # Simplified cost estimation per GB per month
29
- cost_per_gb_month = {
30
- "gp2": 0.095, # General Purpose SSD
31
- "gp3": 0.08, # General Purpose SSD (gp3)
32
- "io1": 0.125, # Provisioned IOPS SSD
33
- "io2": 0.125, # Provisioned IOPS SSD
34
- "standard": 0.05, # Magnetic
35
- }
36
-
37
- rate = cost_per_gb_month.get(storage_type.lower(), 0.095) # Default to gp2
38
- monthly_cost = allocated_storage * rate
36
+ """
37
+ Estimate monthly snapshot storage cost with enhanced accuracy.
38
+
39
+ JIRA AWSO-23: Enhanced cost estimation for $5K-24K annual savings target
40
+ Based on AWS RDS snapshot pricing: https://aws.amazon.com/rds/pricing/
41
+ """
42
+ # RDS Snapshot storage cost per GB per month (USD)
43
+ # Note: RDS snapshots are charged at $0.095/GB-month for all regions (simplified)
44
+ snapshot_cost_per_gb_month = 0.095
45
+
46
+ # Calculate base monthly cost
47
+ monthly_cost = allocated_storage * snapshot_cost_per_gb_month
39
48
 
40
49
  # Pro-rate for actual age if less than a month
41
50
  if days_old < 30:
@@ -44,34 +53,89 @@ def estimate_snapshot_cost(allocated_storage, storage_type="gp2", days_old=1):
44
53
  return round(monthly_cost, 2)
45
54
 
46
55
 
56
+ def calculate_manual_snapshot_savings(snapshots_data: List[Dict]) -> Dict[str, float]:
57
+ """
58
+ Calculate potential savings from manual snapshot cleanup.
59
+
60
+ JIRA AWSO-23: Focuses on 89 manual snapshots for cost optimization
61
+ """
62
+ manual_snapshots = [s for s in snapshots_data if s.get("SnapshotType", "").lower() == "manual"]
63
+
64
+ # Calculate costs by age groups
65
+ old_manual_snapshots = [s for s in manual_snapshots if s.get("AgeDays", 0) >= 90] # 3+ months old
66
+ very_old_manual_snapshots = [s for s in manual_snapshots if s.get("AgeDays", 0) >= 180] # 6+ months old
67
+
68
+ total_manual_cost = sum(s.get("EstimatedMonthlyCost", 0) for s in manual_snapshots)
69
+ old_manual_cost = sum(s.get("EstimatedMonthlyCost", 0) for s in old_manual_snapshots)
70
+ very_old_manual_cost = sum(s.get("EstimatedMonthlyCost", 0) for s in very_old_manual_snapshots)
71
+
72
+ return {
73
+ "total_manual_snapshots": len(manual_snapshots),
74
+ "total_manual_monthly_cost": total_manual_cost,
75
+ "total_manual_annual_cost": total_manual_cost * 12,
76
+
77
+ "old_manual_snapshots": len(old_manual_snapshots), # 90+ days
78
+ "old_manual_monthly_savings": old_manual_cost,
79
+ "old_manual_annual_savings": old_manual_cost * 12,
80
+
81
+ "very_old_manual_snapshots": len(very_old_manual_snapshots), # 180+ days
82
+ "very_old_manual_monthly_savings": very_old_manual_cost,
83
+ "very_old_manual_annual_savings": very_old_manual_cost * 12,
84
+ }
85
+
86
+
47
87
  @click.command()
48
88
  @click.option("--output-file", default="/tmp/rds_snapshots.csv", help="Output CSV file path")
49
89
  @click.option("--old-days", default=30, help="Days threshold for considering snapshots old")
50
90
  @click.option("--include-cost", is_flag=True, help="Include estimated cost analysis")
51
91
  @click.option("--snapshot-type", help="Filter by snapshot type (automated, manual)")
52
- def get_rds_snapshot_details(output_file, old_days, include_cost, snapshot_type):
53
- """Analyze RDS snapshots for lifecycle management and cost optimization."""
54
- logger.info(f"Analyzing RDS snapshots in {display_aws_account_info()}")
92
+ @click.option("--manual-only", is_flag=True, help="Focus on manual snapshots only (JIRA AWSO-23)")
93
+ @click.option("--older-than", default=90, help="Focus on snapshots older than X days")
94
+ @click.option("--calculate-savings", is_flag=True, help="Calculate detailed cost savings analysis")
95
+ @click.option("--analyze", is_flag=True, help="Perform comprehensive cost analysis")
96
+ def get_rds_snapshot_details(output_file, old_days, include_cost, snapshot_type, manual_only, older_than, calculate_savings, analyze):
97
+ """
98
+ Analyze RDS snapshots for lifecycle management and cost optimization.
99
+
100
+ JIRA AWSO-23: Enhanced RDS snapshots analysis for $5K-24K annual savings
101
+ Focus on 89 manual snapshots causing storage costs and operational clutter
102
+ """
103
+ print_header("RDS Snapshot Cost Optimization Analysis", "v0.9.1")
104
+
105
+ account_info = display_aws_account_info()
106
+ console.print(f"[cyan]Analyzing RDS snapshots in {account_info}[/cyan]")
55
107
 
56
108
  try:
57
109
  rds = get_client("rds")
58
110
 
59
111
  # Get all snapshots
60
- logger.info("Collecting RDS snapshot data...")
112
+ console.print("[yellow]Collecting RDS snapshot data...[/yellow]")
61
113
  response = rds.describe_db_snapshots()
62
114
  snapshots = response.get("DBSnapshots", [])
63
115
 
64
116
  if not snapshots:
65
- logger.info("No RDS snapshots found")
117
+ print_warning("No RDS snapshots found")
66
118
  return
67
119
 
68
- logger.info(f"Found {len(snapshots)} RDS snapshots to analyze")
120
+ console.print(f"[green]Found {len(snapshots)} RDS snapshots to analyze[/green]")
69
121
 
70
- # Filter by snapshot type if specified
122
+ # Apply filters
123
+ if manual_only:
124
+ original_count = len(snapshots)
125
+ snapshots = [s for s in snapshots if s.get("SnapshotType", "").lower() == "manual"]
126
+ console.print(f"[dim]JIRA AWSO-23 Filter: {len(snapshots)} manual snapshots (from {original_count} total)[/dim]")
127
+
71
128
  if snapshot_type:
72
129
  original_count = len(snapshots)
73
130
  snapshots = [s for s in snapshots if s.get("SnapshotType", "").lower() == snapshot_type.lower()]
74
- logger.info(f"Filtered to {len(snapshots)} snapshots of type '{snapshot_type}'")
131
+ console.print(f"[dim]Filtered to {len(snapshots)} snapshots of type '{snapshot_type}'[/dim]")
132
+
133
+ if older_than > 0:
134
+ now = datetime.now(tz=timezone.utc)
135
+ threshold_date = now - timedelta(days=older_than)
136
+ original_count = len(snapshots)
137
+ snapshots = [s for s in snapshots if s.get("SnapshotCreateTime") and s["SnapshotCreateTime"] < threshold_date]
138
+ console.print(f"[dim]Age filter: {len(snapshots)} snapshots older than {older_than} days (from {original_count})[/dim]")
75
139
 
76
140
  data = []
77
141
  old_snapshots = []
@@ -80,95 +144,196 @@ def get_rds_snapshot_details(output_file, old_days, include_cost, snapshot_type)
80
144
  total_storage = 0
81
145
  total_estimated_cost = 0
82
146
 
83
- for i, snapshot in enumerate(snapshots, 1):
84
- snapshot_id = snapshot["DBSnapshotIdentifier"]
85
- logger.info(f"Analyzing snapshot {i}/{len(snapshots)}: {snapshot_id}")
86
-
87
- create_time = snapshot.get("SnapshotCreateTime")
88
- age_days = calculate_snapshot_age(create_time) if create_time else 0
89
- allocated_storage = snapshot.get("AllocatedStorage", 0)
90
- storage_type = snapshot.get("StorageType", "gp2")
91
- snap_type = snapshot.get("SnapshotType", "unknown")
92
-
93
- snapshot_data = {
94
- "DBSnapshotIdentifier": snapshot_id,
95
- "DBInstanceIdentifier": snapshot.get("DBInstanceIdentifier", "Unknown"),
96
- "SnapshotCreateTime": create_time.strftime("%Y-%m-%d %H:%M:%S") if create_time else "Unknown",
97
- "AgeDays": age_days,
98
- "SnapshotType": snap_type,
99
- "Status": snapshot.get("Status", "Unknown"),
100
- "Engine": snapshot.get("Engine", "Unknown"),
101
- "EngineVersion": snapshot.get("EngineVersion", "Unknown"),
102
- "StorageType": storage_type,
103
- "AllocatedStorage": allocated_storage,
104
- "Encrypted": snapshot.get("Encrypted", False),
105
- "AvailabilityZone": snapshot.get("AvailabilityZone", "Unknown"),
106
- }
107
-
108
- # Cost analysis
109
- if include_cost and allocated_storage > 0:
110
- estimated_cost = estimate_snapshot_cost(allocated_storage, storage_type, age_days)
147
+ with create_progress_bar() as progress:
148
+ task_id = progress.add_task(
149
+ f"Analyzing {len(snapshots)} snapshots...",
150
+ total=len(snapshots)
151
+ )
152
+
153
+ for i, snapshot in enumerate(snapshots, 1):
154
+ snapshot_id = snapshot["DBSnapshotIdentifier"]
155
+
156
+ create_time = snapshot.get("SnapshotCreateTime")
157
+ age_days = calculate_snapshot_age(create_time) if create_time else 0
158
+ allocated_storage = snapshot.get("AllocatedStorage", 0)
159
+ storage_type = snapshot.get("StorageType", "gp2")
160
+ snap_type = snapshot.get("SnapshotType", "unknown")
161
+
162
+ snapshot_data = {
163
+ "DBSnapshotIdentifier": snapshot_id,
164
+ "DBInstanceIdentifier": snapshot.get("DBInstanceIdentifier", "Unknown"),
165
+ "SnapshotCreateTime": create_time.strftime("%Y-%m-%d %H:%M:%S") if create_time else "Unknown",
166
+ "AgeDays": age_days,
167
+ "SnapshotType": snap_type,
168
+ "Status": snapshot.get("Status", "Unknown"),
169
+ "Engine": snapshot.get("Engine", "Unknown"),
170
+ "EngineVersion": snapshot.get("EngineVersion", "Unknown"),
171
+ "StorageType": storage_type,
172
+ "AllocatedStorage": allocated_storage,
173
+ "Encrypted": snapshot.get("Encrypted", False),
174
+ "AvailabilityZone": snapshot.get("AvailabilityZone", "Unknown"),
175
+ }
176
+
177
+ # Enhanced cost analysis (JIRA AWSO-23)
178
+ estimated_cost = 0.0
179
+ if include_cost or calculate_savings or analyze:
180
+ if allocated_storage > 0:
181
+ estimated_cost = estimate_snapshot_cost(allocated_storage, storage_type, age_days)
182
+ total_estimated_cost += estimated_cost
183
+
111
184
  snapshot_data["EstimatedMonthlyCost"] = estimated_cost
112
- total_estimated_cost += estimated_cost
113
- else:
114
- snapshot_data["EstimatedMonthlyCost"] = 0
115
-
116
- # Categorization for analysis
117
- if age_days >= old_days:
118
- old_snapshots.append(snapshot_id)
119
- snapshot_data["IsOld"] = True
120
- else:
121
- snapshot_data["IsOld"] = False
122
-
123
- if snap_type.lower() == "manual":
124
- manual_snapshots.append(snapshot_id)
125
- elif snap_type.lower() == "automated":
126
- automated_snapshots.append(snapshot_id)
127
-
128
- total_storage += allocated_storage
129
-
130
- # Cleanup recommendations
131
- recommendations = []
132
- if age_days >= old_days and snap_type.lower() == "manual":
133
- recommendations.append(f"Consider deletion (>{old_days} days old)")
134
- if snap_type.lower() == "automated" and age_days > 35: # AWS default retention
135
- recommendations.append("Check retention policy")
136
- if not snapshot.get("Encrypted", False):
137
- recommendations.append("Not encrypted")
138
-
139
- snapshot_data["Recommendations"] = "; ".join(recommendations) if recommendations else "None"
140
-
141
- data.append(snapshot_data)
142
-
143
- # Log summary for this snapshot
144
- status = "OLD" if age_days >= old_days else "RECENT"
145
- logger.info(f" → {snap_type}, {age_days}d old, {allocated_storage}GB, {status}")
185
+ snapshot_data["EstimatedAnnualCost"] = estimated_cost * 12
186
+
187
+ # Categorization for analysis
188
+ if age_days >= old_days:
189
+ old_snapshots.append(snapshot_id)
190
+ snapshot_data["IsOld"] = True
191
+ else:
192
+ snapshot_data["IsOld"] = False
193
+
194
+ if snap_type.lower() == "manual":
195
+ manual_snapshots.append(snapshot_id)
196
+ elif snap_type.lower() == "automated":
197
+ automated_snapshots.append(snapshot_id)
198
+
199
+ total_storage += allocated_storage
200
+
201
+ # Enhanced cleanup recommendations (JIRA AWSO-23)
202
+ recommendations = []
203
+ if age_days >= older_than and snap_type.lower() == "manual":
204
+ recommendations.append(f"HIGH PRIORITY: Manual snapshot >{older_than} days old")
205
+ elif age_days >= old_days and snap_type.lower() == "manual":
206
+ recommendations.append(f"Consider deletion (>{old_days} days old)")
207
+ if snap_type.lower() == "automated" and age_days > 35: # AWS default retention
208
+ recommendations.append("Check retention policy")
209
+ if not snapshot.get("Encrypted", False):
210
+ recommendations.append("Not encrypted")
211
+
212
+ snapshot_data["Recommendations"] = "; ".join(recommendations) if recommendations else "None"
213
+ data.append(snapshot_data)
214
+ progress.advance(task_id)
146
215
 
147
216
  # Export results
148
217
  write_to_csv(data, output_file)
149
- logger.info(f"RDS snapshot analysis exported to: {output_file}")
150
-
151
- # Summary report
152
- logger.info("\n=== ANALYSIS SUMMARY ===")
153
- logger.info(f"Total snapshots: {len(snapshots)}")
154
- logger.info(f"Manual snapshots: {len(manual_snapshots)}")
155
- logger.info(f"Automated snapshots: {len(automated_snapshots)}")
156
- logger.info(f"Old snapshots (>{old_days} days): {len(old_snapshots)}")
157
- logger.info(f"Total storage: {total_storage} GB")
158
-
159
- if include_cost:
160
- logger.info(f"Estimated total monthly cost: ${total_estimated_cost:.2f}")
161
-
162
- # Cleanup recommendations
218
+ print_success(f"RDS snapshot analysis exported to: {output_file}")
219
+
220
+ # Enhanced cost analysis for JIRA AWSO-23
221
+ if calculate_savings or analyze:
222
+ savings_analysis = calculate_manual_snapshot_savings(data)
223
+
224
+ # Create comprehensive summary table with Rich CLI
225
+ print_header("RDS Snapshot Analysis Summary")
226
+
227
+ summary_table = create_table(
228
+ title="RDS Snapshot Cost Analysis - JIRA AWSO-23",
229
+ columns=[
230
+ {"header": "Metric", "style": "cyan"},
231
+ {"header": "Count", "style": "green bold"},
232
+ {"header": "Storage (GB)", "style": "yellow"},
233
+ {"header": "Monthly Cost", "style": "red"},
234
+ {"header": "Annual Cost", "style": "red bold"}
235
+ ]
236
+ )
237
+
238
+ # Basic metrics
239
+ summary_table.add_row(
240
+ "Total Snapshots",
241
+ str(len(data)),
242
+ str(total_storage),
243
+ format_cost(total_estimated_cost) if (include_cost or calculate_savings or analyze) else "N/A",
244
+ format_cost(total_estimated_cost * 12) if (include_cost or calculate_savings or analyze) else "N/A"
245
+ )
246
+
247
+ summary_table.add_row(
248
+ "Manual Snapshots",
249
+ str(len(manual_snapshots)),
250
+ str(sum(s["AllocatedStorage"] for s in data if s["SnapshotType"].lower() == "manual")),
251
+ format_cost(savings_analysis["total_manual_monthly_cost"]) if (calculate_savings or analyze) else "N/A",
252
+ format_cost(savings_analysis["total_manual_annual_cost"]) if (calculate_savings or analyze) else "N/A"
253
+ )
254
+
255
+ summary_table.add_row(
256
+ "Automated Snapshots",
257
+ str(len(automated_snapshots)),
258
+ str(sum(s["AllocatedStorage"] for s in data if s["SnapshotType"].lower() == "automated")),
259
+ "Retention Policy",
260
+ "Retention Policy"
261
+ )
262
+
263
+ summary_table.add_row(
264
+ f"Old Snapshots (>{old_days} days)",
265
+ str(len(old_snapshots)),
266
+ str(sum(s["AllocatedStorage"] for s in data if s["IsOld"])),
267
+ "Mixed Types",
268
+ "Mixed Types"
269
+ )
270
+
271
+ # JIRA AWSO-23 specific analysis
272
+ if calculate_savings or analyze:
273
+ summary_table.add_row(
274
+ f"🎯 Manual >{older_than}d (Cleanup Target)",
275
+ str(savings_analysis["old_manual_snapshots"]),
276
+ str(sum(s["AllocatedStorage"] for s in data if s["AgeDays"] >= older_than and s["SnapshotType"].lower() == "manual")),
277
+ format_cost(savings_analysis["old_manual_monthly_savings"]),
278
+ format_cost(savings_analysis["old_manual_annual_savings"])
279
+ )
280
+
281
+ console.print(summary_table)
282
+
283
+ # Cleanup recommendations with Rich CLI
163
284
  cleanup_candidates = [s for s in data if s["IsOld"] and s["SnapshotType"].lower() == "manual"]
164
- if cleanup_candidates:
165
- logger.warning(f"⚠ {len(cleanup_candidates)} old manual snapshots for review:")
166
- for snap in cleanup_candidates:
167
- logger.warning(
168
- f" - {snap['DBSnapshotIdentifier']}: {snap['AgeDays']} days old, {snap['AllocatedStorage']}GB"
285
+ high_priority_candidates = [s for s in data if s["AgeDays"] >= older_than and s["SnapshotType"].lower() == "manual"]
286
+
287
+ if high_priority_candidates:
288
+ print_warning(f"🎯 JIRA AWSO-23: {len(high_priority_candidates)} high-priority manual snapshots (>{older_than} days):")
289
+
290
+ # Create detailed cleanup candidates table
291
+ cleanup_table = create_table(
292
+ title=f"High-Priority Manual Snapshots (>{older_than} days old)",
293
+ columns=[
294
+ {"header": "Snapshot ID", "style": "cyan"},
295
+ {"header": "DB Instance", "style": "blue"},
296
+ {"header": "Age (Days)", "style": "yellow"},
297
+ {"header": "Size (GB)", "style": "green"},
298
+ {"header": "Monthly Cost", "style": "red"},
299
+ {"header": "Engine", "style": "magenta"}
300
+ ]
301
+ )
302
+
303
+ for snap in high_priority_candidates[:15]: # Show first 15 for readability
304
+ cleanup_table.add_row(
305
+ snap['DBSnapshotIdentifier'],
306
+ snap['DBInstanceIdentifier'],
307
+ str(snap['AgeDays']),
308
+ str(snap['AllocatedStorage']),
309
+ format_cost(snap['EstimatedMonthlyCost']) if snap['EstimatedMonthlyCost'] > 0 else "N/A",
310
+ snap['Engine']
169
311
  )
312
+
313
+ console.print(cleanup_table)
314
+
315
+ if len(high_priority_candidates) > 15:
316
+ console.print(f"[dim]... and {len(high_priority_candidates) - 15} more high-priority snapshots[/dim]")
317
+
318
+ elif cleanup_candidates:
319
+ print_warning(f"⚠ {len(cleanup_candidates)} old manual snapshots for review (>{old_days} days)")
170
320
  else:
171
- logger.info("✓ No old manual snapshots found")
321
+ print_success("✓ No old manual snapshots found")
322
+
323
+ # Target validation (JIRA AWSO-23: $5K-24K annual savings)
324
+ if calculate_savings or analyze:
325
+ target_min_annual = 5000.0
326
+ target_max_annual = 24000.0
327
+ actual_savings = savings_analysis["old_manual_annual_savings"]
328
+
329
+ if actual_savings >= target_min_annual:
330
+ if actual_savings <= target_max_annual:
331
+ print_success(f"🎯 Target Achievement: ${actual_savings:,.0f} within JIRA AWSO-23 range (${target_min_annual:,.0f}-${target_max_annual:,.0f})")
332
+ else:
333
+ print_success(f"🎯 Target Exceeded: ${actual_savings:,.0f} exceeds JIRA AWSO-23 maximum target (${target_max_annual:,.0f})")
334
+ else:
335
+ percentage = (actual_savings / target_min_annual) * 100
336
+ print_warning(f"📊 Analysis: ${actual_savings:,.0f} is {percentage:.1f}% of JIRA AWSO-23 minimum target (${target_min_annual:,.0f})")
172
337
 
173
338
  # Encryption status
174
339
  encrypted_count = sum(1 for s in data if s["Encrypted"])