runbooks 1.1.4__py3-none-any.whl → 1.1.6__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 +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/assessment/compliance.py +1 -1
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cli/__init__.py +1 -1
- runbooks/cli/commands/cfat.py +64 -23
- runbooks/cli/commands/finops.py +1005 -54
- runbooks/cli/commands/inventory.py +135 -91
- runbooks/cli/commands/operate.py +9 -36
- runbooks/cli/commands/security.py +42 -18
- runbooks/cli/commands/validation.py +432 -18
- runbooks/cli/commands/vpc.py +81 -17
- runbooks/cli/registry.py +22 -10
- runbooks/cloudops/__init__.py +20 -27
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +544 -542
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +224 -225
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +177 -213
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +17 -12
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +40 -36
- runbooks/common/aws_utils.py +74 -79
- runbooks/common/business_logic.py +126 -104
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
- runbooks/common/cross_account_manager.py +197 -204
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +29 -19
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +476 -493
- runbooks/common/mcp_integration.py +99 -79
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +175 -193
- runbooks/common/patterns.py +23 -25
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +111 -37
- runbooks/common/rich_utils.py +315 -141
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +26 -30
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +484 -618
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +32 -29
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +223 -285
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +337 -174
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1512 -481
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +19 -23
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +64 -65
- runbooks/finops/scenarios.py +1277 -438
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +259 -265
- runbooks/finops/vpc_cleanup_exporter.py +189 -144
- runbooks/finops/vpc_cleanup_optimizer.py +591 -573
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/aws_decorators.py +2 -3
- runbooks/inventory/check_cloudtrail_compliance.py +2 -4
- runbooks/inventory/check_controltower_readiness.py +152 -151
- runbooks/inventory/check_landingzone_readiness.py +85 -84
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +1 -1
- runbooks/inventory/collectors/aws_networking.py +109 -99
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/core/formatter.py +11 -0
- runbooks/inventory/draw_org_structure.py +8 -9
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/ec2_vpc_utils.py +2 -2
- runbooks/inventory/find_cfn_drift_detection.py +5 -7
- runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
- runbooks/inventory/find_cfn_stackset_drift.py +5 -6
- runbooks/inventory/find_ec2_security_groups.py +48 -42
- runbooks/inventory/find_landingzone_versions.py +4 -6
- runbooks/inventory/find_vpc_flow_logs.py +7 -9
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/inventory_modules.py +103 -91
- runbooks/inventory/list_cfn_stacks.py +9 -10
- runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
- runbooks/inventory/list_cfn_stackset_operations.py +79 -57
- runbooks/inventory/list_cfn_stacksets.py +8 -10
- runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
- runbooks/inventory/list_ds_directories.py +65 -53
- runbooks/inventory/list_ec2_availability_zones.py +2 -4
- runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
- runbooks/inventory/list_ec2_instances.py +23 -28
- runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
- runbooks/inventory/list_elbs_load_balancers.py +22 -20
- runbooks/inventory/list_enis_network_interfaces.py +26 -33
- runbooks/inventory/list_guardduty_detectors.py +2 -4
- runbooks/inventory/list_iam_policies.py +2 -4
- runbooks/inventory/list_iam_roles.py +5 -7
- runbooks/inventory/list_iam_saml_providers.py +4 -6
- runbooks/inventory/list_lambda_functions.py +38 -38
- runbooks/inventory/list_org_accounts.py +6 -8
- runbooks/inventory/list_org_accounts_users.py +55 -44
- runbooks/inventory/list_rds_db_instances.py +31 -33
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/list_route53_hosted_zones.py +3 -5
- runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
- runbooks/inventory/list_sns_topics.py +2 -4
- runbooks/inventory/list_ssm_parameters.py +4 -7
- runbooks/inventory/list_vpc_subnets.py +2 -4
- runbooks/inventory/list_vpcs.py +7 -10
- runbooks/inventory/mcp_inventory_validator.py +554 -468
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +63 -55
- runbooks/inventory/recover_cfn_stack_ids.py +7 -8
- runbooks/inventory/requirements.txt +0 -1
- runbooks/inventory/rich_inventory_display.py +35 -34
- runbooks/inventory/run_on_multi_accounts.py +3 -5
- runbooks/inventory/unified_validation_engine.py +281 -253
- runbooks/inventory/verify_ec2_security_groups.py +1 -1
- runbooks/inventory/vpc_analyzer.py +735 -697
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +384 -380
- runbooks/inventory/vpc_flow_analyzer.py +1 -1
- runbooks/main.py +49 -34
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/networking_cost_heatmap.py +29 -8
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_operations.py +646 -616
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +70 -66
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +86 -60
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +46 -41
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +50 -47
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +745 -704
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +461 -454
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +185 -160
- runbooks/vpc/mcp_no_eni_validator.py +680 -639
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1297 -1124
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.6.dist-info/METADATA +327 -0
- runbooks-1.1.6.dist-info/RECORD +489 -0
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -973
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.4.dist-info/METADATA +0 -800
- runbooks-1.1.4.dist-info/RECORD +0 -468
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/top_level.txt +0 -0
@@ -455,12 +455,12 @@ def print_timings(fTiming: bool = False, fverbose: int = 50, fbegin_time=None, f
|
|
455
455
|
"""
|
456
456
|
from time import time
|
457
457
|
|
458
|
-
from
|
458
|
+
from runbooks.common.rich_utils import console
|
459
459
|
|
460
460
|
init()
|
461
461
|
|
462
462
|
if fTiming and fverbose < 50 and fbegin_time is not None:
|
463
|
-
print(f"{
|
463
|
+
print(f"[green]{fmessage}\nThis script has taken {time() - fbegin_time:.6f} seconds so far")
|
464
464
|
|
465
465
|
|
466
466
|
def make_creds(faws_acct):
|
@@ -4241,12 +4241,14 @@ def find_stacksets3(
|
|
4241
4241
|
from queue import Queue
|
4242
4242
|
from threading import Thread
|
4243
4243
|
|
4244
|
-
from
|
4244
|
+
from runbooks.common.rich_utils import create_progress_bar
|
4245
4245
|
|
4246
4246
|
class GetStackSetStatus(Thread):
|
4247
|
-
def __init__(self, queue):
|
4247
|
+
def __init__(self, queue, progress, task_id):
|
4248
4248
|
Thread.__init__(self)
|
4249
4249
|
self.queue = queue
|
4250
|
+
self.progress = progress
|
4251
|
+
self.task_id = task_id
|
4250
4252
|
|
4251
4253
|
def run(self):
|
4252
4254
|
while True:
|
@@ -4317,7 +4319,7 @@ def find_stacksets3(
|
|
4317
4319
|
)
|
4318
4320
|
continue
|
4319
4321
|
finally:
|
4320
|
-
|
4322
|
+
self.progress.update(self.task_id, advance=1)
|
4321
4323
|
self.queue.task_done()
|
4322
4324
|
|
4323
4325
|
###########
|
@@ -4328,35 +4330,35 @@ def find_stacksets3(
|
|
4328
4330
|
WorkerThreads = min(len(fStackSetsCopy), MaxWorkerThreads)
|
4329
4331
|
logging.info(f"Using {WorkerThreads} threads")
|
4330
4332
|
|
4331
|
-
|
4332
|
-
|
4333
|
-
|
4334
|
-
|
4335
|
-
|
4333
|
+
with create_progress_bar() as progress:
|
4334
|
+
task = progress.add_task(
|
4335
|
+
"[cyan]Finding all Stacksets...",
|
4336
|
+
total=len(fStackSetsCopy)
|
4337
|
+
)
|
4336
4338
|
|
4337
|
-
|
4338
|
-
|
4339
|
-
|
4340
|
-
|
4341
|
-
|
4339
|
+
for x in range(WorkerThreads):
|
4340
|
+
worker = GetStackSetStatus(checkqueue, progress, task)
|
4341
|
+
# Setting daemon to True will let the main thread exit even though the workers are blocking
|
4342
|
+
worker.daemon = True
|
4343
|
+
worker.start()
|
4344
|
+
|
4345
|
+
for stackset in fStackSetsCopy:
|
4346
|
+
logging.debug(f"Beginning to queue data - starting with {stackset['StackSetName']}")
|
4347
|
+
try:
|
4348
|
+
# I don't know why - but double parens are necessary below. If you remove them, only the first parameter is queued.
|
4349
|
+
PlaceCount += 1
|
4350
|
+
# print(".", end='')
|
4351
|
+
checkqueue.put((stackset, fRegion, PlaceCount))
|
4352
|
+
except ClientError as my_Error:
|
4353
|
+
if "AuthFailure" in str(my_Error):
|
4354
|
+
logging.error(
|
4355
|
+
f"Authorization Failure accessing stack set {stackset['StackSetName']} in {fRegion} region"
|
4356
|
+
)
|
4357
|
+
logging.warning(f"It's possible that the region {fRegion} hasn't been opted-into")
|
4358
|
+
pass
|
4359
|
+
checkqueue.join()
|
4360
|
+
logging.info(f"Getting the stackset operation data took {time() - begin_time:.2f} seconds")
|
4342
4361
|
|
4343
|
-
for stackset in fStackSetsCopy:
|
4344
|
-
logging.debug(f"Beginning to queue data - starting with {stackset['StackSetName']}")
|
4345
|
-
try:
|
4346
|
-
# I don't know why - but double parens are necessary below. If you remove them, only the first parameter is queued.
|
4347
|
-
PlaceCount += 1
|
4348
|
-
# print(".", end='')
|
4349
|
-
checkqueue.put((stackset, fRegion, PlaceCount))
|
4350
|
-
except ClientError as my_Error:
|
4351
|
-
if "AuthFailure" in str(my_Error):
|
4352
|
-
logging.error(
|
4353
|
-
f"Authorization Failure accessing stack set {stackset['StackSetName']} in {fRegion} region"
|
4354
|
-
)
|
4355
|
-
logging.warning(f"It's possible that the region {fRegion} hasn't been opted-into")
|
4356
|
-
pass
|
4357
|
-
checkqueue.join()
|
4358
|
-
logging.info(f"Getting the stackset operation data took {time() - begin_time:.2f} seconds")
|
4359
|
-
pbar.close()
|
4360
4362
|
return fStackSetsCopy
|
4361
4363
|
|
4362
4364
|
# Logging Settings
|
@@ -5341,7 +5343,7 @@ def display_results(
|
|
5341
5343
|
):
|
5342
5344
|
from datetime import datetime
|
5343
5345
|
|
5344
|
-
from
|
5346
|
+
from runbooks.common.rich_utils import console
|
5345
5347
|
|
5346
5348
|
init()
|
5347
5349
|
"""
|
@@ -5757,12 +5759,12 @@ def get_all_credentials(
|
|
5757
5759
|
from .account_class import aws_acct_access
|
5758
5760
|
|
5759
5761
|
# from time import time
|
5760
|
-
from
|
5762
|
+
from runbooks.common.rich_utils import console
|
5761
5763
|
|
5762
5764
|
init()
|
5763
5765
|
# ERASE_LINE = '\x1b[2K'
|
5764
5766
|
# begin_time = time()
|
5765
|
-
print(f"
|
5767
|
+
print(f"[green]Timing is enabled") if fTiming else None
|
5766
5768
|
|
5767
5769
|
AllCredentials = []
|
5768
5770
|
if fSkipProfiles is None:
|
@@ -5848,16 +5850,18 @@ def get_credentials_for_accounts_in_org(
|
|
5848
5850
|
from time import time
|
5849
5851
|
|
5850
5852
|
from botocore.exceptions import ClientError
|
5851
|
-
from
|
5852
|
-
from
|
5853
|
+
from runbooks.common.rich_utils import console
|
5854
|
+
from runbooks.common.rich_utils import create_progress_bar
|
5853
5855
|
|
5854
5856
|
init()
|
5855
5857
|
begin_time = time()
|
5856
5858
|
|
5857
5859
|
class AssembleCredentials(Thread):
|
5858
|
-
def __init__(self, queue):
|
5860
|
+
def __init__(self, queue, progress, task_id):
|
5859
5861
|
Thread.__init__(self)
|
5860
5862
|
self.queue = queue
|
5863
|
+
self.progress = progress
|
5864
|
+
self.task_id = task_id
|
5861
5865
|
|
5862
5866
|
def run(self):
|
5863
5867
|
while True:
|
@@ -5914,7 +5918,7 @@ def get_credentials_for_accounts_in_org(
|
|
5914
5918
|
logging.error(f"Error: Likely that one of the supplied profiles was wrong\nError: {my_Error}")
|
5915
5919
|
continue
|
5916
5920
|
finally:
|
5917
|
-
|
5921
|
+
self.progress.update(self.task_id, advance=1)
|
5918
5922
|
self.queue.task_done()
|
5919
5923
|
|
5920
5924
|
if fSkipAccounts is None:
|
@@ -5959,47 +5963,48 @@ def get_credentials_for_accounts_in_org(
|
|
5959
5963
|
# Defaults to 50, unless something more was passed in - which is only done for time testing.
|
5960
5964
|
WorkerThreads = min(len(ChildAccounts) * len(fregions), MaxThreads)
|
5961
5965
|
|
5962
|
-
# Create x worker threads
|
5963
|
-
for x in range(WorkerThreads):
|
5964
|
-
worker = AssembleCredentials(credqueue)
|
5965
|
-
# Setting daemon to True will let the main thread exit even though the workers are blocking
|
5966
|
-
worker.daemon = True
|
5967
|
-
worker.start()
|
5968
|
-
|
5969
|
-
pbar = tqdm(
|
5970
|
-
desc=f"Getting credentials for profile: {fprofile} with {len(ChildAccounts)} accounts in {len(fregions)} regions",
|
5971
|
-
total=len(ChildAccounts) * len(fregions),
|
5972
|
-
unit=" credentials",
|
5973
|
-
)
|
5974
|
-
|
5975
5966
|
logging.info(
|
5976
5967
|
f"You asked to check {len(ChildAccounts) * len(fregions)} place{'s' if len(ChildAccounts) * len(fregions) > 1 else ''}... It's going to take a moment"
|
5977
5968
|
)
|
5978
|
-
|
5979
|
-
|
5980
|
-
|
5981
|
-
|
5982
|
-
|
5983
|
-
|
5984
|
-
|
5985
|
-
|
5986
|
-
|
5987
|
-
|
5988
|
-
|
5989
|
-
|
5990
|
-
|
5991
|
-
|
5992
|
-
|
5993
|
-
|
5994
|
-
|
5995
|
-
|
5996
|
-
|
5997
|
-
|
5998
|
-
|
5999
|
-
|
6000
|
-
|
6001
|
-
|
6002
|
-
|
5969
|
+
|
5970
|
+
with create_progress_bar() as progress:
|
5971
|
+
task = progress.add_task(
|
5972
|
+
"[cyan]Getting credentials...",
|
5973
|
+
total=len(ChildAccounts) * len(fregions)
|
5974
|
+
)
|
5975
|
+
|
5976
|
+
# Create x worker threads
|
5977
|
+
for x in range(WorkerThreads):
|
5978
|
+
worker = AssembleCredentials(credqueue, progress, task)
|
5979
|
+
# Setting daemon to True will let the main thread exit even though the workers are blocking
|
5980
|
+
worker.daemon = True
|
5981
|
+
worker.start()
|
5982
|
+
|
5983
|
+
logging.info(
|
5984
|
+
f"[green]It's taken {time() - begin_time:.2f} seconds to prep WorkerThreads and such"
|
5985
|
+
) if fTiming else None
|
5986
|
+
for account in ChildAccounts:
|
5987
|
+
if account["AccountId"] in fSkipAccounts:
|
5988
|
+
continue
|
5989
|
+
elif fRootOnly and not account["AccountId"] == account["MgmtAccount"]:
|
5990
|
+
continue
|
5991
|
+
elif accountlist and account["AccountId"] not in accountlist:
|
5992
|
+
continue
|
5993
|
+
AccountNum += 1
|
5994
|
+
logging.info(f"Queuing account info for {AccountNum} / {len(ChildAccounts)} accounts in profile {fprofile}")
|
5995
|
+
RegionNum = 0
|
5996
|
+
for region in fregions:
|
5997
|
+
RegionNum += 1
|
5998
|
+
logging.info(f"\t\tRegion {RegionNum} of {len(fregions)}")
|
5999
|
+
credqueue.put((account, fprofile, region))
|
6000
|
+
logging.info(f"Account / Region: {account} / {region} | {datetime.now()}")
|
6001
|
+
logging.info(f"Queue Size: {credqueue.qsize()}")
|
6002
|
+
print(
|
6003
|
+
f"[green]Enumerating {AccountNum} account{'s' if len(ChildAccounts) * len(fregions) > 1 else ''} and {len(fregions)} regions "
|
6004
|
+
f"took {time() - begin_time:.3f} seconds "
|
6005
|
+
) if fTiming else None
|
6006
|
+
credqueue.join()
|
6007
|
+
|
6003
6008
|
return AllCreds
|
6004
6009
|
|
6005
6010
|
|
@@ -6015,19 +6020,21 @@ def get_org_accounts_from_profiles(fProfileList):
|
|
6015
6020
|
|
6016
6021
|
from .account_class import aws_acct_access
|
6017
6022
|
from botocore.exceptions import ClientError, InvalidConfigError, NoCredentialsError
|
6018
|
-
from
|
6023
|
+
from runbooks.common.rich_utils import create_progress_bar
|
6019
6024
|
|
6020
6025
|
class AssembleCredentials(Thread):
|
6021
|
-
def __init__(self, queue):
|
6026
|
+
def __init__(self, queue, progress, task_id):
|
6022
6027
|
Thread.__init__(self)
|
6023
6028
|
self.queue = queue
|
6029
|
+
self.progress = progress
|
6030
|
+
self.task_id = task_id
|
6024
6031
|
|
6025
6032
|
def run(self):
|
6026
6033
|
# Account = dict()
|
6027
6034
|
while True:
|
6028
6035
|
# Get the work from the queue and expand the tuple
|
6029
6036
|
profile = self.queue.get()
|
6030
|
-
|
6037
|
+
self.progress.update(self.task_id, advance=1)
|
6031
6038
|
Account = {
|
6032
6039
|
"ErrorFlag": False,
|
6033
6040
|
"Success": False,
|
@@ -6111,17 +6118,22 @@ def get_org_accounts_from_profiles(fProfileList):
|
|
6111
6118
|
# WorkerThreads = len(fProfileList)
|
6112
6119
|
WorkerThreads = 2
|
6113
6120
|
|
6114
|
-
|
6115
|
-
|
6116
|
-
|
6117
|
-
|
6118
|
-
|
6119
|
-
|
6121
|
+
with create_progress_bar() as progress:
|
6122
|
+
task = progress.add_task(
|
6123
|
+
f"[cyan]Getting accounts from {len(fProfileList)} profiles...",
|
6124
|
+
total=len(fProfileList)
|
6125
|
+
)
|
6126
|
+
|
6127
|
+
# Create x worker threads
|
6128
|
+
for x in range(WorkerThreads):
|
6129
|
+
worker = AssembleCredentials(profilequeue, progress, task)
|
6130
|
+
# Setting daemon to True will let the main thread exit even though the workers are blocking
|
6131
|
+
worker.daemon = True
|
6132
|
+
worker.start()
|
6120
6133
|
|
6121
|
-
|
6134
|
+
for profile_item in fProfileList:
|
6135
|
+
logging.info(f"Queuing profile {profile_item} / {len(fProfileList)} profiles")
|
6136
|
+
profilequeue.put(profile_item)
|
6137
|
+
profilequeue.join()
|
6122
6138
|
|
6123
|
-
for profile_item in fProfileList:
|
6124
|
-
logging.info(f"Queuing profile {profile_item} / {len(fProfileList)} profiles")
|
6125
|
-
profilequeue.put(profile_item)
|
6126
|
-
profilequeue.join()
|
6127
6139
|
return AllAccounts
|
@@ -67,10 +67,9 @@ import Inventory_Modules
|
|
67
67
|
from account_class import aws_acct_access
|
68
68
|
from ArgumentsClass import CommonArguments
|
69
69
|
from botocore.exceptions import ClientError
|
70
|
-
from
|
70
|
+
from runbooks.common.rich_utils import console
|
71
71
|
from Inventory_Modules import display_results, get_all_credentials
|
72
72
|
|
73
|
-
init()
|
74
73
|
|
75
74
|
__version__ = "2024.05.31"
|
76
75
|
|
@@ -230,16 +229,16 @@ def setup_auth_accounts_and_regions(
|
|
230
229
|
else:
|
231
230
|
AccountList = [account["AccountId"] for account in ChildAccounts if account["AccountId"] in fAccountList]
|
232
231
|
|
233
|
-
print(f"You asked to find stacks with this fragment
|
234
|
-
print(f"in these accounts:\n{
|
235
|
-
print(f"in these regions:\n{
|
236
|
-
print(f"While skipping these accounts:\n{
|
232
|
+
print(f"You asked to find stacks with this fragment [red]'{fStackFrag}'")
|
233
|
+
print(f"in these accounts:\n[red]{AccountList}")
|
234
|
+
print(f"in these regions:\n[red]{RegionList}")
|
235
|
+
print(f"While skipping these accounts:\n[red]{fSkipAccounts}") if fSkipAccounts is not None else ""
|
237
236
|
if fDeletionRun:
|
238
237
|
print()
|
239
238
|
print("And delete the stacks that are found...")
|
240
239
|
|
241
240
|
if fExact:
|
242
|
-
print(f"\t\tFor stacks that
|
241
|
+
print(f"\t\tFor stacks that [red]exactly match these fragments: {fStackFrag}")
|
243
242
|
else:
|
244
243
|
print(f"\t\tFor stacks that contains these fragments: {fStackFrag}")
|
245
244
|
|
@@ -330,7 +329,7 @@ def collect_cfnstacks(fCredentialList: list) -> list:
|
|
330
329
|
|
331
330
|
# Display real-time progress with colored output
|
332
331
|
print(
|
333
|
-
f"{ERASE_LINE}
|
332
|
+
f"{ERASE_LINE}[red]Account: {credential['AccountId']} Region: {credential['Region']} Found {len(Stacks)} Stacks",
|
334
333
|
end="\r",
|
335
334
|
)
|
336
335
|
|
@@ -410,7 +409,7 @@ def display_stacks(fAllStacks: list):
|
|
410
409
|
)
|
411
410
|
print(ERASE_LINE)
|
412
411
|
print(
|
413
|
-
f"
|
412
|
+
f"[red]Found {len(fAllStacks)} stacks across {len(AccountList)} accounts across {len(RegionList)} regions"
|
414
413
|
)
|
415
414
|
print()
|
416
415
|
if args.loglevel < 21: # INFO level
|
@@ -551,7 +550,7 @@ if __name__ == "__main__":
|
|
551
550
|
|
552
551
|
if pTiming:
|
553
552
|
print(ERASE_LINE)
|
554
|
-
print(f"
|
553
|
+
print(f"[green]This script took {time() - begin_time:.2f} seconds")
|
555
554
|
|
556
555
|
print()
|
557
556
|
print("Thanks for using this script...")
|
@@ -76,9 +76,8 @@ import logging
|
|
76
76
|
import re
|
77
77
|
|
78
78
|
from ArgumentsClass import CommonArguments
|
79
|
-
from
|
79
|
+
from runbooks.common.rich_utils import console
|
80
80
|
|
81
|
-
init()
|
82
81
|
__version__ = "2024.06.20"
|
83
82
|
|
84
83
|
# Configure CLI argument parsing for StackSet results analysis and correlation
|
@@ -123,7 +122,6 @@ logging.getLogger("urllib3").setLevel(logging.CRITICAL) # Suppress HTTP client
|
|
123
122
|
# Analysis and Data Processing
|
124
123
|
##########################
|
125
124
|
|
126
|
-
ERASE_LINE = "\x1b[2K" # Terminal line clearing for dynamic output updates
|
127
125
|
|
128
126
|
# Initialize StackSets data structure for comprehensive deployment analysis
|
129
127
|
StackSets = {}
|
@@ -77,14 +77,13 @@ import Inventory_Modules
|
|
77
77
|
from account_class import aws_acct_access
|
78
78
|
from ArgumentsClass import CommonArguments
|
79
79
|
from botocore.exceptions import ClientError
|
80
|
-
from
|
80
|
+
from runbooks.common.rich_utils import console
|
81
81
|
from Inventory_Modules import display_results, find_stacksets3, get_regions3
|
82
|
-
|
82
|
+
# Migrated to Rich.Progress - see rich_utils.py for enterprise UX standards
|
83
|
+
# from tqdm.auto import tqdm
|
83
84
|
|
84
|
-
init()
|
85
85
|
|
86
86
|
__version__ = "2024.05.18"
|
87
|
-
ERASE_LINE = "\x1b[2K"
|
88
87
|
begin_time = time()
|
89
88
|
DefaultMaxWorkerThreads = 5
|
90
89
|
|
@@ -268,8 +267,8 @@ def setup_auth_and_regions(
|
|
268
267
|
if fRegion.lower() not in RegionList:
|
269
268
|
print()
|
270
269
|
print(
|
271
|
-
f"
|
272
|
-
f"Please run the command again and specify only a single, valid region
|
270
|
+
f"[red]You specified '{fRegion}' as the region, but this script only works with a single region.\n"
|
271
|
+
f"Please run the command again and specify only a single, valid region"
|
273
272
|
)
|
274
273
|
print()
|
275
274
|
raise ValueError(f"You specified '{fRegion}' as the region, but this script only works with a single region.")
|
@@ -283,7 +282,7 @@ def setup_auth_and_regions(
|
|
283
282
|
|
284
283
|
# Display fragment matching configuration for search transparency
|
285
284
|
if fExact:
|
286
|
-
print(f"\t\tFor stacksets that
|
285
|
+
print(f"\t\tFor stacksets that [red]exactly match these fragments: {fStackfrag}")
|
287
286
|
else:
|
288
287
|
print(f"\t\tFor stacksets that contains these fragments: {fStackfrag}")
|
289
288
|
|
@@ -557,7 +556,7 @@ def find_stack_set_instances(fStackSetNames: list, fRegion: str) -> list:
|
|
557
556
|
logging.info(
|
558
557
|
f"{ERASE_LINE}Finished finding stack instances in stackset {c_stacksetname} in region {c_region} - {c_PlaceCount} / {len(fStackSetNames)}"
|
559
558
|
)
|
560
|
-
pbar.update() # Update progress bar for operational visibility
|
559
|
+
pbar.update(pbar_task, advance=1) # Update Rich progress bar for operational visibility
|
561
560
|
self.queue.task_done() # Mark queue item as completed
|
562
561
|
|
563
562
|
###########
|
@@ -573,40 +572,50 @@ def find_stack_set_instances(fStackSetNames: list, fRegion: str) -> list:
|
|
573
572
|
# Configure optimal worker thread count based on StackSet count and system limits
|
574
573
|
WorkerThreads = min(len(fStackSetNames), DefaultMaxWorkerThreads)
|
575
574
|
|
575
|
+
# Import Rich display utilities for professional progress tracking
|
576
|
+
from runbooks.common.rich_utils import create_progress_bar
|
577
|
+
|
576
578
|
# Initialize progress tracking for operational visibility during discovery
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
579
|
+
with create_progress_bar() as progress:
|
580
|
+
task = progress.add_task(
|
581
|
+
f"[cyan]Finding Stackset instances from {len(fStackSetNames)} stacksets...",
|
582
|
+
total=len(fStackSetNames)
|
583
|
+
)
|
584
|
+
|
585
|
+
# Make progress object available to worker threads via global (multi-threaded pattern)
|
586
|
+
global pbar
|
587
|
+
pbar = progress
|
588
|
+
global pbar_task
|
589
|
+
pbar_task = task
|
590
|
+
|
591
|
+
# Create and start worker thread pool for concurrent StackSet instance discovery
|
592
|
+
for x in range(WorkerThreads):
|
593
|
+
worker = FindStackSets(checkqueue)
|
594
|
+
# Daemon threads allow main thread exit even if workers are still processing
|
595
|
+
worker.daemon = True
|
596
|
+
worker.start()
|
597
|
+
|
598
|
+
# Queue StackSet discovery work items for worker thread processing
|
599
|
+
for stacksetname in fStackSetNames:
|
600
|
+
logging.debug(f"Beginning to queue data - starting with {stacksetname}")
|
601
|
+
try:
|
602
|
+
# Queue StackSet information for worker thread processing
|
603
|
+
# Note: Tuple structure is critical for proper parameter expansion in worker threads
|
604
|
+
PlaceCount += 1
|
605
|
+
checkqueue.put((stacksetname, fRegion, stacksetname, PlaceCount))
|
606
|
+
except ClientError as my_Error:
|
607
|
+
# Handle authorization failures with informative error messaging
|
608
|
+
if "AuthFailure" in str(my_Error):
|
609
|
+
logging.error(
|
610
|
+
f"Authorization Failure accessing stack set {stacksetname['StackSetName']} in {fRegion} region"
|
611
|
+
)
|
612
|
+
logging.warning(f"It's possible that the region {fRegion} hasn't been opted-into")
|
613
|
+
pass
|
614
|
+
|
615
|
+
# Wait for all worker threads to complete processing
|
616
|
+
checkqueue.join()
|
617
|
+
# Progress bar auto-closes when exiting context manager
|
582
618
|
|
583
|
-
# Create and start worker thread pool for concurrent StackSet instance discovery
|
584
|
-
for x in range(WorkerThreads):
|
585
|
-
worker = FindStackSets(checkqueue)
|
586
|
-
# Daemon threads allow main thread exit even if workers are still processing
|
587
|
-
worker.daemon = True
|
588
|
-
worker.start()
|
589
|
-
|
590
|
-
# Queue StackSet discovery work items for worker thread processing
|
591
|
-
for stacksetname in fStackSetNames:
|
592
|
-
logging.debug(f"Beginning to queue data - starting with {stacksetname}")
|
593
|
-
try:
|
594
|
-
# Queue StackSet information for worker thread processing
|
595
|
-
# Note: Tuple structure is critical for proper parameter expansion in worker threads
|
596
|
-
PlaceCount += 1
|
597
|
-
checkqueue.put((stacksetname, fRegion, stacksetname, PlaceCount))
|
598
|
-
except ClientError as my_Error:
|
599
|
-
# Handle authorization failures with informative error messaging
|
600
|
-
if "AuthFailure" in str(my_Error):
|
601
|
-
logging.error(
|
602
|
-
f"Authorization Failure accessing stack set {stacksetname['StackSetName']} in {fRegion} region"
|
603
|
-
)
|
604
|
-
logging.warning(f"It's possible that the region {fRegion} hasn't been opted-into")
|
605
|
-
pass
|
606
|
-
|
607
|
-
# Wait for all worker threads to complete processing
|
608
|
-
checkqueue.join()
|
609
|
-
pbar.close() # Close progress bar after completion
|
610
619
|
return f_combined_stack_set_instances
|
611
620
|
|
612
621
|
|
@@ -659,23 +668,36 @@ def find_last_operations(faws_acct: aws_acct_access, fStackSetNames: list):
|
|
659
668
|
StackSetOps_client = faws_acct.session.client("cloudformation")
|
660
669
|
AllStackSetOps = []
|
661
670
|
|
671
|
+
# Import Rich display utilities for professional progress tracking
|
672
|
+
from runbooks.common.rich_utils import create_progress_bar
|
673
|
+
|
662
674
|
# Discover last operation for each StackSet with progress tracking
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
)["Summaries"]
|
668
|
-
|
669
|
-
# Extract and aggregate operation metadata for analysis
|
670
|
-
AllStackSetOps.append(
|
671
|
-
{
|
672
|
-
"StackSetName": stacksetname, # StackSet identifier for correlation
|
673
|
-
"Operation": StackSetOps[0]["Action"], # Operation type for lifecycle tracking
|
674
|
-
"LatestStatus": StackSetOps[0]["Status"], # Current operation status
|
675
|
-
"LatestDate": StackSetOps[0]["EndTimestamp"], # Completion timestamp
|
676
|
-
"Details": StackSetOps[0]["StatusDetails"]["FailedStackInstancesCount"], # Failure count for analysis
|
677
|
-
}
|
675
|
+
with create_progress_bar() as progress:
|
676
|
+
task = progress.add_task(
|
677
|
+
"[cyan]Checking stackset operations...",
|
678
|
+
total=len(fStackSetNames)
|
678
679
|
)
|
680
|
+
|
681
|
+
for stacksetname in fStackSetNames:
|
682
|
+
# Retrieve most recent operation for the current StackSet
|
683
|
+
StackSetOps = StackSetOps_client.list_stack_set_operations(
|
684
|
+
StackSetName=stacksetname, MaxResults=1, CallAs="SELF"
|
685
|
+
)["Summaries"]
|
686
|
+
|
687
|
+
# Extract and aggregate operation metadata for analysis
|
688
|
+
AllStackSetOps.append(
|
689
|
+
{
|
690
|
+
"StackSetName": stacksetname, # StackSet identifier for correlation
|
691
|
+
"Operation": StackSetOps[0]["Action"], # Operation type for lifecycle tracking
|
692
|
+
"LatestStatus": StackSetOps[0]["Status"], # Current operation status
|
693
|
+
"LatestDate": StackSetOps[0]["EndTimestamp"], # Completion timestamp
|
694
|
+
"Details": StackSetOps[0]["StatusDetails"]["FailedStackInstancesCount"], # Failure count for analysis
|
695
|
+
}
|
696
|
+
)
|
697
|
+
|
698
|
+
# Update progress after processing each StackSet
|
699
|
+
progress.update(task, advance=1)
|
700
|
+
|
679
701
|
return AllStackSetOps
|
680
702
|
|
681
703
|
|
@@ -724,11 +746,11 @@ if __name__ == "__main__":
|
|
724
746
|
print()
|
725
747
|
print(ERASE_LINE)
|
726
748
|
print(
|
727
|
-
f"
|
749
|
+
f"[red]Found {len(StackSets['StackSetsList'])} Stacksets across {len(Accounts)} accounts across {len(Regions)} regions"
|
728
750
|
)
|
729
751
|
print()
|
730
752
|
if pTiming:
|
731
753
|
print(ERASE_LINE)
|
732
|
-
print(f"
|
754
|
+
print(f"[green]This script took {time() - begin_time:.2f} seconds")
|
733
755
|
print("Thanks for using this script...")
|
734
756
|
print()
|
@@ -88,7 +88,7 @@ from time import time
|
|
88
88
|
|
89
89
|
from account_class import aws_acct_access
|
90
90
|
from ArgumentsClass import CommonArguments
|
91
|
-
from
|
91
|
+
from runbooks.common.rich_utils import console
|
92
92
|
from Inventory_Modules import (
|
93
93
|
RemoveCoreAccounts,
|
94
94
|
display_results,
|
@@ -98,11 +98,9 @@ from Inventory_Modules import (
|
|
98
98
|
get_regions3,
|
99
99
|
)
|
100
100
|
|
101
|
-
init()
|
102
101
|
|
103
102
|
__version__ = "2024.06.20"
|
104
103
|
begin_time = time()
|
105
|
-
ERASE_LINE = "\x1b[2K"
|
106
104
|
|
107
105
|
#####################
|
108
106
|
# Functions
|
@@ -270,13 +268,13 @@ def setup_auth_accounts_and_regions(fProfile: str) -> (aws_acct_access, list, li
|
|
270
268
|
if pRootOnly:
|
271
269
|
print(f"\tIn only the root account: {aws_acct.acct_number}")
|
272
270
|
else:
|
273
|
-
print(f"\tin these accounts: {
|
274
|
-
print(f"\tin these regions: {
|
271
|
+
print(f"\tin these accounts: [red]{AccountList}")
|
272
|
+
print(f"\tin these regions: [red]{RegionList}")
|
275
273
|
print(
|
276
274
|
f"\tContaining {'this ' + Fore.RED + 'exact fragment' + Fore.RESET if pExact else 'one of these fragments'}: {pFragments}"
|
277
275
|
)
|
278
276
|
if pSkipAccounts is not None:
|
279
|
-
print(f"\tWhile skipping these accounts: {
|
277
|
+
print(f"\tWhile skipping these accounts: [red]{pSkipAccounts}")
|
280
278
|
|
281
279
|
return aws_acct, AccountList, RegionList
|
282
280
|
|
@@ -343,7 +341,7 @@ def find_all_cfnstacksets(f_All_Credentials: list, f_Fragments: list, f_Status)
|
|
343
341
|
# logging.info(f"Account Creds: {account_credentials}")
|
344
342
|
# Display progress for operational visibility during StackSet discovery
|
345
343
|
print(
|
346
|
-
f"{ERASE_LINE}
|
344
|
+
f"{ERASE_LINE}[red]Checking Account: {credential['AccountId']} Region: {credential['Region']} for stacksets matching {f_Fragments} with status: {f_Status}",
|
347
345
|
end="\r",
|
348
346
|
)
|
349
347
|
|
@@ -361,7 +359,7 @@ def find_all_cfnstacksets(f_All_Credentials: list, f_Fragments: list, f_Status)
|
|
361
359
|
) if verbose < 50 else ""
|
362
360
|
else:
|
363
361
|
print(
|
364
|
-
f"{ERASE_LINE}
|
362
|
+
f"{ERASE_LINE}[red]Account: {credential['AccountId']} Region: {credential['Region']} Found {len(StackSets)} Stacksets",
|
365
363
|
end="\r",
|
366
364
|
) if verbose < 50 else ""
|
367
365
|
|
@@ -443,11 +441,11 @@ if __name__ == "__main__":
|
|
443
441
|
|
444
442
|
print(ERASE_LINE)
|
445
443
|
print(
|
446
|
-
f"
|
444
|
+
f"[red]Found {len(All_Results)} Stacksets across {len(AccountList)} accounts across {len(RegionList)} regions"
|
447
445
|
)
|
448
446
|
print()
|
449
447
|
if pTiming:
|
450
448
|
print(ERASE_LINE)
|
451
|
-
print(f"
|
449
|
+
print(f"[green]This script took {time() - begin_time:.2f} seconds")
|
452
450
|
print("Thanks for using this script...")
|
453
451
|
print()
|