runbooks 1.0.0__py3-none-any.whl → 1.0.2__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/cfat/WEIGHT_CONFIG_README.md +368 -0
- runbooks/cfat/app.ts +27 -19
- runbooks/cfat/assessment/runner.py +6 -5
- runbooks/cfat/tests/test_weight_configuration.ts +449 -0
- runbooks/cfat/weight_config.ts +574 -0
- runbooks/cloudops/models.py +20 -14
- runbooks/common/__init__.py +26 -9
- runbooks/common/aws_pricing.py +1070 -105
- runbooks/common/aws_pricing_api.py +276 -44
- runbooks/common/date_utils.py +115 -0
- runbooks/common/dry_run_examples.py +587 -0
- runbooks/common/dry_run_framework.py +520 -0
- runbooks/common/enhanced_exception_handler.py +10 -7
- runbooks/common/mcp_cost_explorer_integration.py +5 -4
- runbooks/common/memory_optimization.py +533 -0
- runbooks/common/performance_optimization_engine.py +1153 -0
- runbooks/common/profile_utils.py +86 -118
- runbooks/common/rich_utils.py +3 -3
- runbooks/common/sre_performance_suite.py +574 -0
- runbooks/finops/business_case_config.py +314 -0
- runbooks/finops/cost_processor.py +19 -4
- runbooks/finops/dashboard_runner.py +47 -28
- runbooks/finops/ebs_cost_optimizer.py +1 -1
- runbooks/finops/ebs_optimizer.py +56 -9
- runbooks/finops/embedded_mcp_validator.py +642 -36
- runbooks/finops/enhanced_trend_visualization.py +7 -2
- runbooks/finops/executive_export.py +789 -0
- runbooks/finops/finops_dashboard.py +6 -5
- runbooks/finops/finops_scenarios.py +34 -27
- runbooks/finops/iam_guidance.py +6 -1
- runbooks/finops/nat_gateway_optimizer.py +46 -27
- runbooks/finops/notebook_utils.py +1 -1
- runbooks/finops/schemas.py +73 -58
- runbooks/finops/single_dashboard.py +20 -4
- runbooks/finops/tests/test_integration.py +3 -1
- runbooks/finops/vpc_cleanup_exporter.py +2 -1
- runbooks/finops/vpc_cleanup_optimizer.py +22 -29
- runbooks/inventory/core/collector.py +51 -28
- runbooks/inventory/discovery.md +197 -247
- runbooks/inventory/inventory_modules.py +2 -2
- runbooks/inventory/list_ec2_instances.py +3 -3
- runbooks/inventory/models/account.py +5 -3
- runbooks/inventory/models/inventory.py +1 -1
- runbooks/inventory/models/resource.py +5 -3
- runbooks/inventory/organizations_discovery.py +102 -13
- runbooks/inventory/unified_validation_engine.py +2 -15
- runbooks/main.py +255 -92
- runbooks/operate/base.py +9 -6
- runbooks/operate/deployment_framework.py +5 -4
- runbooks/operate/deployment_validator.py +6 -5
- runbooks/operate/mcp_integration.py +6 -5
- runbooks/operate/networking_cost_heatmap.py +17 -13
- runbooks/operate/vpc_operations.py +82 -13
- runbooks/remediation/base.py +3 -1
- runbooks/remediation/commons.py +5 -5
- runbooks/remediation/commvault_ec2_analysis.py +66 -18
- runbooks/remediation/config/accounts_example.json +31 -0
- runbooks/remediation/multi_account.py +120 -7
- runbooks/remediation/remediation_cli.py +710 -0
- runbooks/remediation/universal_account_discovery.py +377 -0
- runbooks/remediation/workspaces_list.py +2 -2
- runbooks/security/compliance_automation_engine.py +99 -20
- runbooks/security/config/__init__.py +24 -0
- runbooks/security/config/compliance_config.py +255 -0
- runbooks/security/config/compliance_weights_example.json +22 -0
- runbooks/security/config_template_generator.py +500 -0
- runbooks/security/security_cli.py +377 -0
- runbooks/validation/cli.py +8 -7
- runbooks/validation/comprehensive_2way_validator.py +26 -15
- runbooks/validation/mcp_validator.py +62 -8
- runbooks/vpc/config.py +49 -15
- runbooks/vpc/cross_account_session.py +5 -1
- runbooks/vpc/heatmap_engine.py +438 -59
- runbooks/vpc/mcp_no_eni_validator.py +115 -36
- runbooks/vpc/performance_optimized_analyzer.py +546 -0
- runbooks/vpc/runbooks_adapter.py +33 -12
- runbooks/vpc/tests/conftest.py +4 -2
- runbooks/vpc/tests/test_cost_engine.py +3 -1
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/METADATA +1 -1
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/RECORD +85 -79
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/tests/results_test_finops_dashboard.xml +0 -1
- runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
- runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/runbooks.security.security_export.log +0 -0
- runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/vpc/runbooks.security.report_generator.log +0 -0
- runbooks/vpc/runbooks.security.run_script.log +0 -0
- runbooks/vpc/runbooks.security.security_export.log +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/WHEEL +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/entry_points.txt +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/top_level.txt +0 -0
runbooks/common/profile_utils.py
CHANGED
@@ -1,59 +1,51 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
Profile Management
|
3
|
+
Universal AWS Profile Management for CloudOps Runbooks Platform
|
4
4
|
|
5
|
-
This module provides
|
6
|
-
|
5
|
+
This module provides truly universal AWS profile management that works with ANY AWS setup:
|
6
|
+
- Single account setups
|
7
|
+
- Multi-account setups
|
8
|
+
- Any profile naming convention
|
9
|
+
- No specific environment variable requirements
|
7
10
|
|
8
11
|
Features:
|
9
|
-
-
|
10
|
-
-
|
11
|
-
-
|
12
|
-
-
|
13
|
-
- Profile validation and error handling
|
12
|
+
- Universal compatibility: User --profile → AWS_PROFILE → "default"
|
13
|
+
- Works with ANY AWS profile names (not just specific test profiles)
|
14
|
+
- No hardcoded environment variable assumptions
|
15
|
+
- Simple, reliable profile selection for all users
|
14
16
|
|
15
17
|
Author: CloudOps Runbooks Team
|
16
|
-
Version: 0.
|
18
|
+
Version: 1.0.0 - Universal Compatibility
|
17
19
|
"""
|
18
20
|
|
19
21
|
import os
|
20
22
|
import time
|
21
|
-
from typing import Dict, Optional
|
23
|
+
from typing import Dict, Optional, Union, List, Tuple
|
22
24
|
|
23
25
|
import boto3
|
24
26
|
|
25
27
|
from runbooks.common.rich_utils import console
|
26
28
|
|
27
|
-
# Profile cache to reduce duplicate calls (
|
29
|
+
# Profile cache to reduce duplicate calls (performance optimization)
|
28
30
|
_profile_cache = {}
|
29
31
|
_cache_timestamp = None
|
30
32
|
_cache_ttl = 300 # 5 minutes cache TTL
|
31
33
|
|
32
|
-
# Enterprise AWS profile mappings with fallback defaults
|
33
|
-
ENV_PROFILE_MAP = {
|
34
|
-
"billing": os.getenv("BILLING_PROFILE"),
|
35
|
-
"management": os.getenv("MANAGEMENT_PROFILE"),
|
36
|
-
"operational": os.getenv("CENTRALISED_OPS_PROFILE"),
|
37
|
-
}
|
38
34
|
|
39
|
-
|
40
|
-
DEFAULT_PROFILE = os.getenv("AWS_PROFILE") or "default" # "default" is AWS boto3 expected fallback
|
41
|
-
|
42
|
-
|
43
|
-
def get_profile_for_operation(operation_type: str, user_specified_profile: Optional[str] = None) -> str:
|
35
|
+
def get_profile_for_operation(operation_type: str, user_specified_profile: Optional[Union[str, Tuple[str, ...], List[str]]] = None) -> str:
|
44
36
|
"""
|
45
|
-
|
37
|
+
Universal AWS profile selection that works with ANY AWS setup.
|
46
38
|
|
47
|
-
PRIORITY ORDER (
|
39
|
+
SIMPLE PRIORITY ORDER (Universal Compatibility):
|
48
40
|
1. User-specified profile (--profile parameter) - HIGHEST PRIORITY
|
49
|
-
2.
|
50
|
-
3.
|
41
|
+
2. AWS_PROFILE environment variable - STANDARD AWS CONVENTION
|
42
|
+
3. "default" profile - AWS STANDARD FALLBACK
|
51
43
|
|
52
|
-
|
44
|
+
Works with ANY profile names and ANY AWS setup - no specific environment variable requirements.
|
53
45
|
|
54
46
|
Args:
|
55
|
-
operation_type: Type of operation (
|
56
|
-
user_specified_profile: Profile specified by user via --profile parameter
|
47
|
+
operation_type: Type of operation (informational only, not used for profile selection)
|
48
|
+
user_specified_profile: Profile specified by user via --profile parameter (handles both str and tuple)
|
57
49
|
|
58
50
|
Returns:
|
59
51
|
str: Profile name to use for the operation
|
@@ -61,9 +53,16 @@ def get_profile_for_operation(operation_type: str, user_specified_profile: Optio
|
|
61
53
|
Raises:
|
62
54
|
SystemExit: If user-specified profile not found in AWS config
|
63
55
|
"""
|
56
|
+
# SAFETY NET: Handle tuple profiles (Click multiple=True parameter issue)
|
57
|
+
# This prevents errors like: Profile '('profile-name',)' not found
|
58
|
+
if isinstance(user_specified_profile, (tuple, list)) and user_specified_profile:
|
59
|
+
user_specified_profile = user_specified_profile[0] # Take first profile from tuple/list
|
60
|
+
elif isinstance(user_specified_profile, (tuple, list)) and not user_specified_profile:
|
61
|
+
user_specified_profile = None # Empty tuple/list becomes None
|
62
|
+
|
64
63
|
global _profile_cache, _cache_timestamp
|
65
64
|
|
66
|
-
# Check cache first to reduce duplicate calls (
|
65
|
+
# Check cache first to reduce duplicate calls (performance optimization)
|
67
66
|
cache_key = f"{operation_type}:{user_specified_profile or 'None'}"
|
68
67
|
current_time = time.time()
|
69
68
|
|
@@ -82,33 +81,26 @@ def get_profile_for_operation(operation_type: str, user_specified_profile: Optio
|
|
82
81
|
# PRIORITY 1: User-specified profile ALWAYS takes precedence
|
83
82
|
if user_specified_profile and user_specified_profile != "default":
|
84
83
|
if user_specified_profile in available_profiles:
|
85
|
-
console.log(f"[green]Using user-specified profile
|
84
|
+
console.log(f"[green]Using user-specified profile: {user_specified_profile}[/]")
|
86
85
|
# Cache the result to reduce duplicate calls
|
87
86
|
_profile_cache[cache_key] = user_specified_profile
|
88
87
|
return user_specified_profile
|
89
88
|
else:
|
90
|
-
console.log(f"[red]Error:
|
91
|
-
|
89
|
+
console.log(f"[red]Error: Profile '{user_specified_profile}' not found in AWS config[/]")
|
90
|
+
console.log(f"[yellow]Available profiles: {', '.join(available_profiles)}[/]")
|
92
91
|
raise SystemExit(1)
|
93
92
|
|
94
|
-
# PRIORITY 2:
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
"operational": os.getenv("AWS_CENTRALISED_OPS_PROFILE") or os.getenv("CENTRALISED_OPS_PROFILE"),
|
99
|
-
"single_account": os.getenv("AWS_SINGLE_ACCOUNT_PROFILE") or os.getenv("SINGLE_AWS_PROFILE"),
|
100
|
-
}
|
101
|
-
|
102
|
-
env_profile = profile_map.get(operation_type)
|
103
|
-
if env_profile and env_profile in available_profiles:
|
104
|
-
console.log(f"[dim cyan]Using {operation_type} profile from environment: {env_profile}[/]")
|
93
|
+
# PRIORITY 2: AWS_PROFILE environment variable (standard AWS convention)
|
94
|
+
aws_profile = os.getenv("AWS_PROFILE")
|
95
|
+
if aws_profile and aws_profile in available_profiles:
|
96
|
+
console.log(f"[dim cyan]Using AWS_PROFILE environment variable: {aws_profile}[/]")
|
105
97
|
# Cache the result to reduce duplicate calls
|
106
|
-
_profile_cache[cache_key] =
|
107
|
-
return
|
98
|
+
_profile_cache[cache_key] = aws_profile
|
99
|
+
return aws_profile
|
108
100
|
|
109
|
-
# PRIORITY 3: Default profile (
|
110
|
-
default_profile =
|
111
|
-
console.log(f"[yellow]
|
101
|
+
# PRIORITY 3: Default profile (AWS standard fallback)
|
102
|
+
default_profile = "default"
|
103
|
+
console.log(f"[yellow]Using default AWS profile: {default_profile}[/]")
|
112
104
|
# Cache the result to reduce duplicate calls
|
113
105
|
_profile_cache[cache_key] = default_profile
|
114
106
|
return default_profile
|
@@ -116,11 +108,11 @@ def get_profile_for_operation(operation_type: str, user_specified_profile: Optio
|
|
116
108
|
|
117
109
|
def resolve_profile_for_operation_silent(operation_type: str, user_specified_profile: Optional[str] = None) -> str:
|
118
110
|
"""
|
119
|
-
|
120
|
-
Uses the same logic as get_profile_for_operation but without console output.
|
111
|
+
Universal AWS profile resolution without logging (for display purposes).
|
112
|
+
Uses the same universal logic as get_profile_for_operation but without console output.
|
121
113
|
|
122
114
|
Args:
|
123
|
-
operation_type: Type of operation (
|
115
|
+
operation_type: Type of operation (informational only, not used for profile selection)
|
124
116
|
user_specified_profile: Profile specified by user via --profile parameter
|
125
117
|
|
126
118
|
Returns:
|
@@ -139,78 +131,72 @@ def resolve_profile_for_operation_silent(operation_type: str, user_specified_pro
|
|
139
131
|
# Don't fall back - user explicitly chose this profile
|
140
132
|
raise SystemExit(1)
|
141
133
|
|
142
|
-
# PRIORITY 2:
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
"operational": os.getenv("AWS_CENTRALISED_OPS_PROFILE") or os.getenv("CENTRALISED_OPS_PROFILE"),
|
147
|
-
"single_account": os.getenv("AWS_SINGLE_ACCOUNT_PROFILE") or os.getenv("SINGLE_AWS_PROFILE"),
|
148
|
-
}
|
149
|
-
|
150
|
-
env_profile = profile_map.get(operation_type)
|
151
|
-
if env_profile and env_profile in available_profiles:
|
152
|
-
return env_profile
|
134
|
+
# PRIORITY 2: AWS_PROFILE environment variable (standard AWS convention)
|
135
|
+
aws_profile = os.getenv("AWS_PROFILE")
|
136
|
+
if aws_profile and aws_profile in available_profiles:
|
137
|
+
return aws_profile
|
153
138
|
|
154
|
-
# PRIORITY 3: Default profile (
|
155
|
-
return
|
139
|
+
# PRIORITY 3: Default profile (AWS standard fallback)
|
140
|
+
return "default"
|
156
141
|
|
157
142
|
|
158
143
|
def create_cost_session(profile: Optional[str] = None) -> boto3.Session:
|
159
144
|
"""
|
160
|
-
Create a boto3 session
|
161
|
-
|
145
|
+
Create a boto3 session for cost operations with universal profile support.
|
146
|
+
Works with ANY AWS profile configuration.
|
162
147
|
|
163
148
|
Args:
|
164
149
|
profile: User-specified profile (from --profile parameter)
|
165
150
|
|
166
151
|
Returns:
|
167
|
-
boto3.Session: Session configured for
|
152
|
+
boto3.Session: Session configured for AWS operations
|
168
153
|
"""
|
169
|
-
|
170
|
-
return boto3.Session(profile_name=
|
154
|
+
selected_profile = get_profile_for_operation("cost", profile)
|
155
|
+
return boto3.Session(profile_name=selected_profile)
|
171
156
|
|
172
157
|
|
173
158
|
def create_management_session(profile: Optional[str] = None) -> boto3.Session:
|
174
159
|
"""
|
175
|
-
Create a boto3 session
|
176
|
-
|
160
|
+
Create a boto3 session for management operations with universal profile support.
|
161
|
+
Works with ANY AWS profile configuration.
|
177
162
|
|
178
163
|
Args:
|
179
164
|
profile: User-specified profile (from --profile parameter)
|
180
165
|
|
181
166
|
Returns:
|
182
|
-
boto3.Session: Session configured for
|
167
|
+
boto3.Session: Session configured for AWS operations
|
183
168
|
"""
|
184
|
-
|
185
|
-
return boto3.Session(profile_name=
|
169
|
+
selected_profile = get_profile_for_operation("management", profile)
|
170
|
+
return boto3.Session(profile_name=selected_profile)
|
186
171
|
|
187
172
|
|
188
173
|
def create_operational_session(profile: Optional[str] = None) -> boto3.Session:
|
189
174
|
"""
|
190
|
-
Create a boto3 session
|
191
|
-
|
175
|
+
Create a boto3 session for operational tasks with universal profile support.
|
176
|
+
Works with ANY AWS profile configuration.
|
192
177
|
|
193
178
|
Args:
|
194
179
|
profile: User-specified profile (from --profile parameter)
|
195
180
|
|
196
181
|
Returns:
|
197
|
-
boto3.Session: Session configured for
|
182
|
+
boto3.Session: Session configured for AWS operations
|
198
183
|
"""
|
199
|
-
|
200
|
-
return boto3.Session(profile_name=
|
184
|
+
selected_profile = get_profile_for_operation("operational", profile)
|
185
|
+
return boto3.Session(profile_name=selected_profile)
|
201
186
|
|
202
187
|
|
203
|
-
def
|
188
|
+
def get_current_profile_info() -> Dict[str, Optional[str]]:
|
204
189
|
"""
|
205
|
-
Get current
|
190
|
+
Get current AWS profile information using universal approach.
|
191
|
+
Works with ANY AWS setup without hardcoded environment variable assumptions.
|
206
192
|
|
207
193
|
Returns:
|
208
|
-
Dict
|
194
|
+
Dict with current profile information
|
209
195
|
"""
|
210
196
|
return {
|
211
|
-
"
|
212
|
-
"
|
213
|
-
"
|
197
|
+
"aws_profile": os.getenv("AWS_PROFILE"),
|
198
|
+
"default_profile": "default",
|
199
|
+
"available_profiles": boto3.Session().available_profiles
|
214
200
|
}
|
215
201
|
|
216
202
|
|
@@ -247,10 +233,10 @@ def validate_profile_access(profile_name: str, operation_type: str = "general")
|
|
247
233
|
|
248
234
|
def get_available_profiles_for_validation() -> list:
|
249
235
|
"""
|
250
|
-
Get available AWS profiles for validation - universal
|
236
|
+
Get available AWS profiles for validation - truly universal approach.
|
251
237
|
|
252
|
-
Returns all configured AWS profiles for validation without hardcoded assumptions.
|
253
|
-
|
238
|
+
Returns all configured AWS profiles for validation without ANY hardcoded assumptions.
|
239
|
+
Works with any AWS setup: single account, multi-account, any profile naming convention.
|
254
240
|
|
255
241
|
Returns:
|
256
242
|
list: Available AWS profile names for validation
|
@@ -259,38 +245,20 @@ def get_available_profiles_for_validation() -> list:
|
|
259
245
|
# Get all available profiles from AWS CLI configuration
|
260
246
|
available_profiles = boto3.Session().available_profiles
|
261
247
|
|
262
|
-
#
|
263
|
-
system_profiles = {'default', 'none', 'null', ''}
|
264
|
-
|
265
|
-
# Return profiles for validation, including default if it's the only one
|
248
|
+
# Start with AWS_PROFILE if set
|
266
249
|
validation_profiles = []
|
250
|
+
aws_profile = os.getenv("AWS_PROFILE")
|
251
|
+
if aws_profile and aws_profile in available_profiles:
|
252
|
+
validation_profiles.append(aws_profile)
|
267
253
|
|
268
|
-
# Add
|
269
|
-
|
270
|
-
|
271
|
-
os.getenv("AWS_MANAGEMENT_PROFILE"),
|
272
|
-
os.getenv("AWS_CENTRALISED_OPS_PROFILE"),
|
273
|
-
os.getenv("AWS_SINGLE_ACCOUNT_PROFILE"),
|
274
|
-
os.getenv("BILLING_PROFILE"),
|
275
|
-
os.getenv("MANAGEMENT_PROFILE"),
|
276
|
-
os.getenv("CENTRALISED_OPS_PROFILE"),
|
277
|
-
os.getenv("SINGLE_AWS_PROFILE"),
|
278
|
-
]
|
279
|
-
|
280
|
-
# Add valid environment profiles
|
281
|
-
for profile in env_profiles:
|
282
|
-
if profile and profile in available_profiles and profile not in validation_profiles:
|
254
|
+
# Add all other available profiles (universal approach)
|
255
|
+
for profile in available_profiles:
|
256
|
+
if profile not in validation_profiles:
|
283
257
|
validation_profiles.append(profile)
|
284
258
|
|
285
|
-
#
|
259
|
+
# Ensure we have at least one profile to test
|
286
260
|
if not validation_profiles:
|
287
|
-
|
288
|
-
if profile not in system_profiles:
|
289
|
-
validation_profiles.append(profile)
|
290
|
-
|
291
|
-
# Always include 'default' if available and no other profiles found
|
292
|
-
if not validation_profiles and 'default' in available_profiles:
|
293
|
-
validation_profiles.append('default')
|
261
|
+
validation_profiles = ['default']
|
294
262
|
|
295
263
|
return validation_profiles
|
296
264
|
|
@@ -303,10 +271,10 @@ def get_available_profiles_for_validation() -> list:
|
|
303
271
|
__all__ = [
|
304
272
|
"get_profile_for_operation",
|
305
273
|
"resolve_profile_for_operation_silent",
|
306
|
-
"create_cost_session",
|
274
|
+
"create_cost_session",
|
307
275
|
"create_management_session",
|
308
276
|
"create_operational_session",
|
309
|
-
"
|
277
|
+
"get_current_profile_info",
|
310
278
|
"validate_profile_access",
|
311
279
|
"get_available_profiles_for_validation",
|
312
280
|
]
|
runbooks/common/rich_utils.py
CHANGED
@@ -358,8 +358,8 @@ def create_display_profile_name(profile_name: str, max_length: int = 25, context
|
|
358
358
|
meaningful information for identification. Full names remain available for AWS API calls.
|
359
359
|
|
360
360
|
Examples:
|
361
|
-
'
|
362
|
-
'
|
361
|
+
'your-admin-Billing-ReadOnlyAccess-123456789012' → 'your-admin-Billing-1234...'
|
362
|
+
'your-centralised-ops-ReadOnlyAccess-987654321098' → 'your-centralised-ops-9876...'
|
363
363
|
'short-profile' → 'short-profile' (no truncation needed)
|
364
364
|
|
365
365
|
Args:
|
@@ -398,7 +398,7 @@ def create_display_profile_name(profile_name: str, max_length: int = 25, context
|
|
398
398
|
|
399
399
|
# Strategy 1: Keep meaningful prefix + account ID suffix
|
400
400
|
if len(parts) >= 4 and parts[-1].isdigit():
|
401
|
-
# Enterprise pattern:
|
401
|
+
# Enterprise pattern: your-admin-Billing-ReadOnlyAccess-123456789012
|
402
402
|
account_id = parts[-1]
|
403
403
|
prefix_parts = parts[:-2] # Skip permissions part for brevity
|
404
404
|
|