runbooks 1.1.3__py3-none-any.whl → 1.1.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.
- runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
- runbooks/cfat/assessment/compliance.py +7 -7
- runbooks/cfat/models.py +6 -2
- runbooks/cfat/tests/__init__.py +6 -1
- runbooks/cli/__init__.py +13 -0
- runbooks/cli/commands/cfat.py +233 -0
- runbooks/cli/commands/finops.py +213 -0
- runbooks/cli/commands/inventory.py +276 -0
- runbooks/cli/commands/operate.py +266 -0
- runbooks/cli/commands/security.py +224 -0
- runbooks/cli/commands/validation.py +411 -0
- runbooks/cli/commands/vpc.py +246 -0
- runbooks/cli/registry.py +95 -0
- runbooks/cloudops/__init__.py +3 -3
- runbooks/cloudops/cost_optimizer.py +6 -6
- runbooks/cloudops/interfaces.py +2 -2
- runbooks/cloudops/mcp_cost_validation.py +3 -3
- runbooks/cloudops/notebook_framework.py +2 -2
- runbooks/common/aws_profile_manager.py +337 -0
- runbooks/common/aws_utils.py +1 -1
- runbooks/common/business_logic.py +3 -3
- runbooks/common/comprehensive_cost_explorer_integration.py +1 -1
- runbooks/common/cross_account_manager.py +1 -1
- runbooks/common/decorators.py +225 -0
- runbooks/common/mcp_cost_explorer_integration.py +2 -2
- runbooks/common/organizations_client.py +1 -1
- runbooks/common/patterns.py +206 -0
- runbooks/common/profile_utils.py +149 -14
- runbooks/common/rich_utils.py +502 -11
- runbooks/finops/README.md +8 -8
- runbooks/finops/__init__.py +4 -4
- runbooks/finops/business_cases.py +3 -3
- runbooks/finops/cost_optimizer.py +4 -4
- runbooks/finops/dashboard_router.py +2 -2
- runbooks/finops/ebs_cost_optimizer.py +4 -4
- runbooks/finops/ebs_optimizer.py +19 -2
- runbooks/finops/enhanced_progress.py +8 -8
- runbooks/finops/enterprise_wrappers.py +7 -7
- runbooks/finops/finops_scenarios.py +11 -11
- runbooks/finops/legacy_migration.py +8 -8
- runbooks/finops/markdown_exporter.py +2 -2
- runbooks/finops/multi_dashboard.py +1 -1
- runbooks/finops/nat_gateway_optimizer.py +1 -1
- runbooks/finops/optimizer.py +6 -6
- runbooks/finops/rds_snapshot_optimizer.py +2 -2
- runbooks/finops/scenario_cli_integration.py +13 -13
- runbooks/finops/scenarios.py +16 -16
- runbooks/finops/single_dashboard.py +10 -10
- runbooks/finops/tests/test_finops_dashboard.py +3 -3
- runbooks/finops/tests/test_reference_images_validation.py +2 -2
- runbooks/finops/tests/test_single_account_features.py +17 -17
- runbooks/finops/tests/validate_test_suite.py +1 -1
- runbooks/finops/validation_framework.py +5 -5
- runbooks/finops/vpc_cleanup_exporter.py +3 -3
- runbooks/finops/vpc_cleanup_optimizer.py +2 -2
- runbooks/finops/workspaces_analyzer.py +1 -1
- runbooks/hitl/enhanced_workflow_engine.py +1 -1
- runbooks/inventory/README.md +3 -3
- runbooks/inventory/Tests/common_test_data.py +30 -30
- runbooks/inventory/collectors/aws_comprehensive.py +28 -11
- runbooks/inventory/collectors/aws_networking.py +2 -2
- runbooks/inventory/discovery.md +2 -2
- runbooks/inventory/find_ec2_security_groups.py +1 -1
- runbooks/inventory/organizations_discovery.py +1 -1
- runbooks/inventory/vpc_analyzer.py +1 -1
- runbooks/inventory/vpc_flow_analyzer.py +2 -2
- runbooks/main.py +143 -9153
- runbooks/metrics/dora_metrics_engine.py +2 -2
- runbooks/operate/mcp_integration.py +1 -1
- runbooks/operate/networking_cost_heatmap.py +4 -2
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/operate/vpc_operations.py +2 -2
- runbooks/remediation/commvault_ec2_analysis.py +1 -1
- runbooks/remediation/rds_snapshot_list.py +5 -5
- runbooks/remediation/workspaces_list.py +5 -5
- runbooks/security/integration_test_enterprise_security.py +5 -3
- runbooks/security/run_script.py +1 -1
- runbooks/sre/mcp_reliability_engine.py +6 -6
- runbooks/utils/version_validator.py +1 -1
- runbooks/validation/comprehensive_2way_validator.py +9 -4
- runbooks/vpc/heatmap_engine.py +7 -4
- runbooks/vpc/mcp_no_eni_validator.py +1 -1
- runbooks/vpc/unified_scenarios.py +7 -7
- {runbooks-1.1.3.dist-info → runbooks-1.1.4.dist-info}/METADATA +53 -52
- {runbooks-1.1.3.dist-info → runbooks-1.1.4.dist-info}/RECORD +90 -78
- {runbooks-1.1.3.dist-info → runbooks-1.1.4.dist-info}/WHEEL +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.4.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.4.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,337 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
AWS Profile Manager - Universal v1.1.x Compatibility
|
4
|
+
|
5
|
+
Centralized AWS profile and account management for CloudOps Runbooks platform.
|
6
|
+
Eliminates ALL hardcoded account IDs and provides universal --profile support.
|
7
|
+
|
8
|
+
Features:
|
9
|
+
- 3-tier profile priority: User > Environment > Default
|
10
|
+
- Dynamic account ID resolution
|
11
|
+
- Multi-account discovery and validation
|
12
|
+
- Profile existence validation with helpful error messages
|
13
|
+
- Integration with Rich CLI for beautiful output
|
14
|
+
|
15
|
+
Author: CloudOps Runbooks Team
|
16
|
+
Version: 1.1.0
|
17
|
+
"""
|
18
|
+
|
19
|
+
import os
|
20
|
+
import boto3
|
21
|
+
from typing import Dict, List, Optional, Any
|
22
|
+
from botocore.exceptions import ClientError, ProfileNotFound, NoCredentialsError
|
23
|
+
from rich.console import Console
|
24
|
+
|
25
|
+
from runbooks.common.rich_utils import console, print_error, print_success, print_warning, print_info
|
26
|
+
|
27
|
+
class AWSProfileManager:
|
28
|
+
"""
|
29
|
+
Universal AWS Profile Manager for CloudOps v1.1.x compatibility.
|
30
|
+
|
31
|
+
Provides centralized profile management, account resolution, and
|
32
|
+
multi-account discovery for all CloudOps modules.
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self, profile: Optional[str] = None):
|
36
|
+
"""
|
37
|
+
Initialize ProfileManager with 3-tier priority.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
profile: User-specified profile (highest priority)
|
41
|
+
"""
|
42
|
+
self.profile = self._resolve_profile(profile)
|
43
|
+
self.session = None
|
44
|
+
self._account_cache: Dict[str, str] = {}
|
45
|
+
|
46
|
+
def _resolve_profile(self, user_profile: Optional[str]) -> Optional[str]:
|
47
|
+
"""
|
48
|
+
Resolve profile using 3-tier priority: User > Environment > Default
|
49
|
+
|
50
|
+
Args:
|
51
|
+
user_profile: User-specified profile
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
Resolved profile name or None for default
|
55
|
+
"""
|
56
|
+
# Tier 1: User-specified profile (highest priority)
|
57
|
+
if user_profile:
|
58
|
+
return user_profile
|
59
|
+
|
60
|
+
# Tier 2: Environment variable
|
61
|
+
env_profile = os.getenv('AWS_PROFILE')
|
62
|
+
if env_profile:
|
63
|
+
return env_profile
|
64
|
+
|
65
|
+
# Tier 3: Default (no explicit profile)
|
66
|
+
return None
|
67
|
+
|
68
|
+
def get_session(self, region: str = 'us-east-1') -> boto3.Session:
|
69
|
+
"""
|
70
|
+
Get boto3 session with resolved profile.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
region: AWS region (default: us-east-1)
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
Configured boto3 session
|
77
|
+
|
78
|
+
Raises:
|
79
|
+
ProfileNotFound: If specified profile doesn't exist
|
80
|
+
NoCredentialsError: If no valid credentials found
|
81
|
+
"""
|
82
|
+
if not self.session:
|
83
|
+
try:
|
84
|
+
if self.profile:
|
85
|
+
self.session = boto3.Session(profile_name=self.profile, region_name=region)
|
86
|
+
print_info(f"Using AWS profile: {self.profile}")
|
87
|
+
else:
|
88
|
+
self.session = boto3.Session(region_name=region)
|
89
|
+
print_info("Using default AWS credentials")
|
90
|
+
|
91
|
+
# Validate credentials by getting caller identity
|
92
|
+
sts = self.session.client('sts')
|
93
|
+
identity = sts.get_caller_identity()
|
94
|
+
print_success(f"✅ Authenticated as: {identity.get('Arn', 'Unknown')}")
|
95
|
+
|
96
|
+
except ProfileNotFound as e:
|
97
|
+
print_error(f"❌ AWS profile '{self.profile}' not found")
|
98
|
+
print_info("Available profiles:")
|
99
|
+
self._list_available_profiles()
|
100
|
+
raise e
|
101
|
+
except NoCredentialsError as e:
|
102
|
+
print_error("❌ No AWS credentials found")
|
103
|
+
print_info("Setup options:")
|
104
|
+
print_info("1. Configure AWS CLI: aws configure")
|
105
|
+
print_info("2. Set AWS_PROFILE environment variable")
|
106
|
+
print_info("3. Use --profile flag with valid profile name")
|
107
|
+
raise e
|
108
|
+
|
109
|
+
return self.session
|
110
|
+
|
111
|
+
def get_account_id(self, region: str = 'us-east-1') -> str:
|
112
|
+
"""
|
113
|
+
Get current AWS account ID dynamically.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
region: AWS region
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
Current AWS account ID
|
120
|
+
"""
|
121
|
+
cache_key = f"{self.profile or 'default'}:{region}"
|
122
|
+
|
123
|
+
if cache_key not in self._account_cache:
|
124
|
+
try:
|
125
|
+
session = self.get_session(region)
|
126
|
+
sts = session.client('sts')
|
127
|
+
identity = sts.get_caller_identity()
|
128
|
+
account_id = identity['Account']
|
129
|
+
self._account_cache[cache_key] = account_id
|
130
|
+
print_info(f"Current account ID: {account_id}")
|
131
|
+
|
132
|
+
except ClientError as e:
|
133
|
+
print_error(f"❌ Failed to get account ID: {e}")
|
134
|
+
# Return generic account ID for testing/mock scenarios
|
135
|
+
return "123456789012"
|
136
|
+
|
137
|
+
return self._account_cache[cache_key]
|
138
|
+
|
139
|
+
def discover_organization_accounts(self, region: str = 'us-east-1') -> List[Dict[str, Any]]:
|
140
|
+
"""
|
141
|
+
Discover all accounts in AWS Organizations (if available).
|
142
|
+
|
143
|
+
Args:
|
144
|
+
region: AWS region
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
List of organization accounts with metadata
|
148
|
+
"""
|
149
|
+
try:
|
150
|
+
session = self.get_session(region)
|
151
|
+
org_client = session.client('organizations')
|
152
|
+
|
153
|
+
# Get organization information
|
154
|
+
try:
|
155
|
+
org_info = org_client.describe_organization()
|
156
|
+
print_success(f"✅ Organization: {org_info['Organization']['Id']}")
|
157
|
+
except ClientError:
|
158
|
+
print_warning("⚠️ Not connected to AWS Organizations")
|
159
|
+
return []
|
160
|
+
|
161
|
+
# List all accounts
|
162
|
+
accounts = []
|
163
|
+
paginator = org_client.get_paginator('list_accounts')
|
164
|
+
|
165
|
+
for page in paginator.paginate():
|
166
|
+
for account in page['Accounts']:
|
167
|
+
accounts.append({
|
168
|
+
'Id': account['Id'],
|
169
|
+
'Name': account['Name'],
|
170
|
+
'Email': account['Email'],
|
171
|
+
'Status': account['Status'],
|
172
|
+
'JoinedMethod': account.get('JoinedMethod', 'UNKNOWN')
|
173
|
+
})
|
174
|
+
|
175
|
+
print_success(f"✅ Discovered {len(accounts)} organization accounts")
|
176
|
+
return accounts
|
177
|
+
|
178
|
+
except ClientError as e:
|
179
|
+
print_warning(f"⚠️ Unable to discover organization accounts: {e}")
|
180
|
+
return []
|
181
|
+
|
182
|
+
def validate_profile_access(self, required_services: List[str] = None) -> Dict[str, bool]:
|
183
|
+
"""
|
184
|
+
Validate profile has access to required AWS services.
|
185
|
+
|
186
|
+
Args:
|
187
|
+
required_services: List of AWS service names to validate
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
Dict mapping service names to access status
|
191
|
+
"""
|
192
|
+
if not required_services:
|
193
|
+
required_services = ['sts', 'ce', 'ec2', 's3']
|
194
|
+
|
195
|
+
access_status = {}
|
196
|
+
session = self.get_session()
|
197
|
+
|
198
|
+
for service in required_services:
|
199
|
+
try:
|
200
|
+
client = session.client(service)
|
201
|
+
|
202
|
+
# Service-specific health checks
|
203
|
+
if service == 'sts':
|
204
|
+
client.get_caller_identity()
|
205
|
+
elif service == 'ce':
|
206
|
+
# Test Cost Explorer access
|
207
|
+
from datetime import datetime, timedelta
|
208
|
+
end_date = datetime.now().strftime('%Y-%m-%d')
|
209
|
+
start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
|
210
|
+
client.get_cost_and_usage(
|
211
|
+
TimePeriod={'Start': start_date, 'End': end_date},
|
212
|
+
Granularity='MONTHLY',
|
213
|
+
Metrics=['UnblendedCost']
|
214
|
+
)
|
215
|
+
elif service == 'ec2':
|
216
|
+
client.describe_regions(MaxResults=1)
|
217
|
+
elif service == 's3':
|
218
|
+
client.list_buckets()
|
219
|
+
|
220
|
+
access_status[service] = True
|
221
|
+
print_success(f"✅ {service.upper()} access validated")
|
222
|
+
|
223
|
+
except ClientError as e:
|
224
|
+
access_status[service] = False
|
225
|
+
print_warning(f"⚠️ {service.upper()} access limited: {e}")
|
226
|
+
|
227
|
+
return access_status
|
228
|
+
|
229
|
+
def _list_available_profiles(self) -> None:
|
230
|
+
"""List available AWS profiles from credentials file."""
|
231
|
+
try:
|
232
|
+
import configparser
|
233
|
+
import os.path
|
234
|
+
|
235
|
+
credentials_path = os.path.expanduser('~/.aws/credentials')
|
236
|
+
config_path = os.path.expanduser('~/.aws/config')
|
237
|
+
|
238
|
+
profiles = set()
|
239
|
+
|
240
|
+
# Check credentials file
|
241
|
+
if os.path.exists(credentials_path):
|
242
|
+
cred_config = configparser.ConfigParser()
|
243
|
+
cred_config.read(credentials_path)
|
244
|
+
profiles.update(cred_config.sections())
|
245
|
+
|
246
|
+
# Check config file (profiles prefixed with 'profile ')
|
247
|
+
if os.path.exists(config_path):
|
248
|
+
config_config = configparser.ConfigParser()
|
249
|
+
config_config.read(config_path)
|
250
|
+
for section in config_config.sections():
|
251
|
+
if section.startswith('profile '):
|
252
|
+
profiles.add(section[8:]) # Remove 'profile ' prefix
|
253
|
+
elif section == 'default':
|
254
|
+
profiles.add('default')
|
255
|
+
|
256
|
+
if profiles:
|
257
|
+
for profile in sorted(profiles):
|
258
|
+
print_info(f" - {profile}")
|
259
|
+
else:
|
260
|
+
print_info(" No profiles found in ~/.aws/credentials or ~/.aws/config")
|
261
|
+
|
262
|
+
except Exception as e:
|
263
|
+
print_warning(f"⚠️ Could not list profiles: {e}")
|
264
|
+
|
265
|
+
@classmethod
|
266
|
+
def create_mock_account_context(cls, mock_account_id: str = "123456789012") -> 'AWSProfileManager':
|
267
|
+
"""
|
268
|
+
Create mock profile manager for testing scenarios.
|
269
|
+
|
270
|
+
Args:
|
271
|
+
mock_account_id: Mock account ID to use
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
ProfileManager configured for testing
|
275
|
+
"""
|
276
|
+
manager = cls()
|
277
|
+
manager._account_cache['mock'] = mock_account_id
|
278
|
+
return manager
|
279
|
+
|
280
|
+
def get_profile_display_name(self) -> str:
|
281
|
+
"""
|
282
|
+
Get human-friendly profile display name.
|
283
|
+
|
284
|
+
Returns:
|
285
|
+
Profile display name for CLI output
|
286
|
+
"""
|
287
|
+
if self.profile:
|
288
|
+
return f"Profile: {self.profile}"
|
289
|
+
else:
|
290
|
+
return "Profile: default"
|
291
|
+
|
292
|
+
def __repr__(self) -> str:
|
293
|
+
return f"AWSProfileManager(profile={self.profile})"
|
294
|
+
|
295
|
+
|
296
|
+
# Global convenience functions for backward compatibility
|
297
|
+
def get_current_account_id(profile: Optional[str] = None, region: str = 'us-east-1') -> str:
|
298
|
+
"""
|
299
|
+
Convenience function to get current account ID.
|
300
|
+
|
301
|
+
Args:
|
302
|
+
profile: AWS profile name
|
303
|
+
region: AWS region
|
304
|
+
|
305
|
+
Returns:
|
306
|
+
Current AWS account ID
|
307
|
+
"""
|
308
|
+
manager = AWSProfileManager(profile)
|
309
|
+
return manager.get_account_id(region)
|
310
|
+
|
311
|
+
|
312
|
+
def validate_profile_or_exit(profile: Optional[str] = None, required_services: List[str] = None) -> AWSProfileManager:
|
313
|
+
"""
|
314
|
+
Validate profile exists and has required access, exit gracefully if not.
|
315
|
+
|
316
|
+
Args:
|
317
|
+
profile: AWS profile name
|
318
|
+
required_services: Required AWS services for validation
|
319
|
+
|
320
|
+
Returns:
|
321
|
+
Validated AWSProfileManager instance
|
322
|
+
"""
|
323
|
+
try:
|
324
|
+
manager = AWSProfileManager(profile)
|
325
|
+
access_status = manager.validate_profile_access(required_services)
|
326
|
+
|
327
|
+
failed_services = [svc for svc, status in access_status.items() if not status]
|
328
|
+
if failed_services:
|
329
|
+
print_warning(f"⚠️ Limited access to services: {', '.join(failed_services)}")
|
330
|
+
print_info("Continuing with available services...")
|
331
|
+
|
332
|
+
return manager
|
333
|
+
|
334
|
+
except (ProfileNotFound, NoCredentialsError):
|
335
|
+
print_error("❌ Cannot proceed without valid AWS credentials")
|
336
|
+
print_info("Please configure AWS credentials and try again.")
|
337
|
+
exit(1)
|
runbooks/common/aws_utils.py
CHANGED
@@ -22,8 +22,8 @@ from .profile_utils import get_profile_for_operation
|
|
22
22
|
class BusinessImpactLevel(Enum):
|
23
23
|
"""Business impact classification for operations and optimizations."""
|
24
24
|
CRITICAL = "CRITICAL" # >$100K annual impact
|
25
|
-
HIGH = "HIGH" #
|
26
|
-
MEDIUM = "MEDIUM" #
|
25
|
+
HIGH = "HIGH" # measurable range annual impact
|
26
|
+
MEDIUM = "MEDIUM" # measurable range annual impact
|
27
27
|
LOW = "LOW" # <$5K annual impact
|
28
28
|
|
29
29
|
|
@@ -189,7 +189,7 @@ class UniversalBusinessLogic:
|
|
189
189
|
# Apply proven profile management patterns
|
190
190
|
selected_profile = get_profile_for_operation("operational", profile)
|
191
191
|
|
192
|
-
print_header(f"{resource_type.title()} {operation.title()}", f"
|
192
|
+
print_header(f"{resource_type.title()} {operation.title()}", f"latest version - {self.module_name}")
|
193
193
|
print_info(f"Using profile: {selected_profile}")
|
194
194
|
|
195
195
|
# Standard operation tracking
|
@@ -749,7 +749,7 @@ class ComprehensiveCostExplorerIntegration:
|
|
749
749
|
|
750
750
|
---
|
751
751
|
|
752
|
-
*Generated by Comprehensive Cost Explorer Integration
|
752
|
+
*Generated by Comprehensive Cost Explorer Integration latest version*
|
753
753
|
*Strategic Coordination: Enterprise Agile Team with systematic delegation*
|
754
754
|
"""
|
755
755
|
|
@@ -0,0 +1,225 @@
|
|
1
|
+
"""
|
2
|
+
Common CLI Decorators for Modular Commands
|
3
|
+
|
4
|
+
KISS Principle: Simple, reusable decorators for consistent CLI patterns
|
5
|
+
DRY Principle: No duplicated decorator logic across command modules
|
6
|
+
|
7
|
+
This module provides consistent decorators used across all modular command
|
8
|
+
files, enabling the DRY principle while maintaining enterprise standards.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import functools
|
12
|
+
import time
|
13
|
+
from typing import Any, Callable
|
14
|
+
|
15
|
+
import click
|
16
|
+
from rich.console import Console
|
17
|
+
|
18
|
+
console = Console()
|
19
|
+
|
20
|
+
|
21
|
+
def common_aws_options(f):
|
22
|
+
"""
|
23
|
+
Common AWS options for all commands.
|
24
|
+
|
25
|
+
Provides consistent AWS configuration options across all command modules:
|
26
|
+
- --profile: AWS profile selection
|
27
|
+
- --region: AWS region targeting
|
28
|
+
- --dry-run: Safety mode for testing
|
29
|
+
"""
|
30
|
+
f = click.option("--profile", default="default", help="AWS profile to use")(f)
|
31
|
+
f = click.option("--region", help="AWS region (overrides profile default)")(f)
|
32
|
+
f = click.option("--dry-run", is_flag=True, help="Perform a dry run without making changes")(f)
|
33
|
+
return f
|
34
|
+
|
35
|
+
|
36
|
+
def common_output_options(f):
|
37
|
+
"""
|
38
|
+
Common output options for commands that generate reports.
|
39
|
+
|
40
|
+
Provides consistent output formatting options:
|
41
|
+
- --format: Output format selection (table, csv, json, markdown, pdf)
|
42
|
+
- --output-file: File output destination
|
43
|
+
"""
|
44
|
+
f = click.option("--format", "output_format", type=click.Choice(['table', 'csv', 'json', 'markdown', 'pdf']),
|
45
|
+
default='table', help="Output format")(f)
|
46
|
+
f = click.option("--output-file", type=click.Path(), help="Output file path")(f)
|
47
|
+
return f
|
48
|
+
|
49
|
+
|
50
|
+
def common_filter_options(f):
|
51
|
+
"""
|
52
|
+
Common filtering options for resource discovery commands.
|
53
|
+
|
54
|
+
Provides consistent filtering capabilities:
|
55
|
+
- --tags: Resource tag filtering
|
56
|
+
- --accounts: Account ID filtering
|
57
|
+
- --regions: Region filtering
|
58
|
+
"""
|
59
|
+
f = click.option("--tags", multiple=True, help="Filter by tags (key=value format)")(f)
|
60
|
+
f = click.option("--accounts", multiple=True, help="Filter by account IDs")(f)
|
61
|
+
f = click.option("--regions", multiple=True, help="Filter by regions")(f)
|
62
|
+
return f
|
63
|
+
|
64
|
+
|
65
|
+
def performance_timing(f):
|
66
|
+
"""
|
67
|
+
Performance timing decorator for measuring command execution time.
|
68
|
+
|
69
|
+
Automatically tracks and reports command execution time for performance
|
70
|
+
monitoring and optimization analysis.
|
71
|
+
"""
|
72
|
+
@functools.wraps(f)
|
73
|
+
def wrapper(*args, **kwargs):
|
74
|
+
start_time = time.time()
|
75
|
+
try:
|
76
|
+
result = f(*args, **kwargs)
|
77
|
+
execution_time = time.time() - start_time
|
78
|
+
|
79
|
+
# Only show timing in debug mode or for slow operations
|
80
|
+
if execution_time > 1.0: # Show for operations > 1 second
|
81
|
+
console.print(f"[dim]⏱️ Completed in {execution_time:.2f}s[/dim]")
|
82
|
+
|
83
|
+
return result
|
84
|
+
except Exception as e:
|
85
|
+
execution_time = time.time() - start_time
|
86
|
+
console.print(f"[red]❌ Failed after {execution_time:.2f}s: {e}[/red]")
|
87
|
+
raise
|
88
|
+
|
89
|
+
return wrapper
|
90
|
+
|
91
|
+
|
92
|
+
def error_handler(f):
|
93
|
+
"""
|
94
|
+
Common error handling decorator for consistent error reporting.
|
95
|
+
|
96
|
+
Provides enterprise-grade error handling with:
|
97
|
+
- Rich formatting for better UX
|
98
|
+
- Consistent error message structure
|
99
|
+
- Debug information when enabled
|
100
|
+
"""
|
101
|
+
@functools.wraps(f)
|
102
|
+
def wrapper(*args, **kwargs):
|
103
|
+
try:
|
104
|
+
return f(*args, **kwargs)
|
105
|
+
except click.ClickException:
|
106
|
+
# Re-raise Click exceptions as-is
|
107
|
+
raise
|
108
|
+
except ImportError as e:
|
109
|
+
console.print(f"[red]❌ Module not available: {e}[/red]")
|
110
|
+
console.print(f"[yellow]💡 This functionality may require additional dependencies[/yellow]")
|
111
|
+
raise click.ClickException("Required module not available")
|
112
|
+
except Exception as e:
|
113
|
+
console.print(f"[red]❌ Unexpected error: {e}[/red]")
|
114
|
+
console.print(f"[yellow]💡 Run with --debug for detailed error information[/yellow]")
|
115
|
+
raise click.ClickException(str(e))
|
116
|
+
|
117
|
+
return wrapper
|
118
|
+
|
119
|
+
|
120
|
+
def require_aws_profile(f):
|
121
|
+
"""
|
122
|
+
Decorator to ensure AWS profile is properly configured.
|
123
|
+
|
124
|
+
Validates that the AWS profile exists and is accessible before
|
125
|
+
executing commands that require AWS API access.
|
126
|
+
"""
|
127
|
+
@functools.wraps(f)
|
128
|
+
def wrapper(*args, **kwargs):
|
129
|
+
# Get profile from context or kwargs
|
130
|
+
ctx = click.get_current_context()
|
131
|
+
profile = ctx.obj.get('profile', 'default')
|
132
|
+
|
133
|
+
try:
|
134
|
+
import boto3
|
135
|
+
# Test profile access
|
136
|
+
session = boto3.Session(profile_name=profile)
|
137
|
+
session.get_credentials()
|
138
|
+
|
139
|
+
return f(*args, **kwargs)
|
140
|
+
except Exception as e:
|
141
|
+
console.print(f"[red]❌ AWS profile '{profile}' not accessible: {e}[/red]")
|
142
|
+
console.print(f"[yellow]💡 Run 'aws configure list-profiles' to see available profiles[/yellow]")
|
143
|
+
raise click.ClickException(f"AWS profile '{profile}' not accessible")
|
144
|
+
|
145
|
+
return wrapper
|
146
|
+
|
147
|
+
|
148
|
+
def enterprise_audit_trail(f):
|
149
|
+
"""
|
150
|
+
Enterprise audit trail decorator for compliance and governance.
|
151
|
+
|
152
|
+
Automatically logs command execution for audit purposes with:
|
153
|
+
- Command name and parameters
|
154
|
+
- User context and timestamp
|
155
|
+
- Execution results and duration
|
156
|
+
"""
|
157
|
+
@functools.wraps(f)
|
158
|
+
def wrapper(*args, **kwargs):
|
159
|
+
ctx = click.get_current_context()
|
160
|
+
|
161
|
+
# Log command execution start
|
162
|
+
audit_data = {
|
163
|
+
'command': ctx.command.name,
|
164
|
+
'profile': ctx.obj.get('profile', 'default'),
|
165
|
+
'region': ctx.obj.get('region', 'default'),
|
166
|
+
'dry_run': ctx.obj.get('dry_run', False),
|
167
|
+
'timestamp': time.time()
|
168
|
+
}
|
169
|
+
|
170
|
+
try:
|
171
|
+
result = f(*args, **kwargs)
|
172
|
+
audit_data['status'] = 'success'
|
173
|
+
audit_data['duration'] = time.time() - audit_data['timestamp']
|
174
|
+
|
175
|
+
# Log successful execution
|
176
|
+
if ctx.obj.get('debug'):
|
177
|
+
console.print(f"[dim]📋 Audit: {audit_data}[/dim]")
|
178
|
+
|
179
|
+
return result
|
180
|
+
except Exception as e:
|
181
|
+
audit_data['status'] = 'error'
|
182
|
+
audit_data['error'] = str(e)
|
183
|
+
audit_data['duration'] = time.time() - audit_data['timestamp']
|
184
|
+
|
185
|
+
# Log failed execution
|
186
|
+
if ctx.obj.get('debug'):
|
187
|
+
console.print(f"[dim]📋 Audit: {audit_data}[/dim]")
|
188
|
+
|
189
|
+
raise
|
190
|
+
|
191
|
+
return wrapper
|
192
|
+
|
193
|
+
|
194
|
+
def rich_progress(description: str = "Processing"):
|
195
|
+
"""
|
196
|
+
Rich progress indicator decorator for long-running operations.
|
197
|
+
|
198
|
+
Args:
|
199
|
+
description: Description text for the progress indicator
|
200
|
+
|
201
|
+
Automatically shows a progress spinner for operations that take time,
|
202
|
+
improving user experience for long-running commands.
|
203
|
+
"""
|
204
|
+
def decorator(f):
|
205
|
+
@functools.wraps(f)
|
206
|
+
def wrapper(*args, **kwargs):
|
207
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
208
|
+
|
209
|
+
with Progress(
|
210
|
+
SpinnerColumn(),
|
211
|
+
TextColumn("[progress.description]{task.description}"),
|
212
|
+
console=console
|
213
|
+
) as progress:
|
214
|
+
task = progress.add_task(description, total=None)
|
215
|
+
|
216
|
+
try:
|
217
|
+
result = f(*args, **kwargs)
|
218
|
+
progress.update(task, description=f"✅ {description} completed")
|
219
|
+
return result
|
220
|
+
except Exception as e:
|
221
|
+
progress.update(task, description=f"❌ {description} failed")
|
222
|
+
raise
|
223
|
+
|
224
|
+
return wrapper
|
225
|
+
return decorator
|
@@ -703,7 +703,7 @@ class MCPCostExplorerIntegration:
|
|
703
703
|
# Generate recommendations
|
704
704
|
if priorities_assessment['priorities']['workspaces_cleanup']['status'] == 'needs_expansion':
|
705
705
|
priorities_assessment['recommendations'].append(
|
706
|
-
"Expand WorkSpaces analysis scope to achieve
|
706
|
+
"Expand WorkSpaces analysis scope to achieve significant annual savings target"
|
707
707
|
)
|
708
708
|
|
709
709
|
if priorities_assessment['priorities']['nat_gateway_optimization']['status'] == 'limited_opportunities':
|
@@ -713,7 +713,7 @@ class MCPCostExplorerIntegration:
|
|
713
713
|
|
714
714
|
if priorities_assessment['priorities']['rds_optimization']['status'] == 'outside_range':
|
715
715
|
priorities_assessment['recommendations'].append(
|
716
|
-
"RDS optimization potential outside
|
716
|
+
"RDS optimization potential outside measurable range range - review Multi-AZ configurations"
|
717
717
|
)
|
718
718
|
|
719
719
|
return priorities_assessment
|