runbooks 1.1.9__py3-none-any.whl → 1.1.10__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 +1 -1
- runbooks/__init___optimized.py +2 -1
- runbooks/_platform/__init__.py +1 -1
- runbooks/cfat/cli.py +4 -3
- runbooks/cfat/cloud_foundations_assessment.py +1 -2
- runbooks/cfat/tests/test_cli.py +4 -1
- runbooks/cli/commands/finops.py +68 -19
- runbooks/cli/commands/inventory.py +796 -7
- runbooks/cli/commands/operate.py +65 -4
- runbooks/cloudops/cost_optimizer.py +1 -3
- runbooks/common/cli_decorators.py +6 -4
- runbooks/common/config_loader.py +787 -0
- runbooks/common/config_schema.py +280 -0
- runbooks/common/dry_run_framework.py +14 -2
- runbooks/common/mcp_integration.py +238 -0
- runbooks/finops/ebs_cost_optimizer.py +7 -4
- runbooks/finops/elastic_ip_optimizer.py +7 -4
- runbooks/finops/infrastructure/__init__.py +3 -2
- runbooks/finops/infrastructure/commands.py +7 -4
- runbooks/finops/infrastructure/load_balancer_optimizer.py +7 -4
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +7 -4
- runbooks/finops/nat_gateway_optimizer.py +7 -4
- runbooks/finops/tests/run_tests.py +1 -1
- runbooks/inventory/ArgumentsClass.py +2 -1
- runbooks/inventory/README.md +111 -12
- runbooks/inventory/Tests/test_Inventory_Modules.py +27 -10
- runbooks/inventory/Tests/test_cfn_describe_stacks.py +18 -7
- runbooks/inventory/Tests/test_ec2_describe_instances.py +30 -15
- runbooks/inventory/Tests/test_lambda_list_functions.py +17 -3
- runbooks/inventory/Tests/test_org_list_accounts.py +17 -4
- runbooks/inventory/account_class.py +0 -1
- runbooks/inventory/all_my_instances_wrapper.py +4 -8
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/check_cloudtrail_compliance.py +4 -4
- runbooks/inventory/check_controltower_readiness.py +50 -47
- runbooks/inventory/check_landingzone_readiness.py +35 -31
- runbooks/inventory/cloud_foundations_integration.py +8 -3
- runbooks/inventory/core/collector.py +201 -1
- runbooks/inventory/discovery.md +2 -1
- runbooks/inventory/{draw_org_structure.py → draw_org.py} +55 -9
- runbooks/inventory/drift_detection_cli.py +8 -68
- runbooks/inventory/find_cfn_drift_detection.py +14 -4
- runbooks/inventory/find_cfn_orphaned_stacks.py +7 -5
- runbooks/inventory/find_cfn_stackset_drift.py +5 -5
- runbooks/inventory/find_ec2_security_groups.py +6 -3
- runbooks/inventory/find_landingzone_versions.py +5 -5
- runbooks/inventory/find_vpc_flow_logs.py +5 -5
- runbooks/inventory/inventory.sh +20 -7
- runbooks/inventory/inventory_mcp_cli.py +4 -0
- runbooks/inventory/inventory_modules.py +9 -7
- runbooks/inventory/list_cfn_stacks.py +18 -8
- runbooks/inventory/list_cfn_stackset_operation_results.py +2 -2
- runbooks/inventory/list_cfn_stackset_operations.py +32 -20
- runbooks/inventory/list_cfn_stacksets.py +7 -4
- runbooks/inventory/list_config_recorders_delivery_channels.py +4 -4
- runbooks/inventory/list_ds_directories.py +3 -3
- runbooks/inventory/list_ec2_availability_zones.py +7 -3
- runbooks/inventory/list_ec2_ebs_volumes.py +3 -3
- runbooks/inventory/list_ec2_instances.py +1 -1
- runbooks/inventory/list_ecs_clusters_and_tasks.py +8 -4
- runbooks/inventory/list_elbs_load_balancers.py +7 -3
- runbooks/inventory/list_enis_network_interfaces.py +3 -3
- runbooks/inventory/list_guardduty_detectors.py +9 -5
- runbooks/inventory/list_iam_policies.py +7 -3
- runbooks/inventory/list_iam_roles.py +3 -3
- runbooks/inventory/list_iam_saml_providers.py +8 -4
- runbooks/inventory/list_lambda_functions.py +8 -4
- runbooks/inventory/list_org_accounts.py +306 -276
- runbooks/inventory/list_org_accounts_users.py +45 -9
- runbooks/inventory/list_rds_db_instances.py +4 -4
- runbooks/inventory/list_route53_hosted_zones.py +3 -3
- runbooks/inventory/list_servicecatalog_provisioned_products.py +5 -5
- runbooks/inventory/list_sns_topics.py +4 -4
- runbooks/inventory/list_ssm_parameters.py +6 -3
- runbooks/inventory/list_vpc_subnets.py +8 -4
- runbooks/inventory/list_vpcs.py +15 -4
- runbooks/inventory/mcp_vpc_validator.py +6 -0
- runbooks/inventory/organizations_discovery.py +17 -3
- runbooks/inventory/organizations_utils.py +553 -0
- runbooks/inventory/output_formatters.py +422 -0
- runbooks/inventory/recover_cfn_stack_ids.py +5 -5
- runbooks/inventory/run_on_multi_accounts.py +3 -3
- runbooks/inventory/tag_coverage.py +481 -0
- runbooks/inventory/validation_utils.py +358 -0
- runbooks/inventory/verify_ec2_security_groups.py +18 -5
- runbooks/inventory/vpc_architecture_validator.py +7 -1
- runbooks/inventory/vpc_dependency_analyzer.py +6 -0
- runbooks/main_final.py +2 -2
- runbooks/main_ultra_minimal.py +2 -2
- runbooks/mcp/integration.py +6 -4
- runbooks/remediation/acm_remediation.py +2 -2
- runbooks/remediation/cloudtrail_remediation.py +2 -2
- runbooks/remediation/cognito_remediation.py +2 -2
- runbooks/remediation/dynamodb_remediation.py +2 -2
- runbooks/remediation/ec2_remediation.py +2 -2
- runbooks/remediation/kms_remediation.py +2 -2
- runbooks/remediation/lambda_remediation.py +2 -2
- runbooks/remediation/rds_remediation.py +2 -2
- runbooks/remediation/s3_remediation.py +1 -1
- runbooks/vpc/cloudtrail_audit_integration.py +1 -1
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/METADATA +74 -4
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/RECORD +106 -100
- runbooks/__init__.py.backup +0 -134
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/WHEEL +0 -0
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,481 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Tag coverage analysis and Rich CLI status display for AWS Organizations.
|
4
|
+
|
5
|
+
Provides visual feedback on AWS tag coverage across organization accounts
|
6
|
+
with tier-based analysis, actionable recommendations, and configurable
|
7
|
+
tag mapping validation.
|
8
|
+
|
9
|
+
Features:
|
10
|
+
- Tier-based tag coverage analysis (TIER 1-4)
|
11
|
+
- Rich CLI status displays with color-coded indicators
|
12
|
+
- Configurable tag mapping validation
|
13
|
+
- Multi-tenant portability across Landing Zone configurations
|
14
|
+
- Actionable recommendations for tag standardization
|
15
|
+
|
16
|
+
Architecture:
|
17
|
+
- Hierarchical config precedence: CLI > env > project > user > defaults
|
18
|
+
- Zero-coverage detection for misconfigured tag mappings
|
19
|
+
- Professional Rich formatting with CloudOps theme
|
20
|
+
|
21
|
+
Author: CloudOps Runbooks Team
|
22
|
+
Version: 1.1.10
|
23
|
+
"""
|
24
|
+
|
25
|
+
import logging
|
26
|
+
from collections import defaultdict
|
27
|
+
from typing import Any, Dict, List, Optional
|
28
|
+
|
29
|
+
from rich import box
|
30
|
+
from rich.panel import Panel
|
31
|
+
from rich.table import Table
|
32
|
+
from rich.text import Text
|
33
|
+
|
34
|
+
from runbooks.common.rich_utils import STATUS_INDICATORS, console, print_header
|
35
|
+
|
36
|
+
logger = logging.getLogger(__name__)
|
37
|
+
|
38
|
+
|
39
|
+
class TagCoverageAnalyzer:
|
40
|
+
"""
|
41
|
+
Analyze and display tag coverage across AWS accounts.
|
42
|
+
|
43
|
+
Provides comprehensive tag coverage analysis with:
|
44
|
+
- Per-field coverage statistics (populated vs total accounts)
|
45
|
+
- Tier-level coverage analysis (all fields in tier must be populated)
|
46
|
+
- Visual Rich CLI status with color-coded indicators
|
47
|
+
- Actionable recommendations for tag standardization
|
48
|
+
|
49
|
+
Coverage Thresholds:
|
50
|
+
≥90% = Excellent (Green ✅)
|
51
|
+
80-89% = Good (Yellow ⚠️)
|
52
|
+
50-79% = Medium (Orange ⚠️)
|
53
|
+
<50% = Poor (Red ❌)
|
54
|
+
|
55
|
+
Tag Tiers:
|
56
|
+
TIER 1: Business Metadata (4 critical fields)
|
57
|
+
TIER 2: Governance Metadata (4 important fields)
|
58
|
+
TIER 3: Operational Metadata (4 standard fields)
|
59
|
+
TIER 4: Extended Metadata (5 optional fields)
|
60
|
+
|
61
|
+
Example:
|
62
|
+
>>> accounts = [
|
63
|
+
... {'id': '123456789012', 'wbs_code': 'WBS-001', 'cost_group': 'Engineering'},
|
64
|
+
... {'id': '234567890123', 'wbs_code': 'N/A', 'cost_group': 'Finance'}
|
65
|
+
... ]
|
66
|
+
>>> tag_mappings = {'wbs_code': 'WBS', 'cost_group': 'CostGroup'}
|
67
|
+
>>> analyzer = TagCoverageAnalyzer(accounts, tag_mappings, logger)
|
68
|
+
>>> analyzer.display_coverage_status()
|
69
|
+
"""
|
70
|
+
|
71
|
+
# Tag tier definitions (aligned with config_schema.py)
|
72
|
+
TIER_1_FIELDS = ["wbs_code", "cost_group", "technical_lead", "account_owner"]
|
73
|
+
TIER_2_FIELDS = ["business_unit", "functional_area", "managed_by", "product_owner"]
|
74
|
+
TIER_3_FIELDS = ["purpose", "environment", "compliance_scope", "data_classification"]
|
75
|
+
TIER_4_FIELDS = ["project_name", "budget_code", "support_tier", "created_date", "expiry_date"]
|
76
|
+
|
77
|
+
# Coverage thresholds
|
78
|
+
COVERAGE_EXCELLENT = 90.0 # ≥90% = Green ✅
|
79
|
+
COVERAGE_GOOD = 80.0 # 80-89% = Yellow ⚠️
|
80
|
+
COVERAGE_MEDIUM = 50.0 # 50-79% = Orange ⚠️
|
81
|
+
# <50% = Red ❌
|
82
|
+
|
83
|
+
def __init__(self, accounts: List[Dict[str, Any]], tag_mappings: Dict[str, str], logger: logging.Logger):
|
84
|
+
"""
|
85
|
+
Initialize coverage analyzer.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
accounts: List of account dicts with tag data (must include tag-based fields)
|
89
|
+
tag_mappings: Field name → AWS tag key mappings (from ConfigLoader)
|
90
|
+
logger: Logger instance for diagnostic output
|
91
|
+
|
92
|
+
Example:
|
93
|
+
>>> accounts = [{'id': '123', 'wbs_code': 'WBS-001'}]
|
94
|
+
>>> mappings = {'wbs_code': 'WBS'}
|
95
|
+
>>> analyzer = TagCoverageAnalyzer(accounts, mappings, logger)
|
96
|
+
"""
|
97
|
+
self.accounts = accounts
|
98
|
+
self.tag_mappings = tag_mappings
|
99
|
+
self.logger = logger
|
100
|
+
self.coverage_stats = self._calculate_coverage()
|
101
|
+
|
102
|
+
def _calculate_coverage(self) -> Dict[str, Any]:
|
103
|
+
"""
|
104
|
+
Calculate tag coverage statistics per field.
|
105
|
+
|
106
|
+
Computes:
|
107
|
+
1. Per-field coverage: percentage of accounts with populated tags
|
108
|
+
2. Tier-level coverage: percentage of accounts with ALL tier fields populated
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
Dictionary containing:
|
112
|
+
- Per-field stats: {field_name: {populated, total, percentage, tag_key}}
|
113
|
+
- Tier stats: {tier_X_overall: {populated, total, percentage}}
|
114
|
+
|
115
|
+
Example:
|
116
|
+
>>> analyzer = TagCoverageAnalyzer([{'id': '123', 'wbs_code': 'WBS-001'}],
|
117
|
+
... {'wbs_code': 'WBS'}, logger)
|
118
|
+
>>> stats = analyzer._calculate_coverage()
|
119
|
+
>>> 'wbs_code' in stats
|
120
|
+
True
|
121
|
+
"""
|
122
|
+
total_accounts = len(self.accounts)
|
123
|
+
if total_accounts == 0:
|
124
|
+
self.logger.warning("No accounts provided for tag coverage analysis")
|
125
|
+
return {}
|
126
|
+
|
127
|
+
coverage = {}
|
128
|
+
|
129
|
+
# Calculate per-field coverage
|
130
|
+
for field_name in self.tag_mappings.keys():
|
131
|
+
populated_count = sum(
|
132
|
+
1 for account in self.accounts if account.get(field_name) not in ["N/A", None, ""]
|
133
|
+
)
|
134
|
+
|
135
|
+
coverage_pct = (populated_count / total_accounts) * 100
|
136
|
+
|
137
|
+
coverage[field_name] = {
|
138
|
+
"populated": populated_count,
|
139
|
+
"total": total_accounts,
|
140
|
+
"percentage": coverage_pct,
|
141
|
+
"tag_key": self.tag_mappings[field_name],
|
142
|
+
}
|
143
|
+
|
144
|
+
# Calculate tier-level coverage (all fields in tier populated)
|
145
|
+
coverage["tier_1_overall"] = self._calculate_tier_coverage(self.TIER_1_FIELDS)
|
146
|
+
coverage["tier_2_overall"] = self._calculate_tier_coverage(self.TIER_2_FIELDS)
|
147
|
+
|
148
|
+
self.logger.info(
|
149
|
+
f"Tag coverage calculated: {total_accounts} accounts, "
|
150
|
+
f"TIER 1: {coverage['tier_1_overall']['percentage']:.1f}%, "
|
151
|
+
f"TIER 2: {coverage['tier_2_overall']['percentage']:.1f}%"
|
152
|
+
)
|
153
|
+
|
154
|
+
return coverage
|
155
|
+
|
156
|
+
def _calculate_tier_coverage(self, tier_fields: List[str]) -> Dict[str, Any]:
|
157
|
+
"""
|
158
|
+
Calculate overall coverage for a tier (all fields populated).
|
159
|
+
|
160
|
+
For an account to have complete tier coverage, ALL fields in that tier
|
161
|
+
must be populated (not 'N/A', None, or empty string).
|
162
|
+
|
163
|
+
Args:
|
164
|
+
tier_fields: List of field names in this tier
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
Dictionary with populated count, total accounts, and percentage
|
168
|
+
|
169
|
+
Example:
|
170
|
+
>>> analyzer = TagCoverageAnalyzer(
|
171
|
+
... [{'wbs_code': 'WBS-001', 'cost_group': 'Eng', 'technical_lead': 'N/A'}],
|
172
|
+
... {'wbs_code': 'WBS', 'cost_group': 'CostGroup', 'technical_lead': 'TechLead'},
|
173
|
+
... logger
|
174
|
+
... )
|
175
|
+
>>> tier_coverage = analyzer._calculate_tier_coverage(['wbs_code', 'cost_group'])
|
176
|
+
>>> tier_coverage['percentage']
|
177
|
+
100.0
|
178
|
+
"""
|
179
|
+
total_accounts = len(self.accounts)
|
180
|
+
if total_accounts == 0:
|
181
|
+
return {"populated": 0, "total": 0, "percentage": 0.0}
|
182
|
+
|
183
|
+
# Count accounts with ALL tier fields populated
|
184
|
+
fully_tagged_count = sum(
|
185
|
+
1
|
186
|
+
for account in self.accounts
|
187
|
+
if all(
|
188
|
+
account.get(field) not in ["N/A", None, ""]
|
189
|
+
for field in tier_fields
|
190
|
+
if field in self.tag_mappings
|
191
|
+
)
|
192
|
+
)
|
193
|
+
|
194
|
+
coverage_pct = (fully_tagged_count / total_accounts) * 100
|
195
|
+
|
196
|
+
return {"populated": fully_tagged_count, "total": total_accounts, "percentage": coverage_pct}
|
197
|
+
|
198
|
+
def _get_coverage_indicator(self, percentage: float) -> str:
|
199
|
+
"""
|
200
|
+
Get Rich status indicator for coverage percentage.
|
201
|
+
|
202
|
+
Args:
|
203
|
+
percentage: Coverage percentage (0-100)
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
Status indicator emoji (✅ ⚠️ ❌)
|
207
|
+
|
208
|
+
Example:
|
209
|
+
>>> analyzer = TagCoverageAnalyzer([], {}, logger)
|
210
|
+
>>> analyzer._get_coverage_indicator(95.0)
|
211
|
+
'🟢'
|
212
|
+
"""
|
213
|
+
if percentage >= self.COVERAGE_EXCELLENT:
|
214
|
+
return STATUS_INDICATORS["success"] # ✅
|
215
|
+
elif percentage >= self.COVERAGE_GOOD:
|
216
|
+
return STATUS_INDICATORS["warning"] # ⚠️
|
217
|
+
elif percentage >= self.COVERAGE_MEDIUM:
|
218
|
+
return STATUS_INDICATORS["warning"] # ⚠️
|
219
|
+
else:
|
220
|
+
return STATUS_INDICATORS["error"] # ❌
|
221
|
+
|
222
|
+
def _get_coverage_color(self, percentage: float) -> str:
|
223
|
+
"""
|
224
|
+
Get Rich color for coverage percentage.
|
225
|
+
|
226
|
+
Args:
|
227
|
+
percentage: Coverage percentage (0-100)
|
228
|
+
|
229
|
+
Returns:
|
230
|
+
Rich color name (green, yellow, orange, red)
|
231
|
+
|
232
|
+
Example:
|
233
|
+
>>> analyzer = TagCoverageAnalyzer([], {}, logger)
|
234
|
+
>>> analyzer._get_coverage_color(85.0)
|
235
|
+
'yellow'
|
236
|
+
"""
|
237
|
+
if percentage >= self.COVERAGE_EXCELLENT:
|
238
|
+
return "green"
|
239
|
+
elif percentage >= self.COVERAGE_GOOD:
|
240
|
+
return "yellow"
|
241
|
+
elif percentage >= self.COVERAGE_MEDIUM:
|
242
|
+
return "orange"
|
243
|
+
else:
|
244
|
+
return "red"
|
245
|
+
|
246
|
+
def display_coverage_status(self, config_source: str = "hierarchical config") -> None:
|
247
|
+
"""
|
248
|
+
Display tag coverage status with Rich formatting.
|
249
|
+
|
250
|
+
Creates professional Rich CLI display with:
|
251
|
+
- Per-field coverage table (TIER 1 & 2 fields)
|
252
|
+
- Color-coded status indicators
|
253
|
+
- Overall TIER 1 coverage summary
|
254
|
+
- Top 3 actionable recommendations
|
255
|
+
|
256
|
+
Args:
|
257
|
+
config_source: Description of configuration source for display
|
258
|
+
|
259
|
+
Example:
|
260
|
+
>>> accounts = [{'wbs_code': 'WBS-001', 'cost_group': 'Engineering'}]
|
261
|
+
>>> mappings = {'wbs_code': 'WBS', 'cost_group': 'CostGroup'}
|
262
|
+
>>> analyzer = TagCoverageAnalyzer(accounts, mappings, logger)
|
263
|
+
>>> analyzer.display_coverage_status("user config")
|
264
|
+
"""
|
265
|
+
# Build coverage table
|
266
|
+
table = Table(title="Tag Coverage Analysis", box=box.ROUNDED, show_header=True, header_style="bold cyan")
|
267
|
+
|
268
|
+
table.add_column("Field Name", style="white", width=20)
|
269
|
+
table.add_column("AWS Tag Key", style="cyan", width=20)
|
270
|
+
table.add_column("Coverage", justify="right", style="white", width=15)
|
271
|
+
table.add_column("Status", justify="center", width=8)
|
272
|
+
|
273
|
+
# TIER 1: Business Metadata
|
274
|
+
table.add_row(
|
275
|
+
Text("TIER 1: Business Metadata", style="bold yellow"), "", "", "", style="bold yellow"
|
276
|
+
)
|
277
|
+
for field in self.TIER_1_FIELDS:
|
278
|
+
if field in self.coverage_stats:
|
279
|
+
self._add_coverage_row(table, field)
|
280
|
+
|
281
|
+
# TIER 2: Governance Metadata
|
282
|
+
table.add_row("", "", "", "") # Spacer
|
283
|
+
table.add_row(
|
284
|
+
Text("TIER 2: Governance Metadata", style="bold magenta"), "", "", "", style="bold magenta"
|
285
|
+
)
|
286
|
+
for field in self.TIER_2_FIELDS:
|
287
|
+
if field in self.coverage_stats:
|
288
|
+
self._add_coverage_row(table, field)
|
289
|
+
|
290
|
+
# Build summary
|
291
|
+
tier1_coverage = self.coverage_stats["tier_1_overall"]["percentage"]
|
292
|
+
tier1_indicator = self._get_coverage_indicator(tier1_coverage)
|
293
|
+
tier1_color = self._get_coverage_color(tier1_coverage)
|
294
|
+
|
295
|
+
summary_text = Text()
|
296
|
+
summary_text.append("Configuration Source: ", style="bold white")
|
297
|
+
summary_text.append(f"{config_source}\n\n", style="cyan")
|
298
|
+
|
299
|
+
summary_text.append("TIER 1 Overall Coverage: ", style="bold white")
|
300
|
+
summary_text.append(f"{tier1_coverage:.1f}% {tier1_indicator}", style=tier1_color)
|
301
|
+
summary_text.append(
|
302
|
+
f" ({self.coverage_stats['tier_1_overall']['populated']}/"
|
303
|
+
f"{self.coverage_stats['tier_1_overall']['total']} accounts)\n",
|
304
|
+
style="white",
|
305
|
+
)
|
306
|
+
|
307
|
+
# Add recommendations
|
308
|
+
recommendations = self._generate_recommendations()
|
309
|
+
if recommendations:
|
310
|
+
summary_text.append("\n💡 Recommendations:\n", style="bold yellow")
|
311
|
+
for rec in recommendations[:3]: # Top 3
|
312
|
+
summary_text.append(f" • {rec}\n", style="white")
|
313
|
+
|
314
|
+
# Display panel
|
315
|
+
panel = Panel(
|
316
|
+
table,
|
317
|
+
title="[bold cyan]Tag Mapping Status[/bold cyan]",
|
318
|
+
subtitle=summary_text,
|
319
|
+
border_style="cyan",
|
320
|
+
box=box.DOUBLE,
|
321
|
+
)
|
322
|
+
|
323
|
+
console.print("\n")
|
324
|
+
console.print(panel)
|
325
|
+
console.print("\n")
|
326
|
+
|
327
|
+
def _add_coverage_row(self, table: Table, field_name: str) -> None:
|
328
|
+
"""
|
329
|
+
Add coverage row to table.
|
330
|
+
|
331
|
+
Args:
|
332
|
+
table: Rich Table instance to add row to
|
333
|
+
field_name: Field name to add row for
|
334
|
+
|
335
|
+
Example:
|
336
|
+
>>> table = Table()
|
337
|
+
>>> analyzer = TagCoverageAnalyzer(
|
338
|
+
... [{'wbs_code': 'WBS-001'}], {'wbs_code': 'WBS'}, logger
|
339
|
+
... )
|
340
|
+
>>> analyzer._add_coverage_row(table, 'wbs_code')
|
341
|
+
"""
|
342
|
+
stats = self.coverage_stats[field_name]
|
343
|
+
percentage = stats["percentage"]
|
344
|
+
indicator = self._get_coverage_indicator(percentage)
|
345
|
+
color = self._get_coverage_color(percentage)
|
346
|
+
|
347
|
+
coverage_text = f"{stats['populated']}/{stats['total']} ({percentage:.1f}%)"
|
348
|
+
|
349
|
+
table.add_row(f" {field_name}", stats["tag_key"], Text(coverage_text, style=color), indicator)
|
350
|
+
|
351
|
+
def _generate_recommendations(self) -> List[str]:
|
352
|
+
"""
|
353
|
+
Generate actionable recommendations based on coverage.
|
354
|
+
|
355
|
+
Analyzes coverage gaps and provides specific recommendations for:
|
356
|
+
1. Low-coverage critical fields (TIER 1 <80%)
|
357
|
+
2. Overall TIER 1 coverage below target
|
358
|
+
3. Zero-coverage fields (potential configuration errors)
|
359
|
+
|
360
|
+
Returns:
|
361
|
+
List of recommendation strings (ordered by priority)
|
362
|
+
|
363
|
+
Example:
|
364
|
+
>>> accounts = [{'wbs_code': 'N/A', 'cost_group': 'N/A'}]
|
365
|
+
>>> mappings = {'wbs_code': 'WBS', 'cost_group': 'CostGroup'}
|
366
|
+
>>> analyzer = TagCoverageAnalyzer(accounts, mappings, logger)
|
367
|
+
>>> recommendations = analyzer._generate_recommendations()
|
368
|
+
>>> len(recommendations) > 0
|
369
|
+
True
|
370
|
+
"""
|
371
|
+
recommendations = []
|
372
|
+
|
373
|
+
# Check TIER 1 critical fields
|
374
|
+
for field in self.TIER_1_FIELDS:
|
375
|
+
if field in self.coverage_stats:
|
376
|
+
stats = self.coverage_stats[field]
|
377
|
+
if stats["percentage"] < self.COVERAGE_GOOD:
|
378
|
+
missing_count = stats["total"] - stats["populated"]
|
379
|
+
recommendations.append(
|
380
|
+
f"Review '{field}' tag configuration - "
|
381
|
+
f"{missing_count} accounts missing '{stats['tag_key']}' tag"
|
382
|
+
)
|
383
|
+
|
384
|
+
# Check overall TIER 1 coverage
|
385
|
+
tier1_coverage = self.coverage_stats["tier_1_overall"]["percentage"]
|
386
|
+
if tier1_coverage < self.COVERAGE_GOOD:
|
387
|
+
recommendations.append(
|
388
|
+
f"Overall TIER 1 coverage is {tier1_coverage:.1f}% (target: ≥80%). "
|
389
|
+
"Consider standardizing tag naming across AWS accounts."
|
390
|
+
)
|
391
|
+
|
392
|
+
# Check for zero coverage (wrong tag mapping?)
|
393
|
+
zero_coverage_fields = [
|
394
|
+
field
|
395
|
+
for field, stats in self.coverage_stats.items()
|
396
|
+
if isinstance(stats, dict) and stats.get("percentage", 100) == 0 and field in self.TIER_1_FIELDS
|
397
|
+
]
|
398
|
+
if zero_coverage_fields:
|
399
|
+
recommendations.append(
|
400
|
+
f"Zero coverage for: {', '.join(zero_coverage_fields)}. "
|
401
|
+
"Verify tag mapping configuration matches AWS tag keys."
|
402
|
+
)
|
403
|
+
|
404
|
+
return recommendations
|
405
|
+
|
406
|
+
def get_coverage_summary(self) -> Dict[str, Any]:
|
407
|
+
"""
|
408
|
+
Get coverage summary for programmatic access.
|
409
|
+
|
410
|
+
Returns:
|
411
|
+
Dictionary containing:
|
412
|
+
- total_accounts: Number of accounts analyzed
|
413
|
+
- tier_1_coverage: TIER 1 overall coverage percentage
|
414
|
+
- tier_2_coverage: TIER 2 overall coverage percentage
|
415
|
+
- field_coverage: Per-field coverage percentages
|
416
|
+
- recommendations: List of actionable recommendations
|
417
|
+
|
418
|
+
Example:
|
419
|
+
>>> accounts = [{'wbs_code': 'WBS-001'}]
|
420
|
+
>>> mappings = {'wbs_code': 'WBS'}
|
421
|
+
>>> analyzer = TagCoverageAnalyzer(accounts, mappings, logger)
|
422
|
+
>>> summary = analyzer.get_coverage_summary()
|
423
|
+
>>> 'total_accounts' in summary
|
424
|
+
True
|
425
|
+
"""
|
426
|
+
return {
|
427
|
+
"total_accounts": len(self.accounts),
|
428
|
+
"tier_1_coverage": self.coverage_stats["tier_1_overall"]["percentage"],
|
429
|
+
"tier_2_coverage": self.coverage_stats["tier_2_overall"]["percentage"],
|
430
|
+
"field_coverage": {
|
431
|
+
field: stats["percentage"]
|
432
|
+
for field, stats in self.coverage_stats.items()
|
433
|
+
if isinstance(stats, dict) and "percentage" in stats
|
434
|
+
},
|
435
|
+
"recommendations": self._generate_recommendations(),
|
436
|
+
}
|
437
|
+
|
438
|
+
|
439
|
+
def display_tag_coverage_status(
|
440
|
+
accounts: List[Dict[str, Any]],
|
441
|
+
tag_mappings: Dict[str, str],
|
442
|
+
config_source: str,
|
443
|
+
logger: logging.Logger,
|
444
|
+
) -> None:
|
445
|
+
"""
|
446
|
+
Display tag coverage statistics with Rich formatting (convenience function).
|
447
|
+
|
448
|
+
Creates TagCoverageAnalyzer and displays professional Rich CLI status.
|
449
|
+
This is the primary public API for tag coverage display.
|
450
|
+
|
451
|
+
Args:
|
452
|
+
accounts: List of account dicts with tag data
|
453
|
+
tag_mappings: Field name → AWS tag key mappings
|
454
|
+
config_source: Description of config source (e.g., "user config", "CLI overrides")
|
455
|
+
logger: Logger instance
|
456
|
+
|
457
|
+
Example:
|
458
|
+
>>> accounts = [
|
459
|
+
... {'id': '123456789012', 'wbs_code': 'WBS-001', 'cost_group': 'Engineering'},
|
460
|
+
... {'id': '234567890123', 'wbs_code': 'WBS-002', 'cost_group': 'Finance'}
|
461
|
+
... ]
|
462
|
+
>>> mappings = {'wbs_code': 'WBS', 'cost_group': 'CostGroup'}
|
463
|
+
>>> display_tag_coverage_status(accounts, mappings, "defaults", logger)
|
464
|
+
"""
|
465
|
+
analyzer = TagCoverageAnalyzer(accounts, tag_mappings, logger)
|
466
|
+
analyzer.display_coverage_status(config_source)
|
467
|
+
|
468
|
+
# Log coverage summary
|
469
|
+
summary = analyzer.get_coverage_summary()
|
470
|
+
logger.info(
|
471
|
+
f"Tag coverage analysis complete: "
|
472
|
+
f"TIER 1: {summary['tier_1_coverage']:.1f}%, "
|
473
|
+
f"Total accounts: {summary['total_accounts']}"
|
474
|
+
)
|
475
|
+
|
476
|
+
|
477
|
+
# Module exports
|
478
|
+
__all__ = [
|
479
|
+
"TagCoverageAnalyzer",
|
480
|
+
"display_tag_coverage_status",
|
481
|
+
]
|