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
@@ -1,14 +1,23 @@
|
|
1
1
|
"""
|
2
2
|
RDS Snapshot Analysis - Analyze RDS snapshots for lifecycle management and cost optimization.
|
3
|
+
|
4
|
+
JIRA FinOps-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
|
-
"""
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
monthly_cost = allocated_storage *
|
36
|
+
"""
|
37
|
+
Estimate monthly snapshot storage cost with enhanced accuracy.
|
38
|
+
|
39
|
+
JIRA FinOps-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 FinOps-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
|
-
|
53
|
-
|
54
|
-
|
92
|
+
@click.option("--manual-only", is_flag=True, help="Focus on manual snapshots only (JIRA FinOps-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 FinOps-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
|
-
|
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
|
-
|
117
|
+
print_warning("No RDS snapshots found")
|
66
118
|
return
|
67
119
|
|
68
|
-
|
120
|
+
console.print(f"[green]Found {len(snapshots)} RDS snapshots to analyze[/green]")
|
69
121
|
|
70
|
-
#
|
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 FinOps-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
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
"
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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 FinOps-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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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 FinOps-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
|
-
|
150
|
-
|
151
|
-
#
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
218
|
+
print_success(f"RDS snapshot analysis exported to: {output_file}")
|
219
|
+
|
220
|
+
# Enhanced cost analysis for JIRA FinOps-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 FinOps-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 FinOps-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
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
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 FinOps-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
|
-
|
321
|
+
print_success("✓ No old manual snapshots found")
|
322
|
+
|
323
|
+
# Target validation (JIRA FinOps-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 FinOps-23 range (${target_min_annual:,.0f}-${target_max_annual:,.0f})")
|
332
|
+
else:
|
333
|
+
print_success(f"🎯 Target Exceeded: ${actual_savings:,.0f} exceeds JIRA FinOps-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 FinOps-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"])
|