runbooks 0.2.5__py3-none-any.whl → 0.7.0__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.
- conftest.py +26 -0
- jupyter-agent/.env +2 -0
- jupyter-agent/.env.template +2 -0
- jupyter-agent/.gitattributes +35 -0
- jupyter-agent/.gradio/certificate.pem +31 -0
- jupyter-agent/README.md +16 -0
- jupyter-agent/__main__.log +8 -0
- jupyter-agent/app.py +256 -0
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +154 -0
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +123 -0
- jupyter-agent/requirements.txt +9 -0
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +68 -0
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +91 -0
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +91 -0
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +57 -0
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +53 -0
- jupyter-agent/tmp/jupyter-agent.ipynb +27 -0
- jupyter-agent/utils.py +409 -0
- runbooks/__init__.py +71 -3
- runbooks/__main__.py +13 -0
- runbooks/aws/ec2_describe_instances.py +1 -1
- runbooks/aws/ec2_run_instances.py +8 -2
- runbooks/aws/ec2_start_stop_instances.py +17 -4
- runbooks/aws/ec2_unused_volumes.py +5 -1
- runbooks/aws/s3_create_bucket.py +4 -2
- runbooks/aws/s3_list_objects.py +6 -1
- runbooks/aws/tagging_lambda_handler.py +13 -2
- runbooks/aws/tags.json +12 -0
- runbooks/base.py +353 -0
- runbooks/cfat/README.md +49 -0
- runbooks/cfat/__init__.py +74 -0
- runbooks/cfat/app.ts +644 -0
- runbooks/cfat/assessment/__init__.py +40 -0
- runbooks/cfat/assessment/asana-import.csv +39 -0
- runbooks/cfat/assessment/cfat-checks.csv +31 -0
- runbooks/cfat/assessment/cfat.txt +520 -0
- runbooks/cfat/assessment/collectors.py +200 -0
- runbooks/cfat/assessment/jira-import.csv +39 -0
- runbooks/cfat/assessment/runner.py +387 -0
- runbooks/cfat/assessment/validators.py +290 -0
- runbooks/cfat/cli.py +103 -0
- runbooks/cfat/docs/asana-import.csv +24 -0
- runbooks/cfat/docs/cfat-checks.csv +31 -0
- runbooks/cfat/docs/cfat.txt +335 -0
- runbooks/cfat/docs/checks-output.png +0 -0
- runbooks/cfat/docs/cloudshell-console-run.png +0 -0
- runbooks/cfat/docs/cloudshell-download.png +0 -0
- runbooks/cfat/docs/cloudshell-output.png +0 -0
- runbooks/cfat/docs/downloadfile.png +0 -0
- runbooks/cfat/docs/jira-import.csv +24 -0
- runbooks/cfat/docs/open-cloudshell.png +0 -0
- runbooks/cfat/docs/report-header.png +0 -0
- runbooks/cfat/models.py +1026 -0
- runbooks/cfat/package-lock.json +5116 -0
- runbooks/cfat/package.json +38 -0
- runbooks/cfat/report.py +496 -0
- runbooks/cfat/reporting/__init__.py +46 -0
- runbooks/cfat/reporting/exporters.py +337 -0
- runbooks/cfat/reporting/formatters.py +496 -0
- runbooks/cfat/reporting/templates.py +135 -0
- runbooks/cfat/run-assessment.sh +23 -0
- runbooks/cfat/runner.py +69 -0
- runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
- runbooks/cfat/src/actions/check-config-existence.ts +37 -0
- runbooks/cfat/src/actions/check-control-tower.ts +37 -0
- runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
- runbooks/cfat/src/actions/check-iam-users.ts +50 -0
- runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
- runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
- runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
- runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
- runbooks/cfat/src/actions/create-backlog.ts +372 -0
- runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
- runbooks/cfat/src/actions/create-report.ts +616 -0
- runbooks/cfat/src/actions/define-account-type.ts +51 -0
- runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
- runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
- runbooks/cfat/src/actions/get-idc-info.ts +34 -0
- runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
- runbooks/cfat/src/actions/get-org-details.ts +35 -0
- runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
- runbooks/cfat/src/actions/get-org-ous.ts +35 -0
- runbooks/cfat/src/actions/get-regions.ts +22 -0
- runbooks/cfat/src/actions/zip-assessment.ts +27 -0
- runbooks/cfat/src/types/index.d.ts +147 -0
- runbooks/cfat/tests/__init__.py +141 -0
- runbooks/cfat/tests/test_cli.py +340 -0
- runbooks/cfat/tests/test_integration.py +290 -0
- runbooks/cfat/tests/test_models.py +505 -0
- runbooks/cfat/tests/test_reporting.py +354 -0
- runbooks/cfat/tsconfig.json +16 -0
- runbooks/cfat/webpack.config.cjs +27 -0
- runbooks/config.py +260 -0
- runbooks/finops/README.md +337 -0
- runbooks/finops/__init__.py +86 -0
- runbooks/finops/aws_client.py +245 -0
- runbooks/finops/cli.py +151 -0
- runbooks/finops/cost_processor.py +410 -0
- runbooks/finops/dashboard_runner.py +448 -0
- runbooks/finops/helpers.py +355 -0
- runbooks/finops/main.py +14 -0
- runbooks/finops/profile_processor.py +174 -0
- runbooks/finops/types.py +66 -0
- runbooks/finops/visualisations.py +80 -0
- runbooks/inventory/.gitignore +354 -0
- runbooks/inventory/ArgumentsClass.py +261 -0
- runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +619 -0
- runbooks/inventory/Inventory_Modules.py +6130 -0
- runbooks/inventory/LandingZone/delete_lz.py +1075 -0
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +738 -0
- runbooks/inventory/README.md +1320 -0
- runbooks/inventory/__init__.py +62 -0
- runbooks/inventory/account_class.py +532 -0
- runbooks/inventory/all_my_instances_wrapper.py +123 -0
- runbooks/inventory/aws_decorators.py +201 -0
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/cfn_move_stack_instances.py +1526 -0
- runbooks/inventory/check_cloudtrail_compliance.py +614 -0
- runbooks/inventory/check_controltower_readiness.py +1107 -0
- runbooks/inventory/check_landingzone_readiness.py +711 -0
- runbooks/inventory/cloudtrail.md +727 -0
- runbooks/inventory/collectors/__init__.py +20 -0
- runbooks/inventory/collectors/aws_compute.py +518 -0
- runbooks/inventory/collectors/aws_networking.py +275 -0
- runbooks/inventory/collectors/base.py +222 -0
- runbooks/inventory/core/__init__.py +19 -0
- runbooks/inventory/core/collector.py +303 -0
- runbooks/inventory/core/formatter.py +296 -0
- runbooks/inventory/delete_s3_buckets_objects.py +169 -0
- runbooks/inventory/discovery.md +81 -0
- runbooks/inventory/draw_org_structure.py +748 -0
- runbooks/inventory/ec2_vpc_utils.py +341 -0
- runbooks/inventory/find_cfn_drift_detection.py +272 -0
- runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
- runbooks/inventory/find_cfn_stackset_drift.py +733 -0
- runbooks/inventory/find_ec2_security_groups.py +669 -0
- runbooks/inventory/find_landingzone_versions.py +201 -0
- runbooks/inventory/find_vpc_flow_logs.py +1221 -0
- runbooks/inventory/inventory.sh +659 -0
- runbooks/inventory/list_cfn_stacks.py +558 -0
- runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
- runbooks/inventory/list_cfn_stackset_operations.py +734 -0
- runbooks/inventory/list_cfn_stacksets.py +453 -0
- runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
- runbooks/inventory/list_ds_directories.py +354 -0
- runbooks/inventory/list_ec2_availability_zones.py +286 -0
- runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
- runbooks/inventory/list_ec2_instances.py +425 -0
- runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
- runbooks/inventory/list_elbs_load_balancers.py +411 -0
- runbooks/inventory/list_enis_network_interfaces.py +526 -0
- runbooks/inventory/list_guardduty_detectors.py +568 -0
- runbooks/inventory/list_iam_policies.py +404 -0
- runbooks/inventory/list_iam_roles.py +518 -0
- runbooks/inventory/list_iam_saml_providers.py +359 -0
- runbooks/inventory/list_lambda_functions.py +882 -0
- runbooks/inventory/list_org_accounts.py +446 -0
- runbooks/inventory/list_org_accounts_users.py +354 -0
- runbooks/inventory/list_rds_db_instances.py +406 -0
- runbooks/inventory/list_route53_hosted_zones.py +318 -0
- runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
- runbooks/inventory/list_sns_topics.py +360 -0
- runbooks/inventory/list_ssm_parameters.py +402 -0
- runbooks/inventory/list_vpc_subnets.py +433 -0
- runbooks/inventory/list_vpcs.py +422 -0
- runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
- runbooks/inventory/models/__init__.py +24 -0
- runbooks/inventory/models/account.py +192 -0
- runbooks/inventory/models/inventory.py +309 -0
- runbooks/inventory/models/resource.py +247 -0
- runbooks/inventory/recover_cfn_stack_ids.py +205 -0
- runbooks/inventory/requirements.txt +12 -0
- runbooks/inventory/run_on_multi_accounts.py +211 -0
- runbooks/inventory/tests/common_test_data.py +3661 -0
- runbooks/inventory/tests/common_test_functions.py +204 -0
- runbooks/inventory/tests/setup.py +24 -0
- runbooks/inventory/tests/src.py +18 -0
- runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
- runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
- runbooks/inventory/tests/test_inventory_modules.py +55 -0
- runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
- runbooks/inventory/tests/test_moto_integration_example.py +273 -0
- runbooks/inventory/tests/test_org_list_accounts.py +49 -0
- runbooks/inventory/update_aws_actions.py +173 -0
- runbooks/inventory/update_cfn_stacksets.py +1215 -0
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
- runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
- runbooks/inventory/update_s3_public_access_block.py +539 -0
- runbooks/inventory/utils/__init__.py +23 -0
- runbooks/inventory/utils/aws_helpers.py +510 -0
- runbooks/inventory/utils/threading_utils.py +493 -0
- runbooks/inventory/utils/validation.py +682 -0
- runbooks/inventory/verify_ec2_security_groups.py +1430 -0
- runbooks/main.py +1004 -0
- runbooks/organizations/__init__.py +12 -0
- runbooks/organizations/manager.py +374 -0
- runbooks/security/README.md +447 -0
- runbooks/security/__init__.py +71 -0
- runbooks/{security_baseline → security}/checklist/alternate_contacts.py +8 -1
- runbooks/{security_baseline → security}/checklist/bucket_public_access.py +4 -1
- runbooks/{security_baseline → security}/checklist/cloudwatch_alarm_configuration.py +9 -2
- runbooks/{security_baseline → security}/checklist/guardduty_enabled.py +9 -2
- runbooks/{security_baseline → security}/checklist/multi_region_instance_usage.py +5 -1
- runbooks/{security_baseline → security}/checklist/root_access_key.py +6 -1
- runbooks/{security_baseline → security}/config-origin.json +1 -1
- runbooks/{security_baseline → security}/config.json +1 -1
- runbooks/{security_baseline → security}/permission.json +1 -1
- runbooks/{security_baseline → security}/report_generator.py +10 -2
- runbooks/{security_baseline → security}/report_template_en.html +7 -7
- runbooks/{security_baseline → security}/report_template_jp.html +7 -7
- runbooks/{security_baseline → security}/report_template_kr.html +12 -12
- runbooks/{security_baseline → security}/report_template_vn.html +7 -7
- runbooks/{security_baseline → security}/run_script.py +8 -2
- runbooks/{security_baseline → security}/security_baseline_tester.py +12 -4
- runbooks/{security_baseline → security}/utils/common.py +5 -1
- runbooks/utils/__init__.py +204 -0
- runbooks-0.7.0.dist-info/METADATA +375 -0
- runbooks-0.7.0.dist-info/RECORD +249 -0
- {runbooks-0.2.5.dist-info → runbooks-0.7.0.dist-info}/WHEEL +1 -1
- runbooks-0.7.0.dist-info/entry_points.txt +7 -0
- runbooks-0.7.0.dist-info/licenses/LICENSE +201 -0
- runbooks-0.7.0.dist-info/top_level.txt +3 -0
- runbooks/python101/calculator.py +0 -34
- runbooks/python101/config.py +0 -1
- runbooks/python101/exceptions.py +0 -16
- runbooks/python101/file_manager.py +0 -218
- runbooks/python101/toolkit.py +0 -153
- runbooks-0.2.5.dist-info/METADATA +0 -439
- runbooks-0.2.5.dist-info/RECORD +0 -61
- runbooks-0.2.5.dist-info/entry_points.txt +0 -3
- runbooks-0.2.5.dist-info/top_level.txt +0 -1
- /runbooks/{security_baseline/__init__.py → inventory/tests/script_test_data.py} +0 -0
- /runbooks/{security_baseline → security}/checklist/__init__.py +0 -0
- /runbooks/{security_baseline → security}/checklist/account_level_bucket_public_access.py +0 -0
- /runbooks/{security_baseline → security}/checklist/direct_attached_policy.py +0 -0
- /runbooks/{security_baseline → security}/checklist/iam_password_policy.py +0 -0
- /runbooks/{security_baseline → security}/checklist/iam_user_mfa.py +0 -0
- /runbooks/{security_baseline → security}/checklist/multi_region_trail.py +0 -0
- /runbooks/{security_baseline → security}/checklist/root_mfa.py +0 -0
- /runbooks/{security_baseline → security}/checklist/root_usage.py +0 -0
- /runbooks/{security_baseline → security}/checklist/trail_enabled.py +0 -0
- /runbooks/{security_baseline → security}/checklist/trusted_advisor.py +0 -0
- /runbooks/{security_baseline → security}/utils/__init__.py +0 -0
- /runbooks/{security_baseline → security}/utils/enums.py +0 -0
- /runbooks/{security_baseline → security}/utils/language.py +0 -0
- /runbooks/{security_baseline → security}/utils/level_const.py +0 -0
- /runbooks/{security_baseline → security}/utils/permission_list.py +0 -0
@@ -0,0 +1,575 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
"""
|
4
|
+
AWS Service Catalog Provisioned Products Discovery and Management Script
|
5
|
+
|
6
|
+
This script provides comprehensive discovery, analysis, and optional cleanup capabilities for
|
7
|
+
AWS Service Catalog provisioned products within single AWS accounts. It's designed for enterprise
|
8
|
+
cloud governance teams who need visibility into Service Catalog product lifecycle management,
|
9
|
+
CloudFormation stack reconciliation, and automated cleanup capabilities for failed or orphaned products.
|
10
|
+
|
11
|
+
Key Features:
|
12
|
+
- Service Catalog provisioned product discovery with comprehensive metadata extraction
|
13
|
+
- CloudFormation stack reconciliation and correlation analysis
|
14
|
+
- Automated detection of errored, tainted, or orphaned products
|
15
|
+
- Optional automated cleanup capabilities with safety controls
|
16
|
+
- Account status correlation and suspended account detection
|
17
|
+
- Fragment-based search for targeted product discovery and analysis
|
18
|
+
- Enterprise reporting with CSV export and structured output
|
19
|
+
|
20
|
+
Enterprise Use Cases:
|
21
|
+
- Service Catalog governance and product lifecycle management
|
22
|
+
- CloudFormation stack reconciliation and orphaned resource detection
|
23
|
+
- Automated cleanup of failed or errored provisioned products
|
24
|
+
- Account provisioning audit and compliance reporting
|
25
|
+
- Cost optimization through orphaned resource identification
|
26
|
+
- Service Catalog product portfolio analysis and governance
|
27
|
+
- Multi-account provisioning oversight and standardization
|
28
|
+
|
29
|
+
Service Catalog Product Analysis Features:
|
30
|
+
- Comprehensive provisioned product enumeration with status tracking
|
31
|
+
- CloudFormation stack correlation and dependency analysis
|
32
|
+
- Account creation tracking through Service Catalog account vending
|
33
|
+
- Product version and artifact analysis for governance oversight
|
34
|
+
- Error detection and automated remediation recommendations
|
35
|
+
- Orphaned product identification for cleanup and cost optimization
|
36
|
+
|
37
|
+
Security Considerations:
|
38
|
+
- Uses single profile authentication for focused Service Catalog operations
|
39
|
+
- Implements proper error handling for authorization failures
|
40
|
+
- Supports optional deletion operations with explicit safety controls
|
41
|
+
- Respects Service Catalog permissions and regional access constraints
|
42
|
+
- Provides comprehensive audit trail through detailed logging
|
43
|
+
- Sensitive provisioning information handling with appropriate access controls
|
44
|
+
|
45
|
+
Governance and Compliance Features:
|
46
|
+
- Service Catalog product lifecycle tracking for organizational oversight
|
47
|
+
- CloudFormation stack correlation for governance and compliance
|
48
|
+
- Account provisioning tracking through Service Catalog automation
|
49
|
+
- Product portfolio analysis for standardization and governance
|
50
|
+
- Error detection and remediation for operational excellence
|
51
|
+
|
52
|
+
Performance Considerations:
|
53
|
+
- Multi-threaded processing for concurrent CloudFormation stack reconciliation
|
54
|
+
- Progress tracking for operational visibility during large-scale operations
|
55
|
+
- Efficient credential management for Service Catalog operations
|
56
|
+
- Memory-optimized data structures for large product inventories
|
57
|
+
- Queue-based worker architecture for scalable stack reconciliation
|
58
|
+
|
59
|
+
Deletion Safety Features:
|
60
|
+
- Explicit deletion flags (+delete) for Service Catalog product cleanup
|
61
|
+
- Error and taint status detection for safe automated cleanup
|
62
|
+
- CloudFormation stack validation before product termination
|
63
|
+
- Comprehensive logging of deletion operations for audit trails
|
64
|
+
- Safety controls to prevent accidental product termination
|
65
|
+
|
66
|
+
Threading Architecture:
|
67
|
+
- Worker thread pool for concurrent CloudFormation stack reconciliation
|
68
|
+
- Queue-based task distribution for efficient stack discovery
|
69
|
+
- Thread-safe error handling and progress tracking
|
70
|
+
- Graceful degradation for stack access failures
|
71
|
+
|
72
|
+
Dependencies:
|
73
|
+
- boto3/botocore for AWS Service Catalog and CloudFormation API interactions
|
74
|
+
- account_class for AWS account access management
|
75
|
+
- ArgumentsClass for standardized CLI argument parsing
|
76
|
+
- Inventory_Modules for common utility functions and stack discovery
|
77
|
+
- colorama for enhanced output formatting
|
78
|
+
- tqdm for progress tracking
|
79
|
+
|
80
|
+
Future Enhancements:
|
81
|
+
- Multi-account Service Catalog product discovery across organizations
|
82
|
+
- Integration with AWS Config for product configuration monitoring
|
83
|
+
- Product optimization recommendations for cost and governance
|
84
|
+
- Automated product lifecycle management and policy enforcement
|
85
|
+
|
86
|
+
Author: AWS CloudOps Team
|
87
|
+
Version: 2023.08.09
|
88
|
+
"""
|
89
|
+
|
90
|
+
import logging
|
91
|
+
import sys
|
92
|
+
from queue import Queue
|
93
|
+
from threading import Thread
|
94
|
+
from time import time
|
95
|
+
|
96
|
+
import Inventory_Modules
|
97
|
+
from account_class import aws_acct_access
|
98
|
+
from ArgumentsClass import CommonArguments
|
99
|
+
from botocore.exceptions import ClientError, ProfileNotFound, UnknownCredentialError, UnknownRegionError
|
100
|
+
from colorama import Fore, init
|
101
|
+
from Inventory_Modules import display_results
|
102
|
+
from tqdm.auto import tqdm
|
103
|
+
|
104
|
+
init()
|
105
|
+
__version__ = "2023.08.09"
|
106
|
+
|
107
|
+
parser = CommonArguments()
|
108
|
+
parser.singleprofile()
|
109
|
+
parser.singleregion()
|
110
|
+
parser.extendedargs()
|
111
|
+
parser.save_to_file()
|
112
|
+
parser.verbosity()
|
113
|
+
parser.timing()
|
114
|
+
parser.version(__version__)
|
115
|
+
parser.my_parser.add_argument(
|
116
|
+
"+d",
|
117
|
+
"+delete",
|
118
|
+
dest="DeletionRun",
|
119
|
+
action="store_true",
|
120
|
+
help="This will delete the SC Provisioned Products found to be in error, or without active CloudFormation stacks - without any opportunity to confirm. Be careful!!",
|
121
|
+
)
|
122
|
+
parser.my_parser.add_argument(
|
123
|
+
"-f",
|
124
|
+
"--fragment",
|
125
|
+
dest="Fragment",
|
126
|
+
metavar="Fragment of the Product name or the product id",
|
127
|
+
default=None,
|
128
|
+
help="Unique fragment of the product name or the product id",
|
129
|
+
)
|
130
|
+
parser.my_parser.add_argument(
|
131
|
+
"-e",
|
132
|
+
"--exact",
|
133
|
+
dest="Exact",
|
134
|
+
action="store_true",
|
135
|
+
help="Use this flag to make sure that ONLY the string you specified will be identified",
|
136
|
+
)
|
137
|
+
args = parser.my_parser.parse_args()
|
138
|
+
|
139
|
+
pProfile = args.Profile
|
140
|
+
pRegion = args.Region
|
141
|
+
pTiming = args.Time
|
142
|
+
pFileName = args.Filename
|
143
|
+
pFragment = args.Fragment
|
144
|
+
pExact = args.Exact
|
145
|
+
verbose = args.loglevel
|
146
|
+
DeletionRun = args.DeletionRun
|
147
|
+
# Setup logging levels
|
148
|
+
logging.basicConfig(level=verbose, format="[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s")
|
149
|
+
logging.getLogger("boto3").setLevel(logging.CRITICAL)
|
150
|
+
logging.getLogger("botocore").setLevel(logging.CRITICAL)
|
151
|
+
logging.getLogger("s3transfer").setLevel(logging.CRITICAL)
|
152
|
+
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
|
153
|
+
logging.getLogger("botocore").setLevel(logging.CRITICAL)
|
154
|
+
|
155
|
+
|
156
|
+
##########################
|
157
|
+
|
158
|
+
|
159
|
+
def sort_by_email(elem):
|
160
|
+
return elem("AccountEmail")
|
161
|
+
|
162
|
+
|
163
|
+
def find_account_stacksets(faws_acct, f_SCProducts, fRegion=None, fstacksetname=None):
|
164
|
+
"""
|
165
|
+
Note that this function takes a list of Credentials and checks for stacks in every account it has creds for
|
166
|
+
"""
|
167
|
+
|
168
|
+
class CheckProducts(Thread):
|
169
|
+
def __init__(self, queue):
|
170
|
+
Thread.__init__(self)
|
171
|
+
self.queue = queue
|
172
|
+
|
173
|
+
def run(self):
|
174
|
+
while True:
|
175
|
+
# Get the work from the queue and expand the tuple
|
176
|
+
c_sc_product, c_region, c_fstacksetname, c_PlacesToLook, c_PlaceCount = self.queue.get()
|
177
|
+
logging.info(f"De-queued info for SC Product: {c_sc_product['SCPName']}")
|
178
|
+
logging.info(f"{Fore.RED}Checking {PlaceCount} of {len(f_SCProducts)} products{Fore.RESET}")
|
179
|
+
CFNresponse = Inventory_Modules.find_stacks3(faws_acct, pRegion, c_sc_product["SCPId"])
|
180
|
+
logging.info(
|
181
|
+
f"There are {len(CFNresponse)} matches for SC Provisioned Product Name {c_sc_product['SCPName']}"
|
182
|
+
)
|
183
|
+
try:
|
184
|
+
if len(CFNresponse) > 0:
|
185
|
+
stack_info = client_cfn.describe_stacks(StackName=CFNresponse[0]["StackName"])
|
186
|
+
# The above command fails if the stack found (by the find_stacks3 function) has been deleted
|
187
|
+
# The following section determines the NEW Account's AccountEmail and AccountID
|
188
|
+
AccountEmail = AccountID = AccountStatus = None
|
189
|
+
if (
|
190
|
+
"Parameters" in stack_info["Stacks"][0].keys()
|
191
|
+
and len(stack_info["Stacks"][0]["Parameters"]) > 0
|
192
|
+
):
|
193
|
+
for y in range(len(stack_info["Stacks"][0]["Parameters"])):
|
194
|
+
if stack_info["Stacks"][0]["Parameters"][y]["ParameterKey"] == "AccountEmail":
|
195
|
+
AccountEmail = stack_info["Stacks"][0]["Parameters"][y]["ParameterValue"]
|
196
|
+
logging.info(f"Account Email is {AccountEmail}")
|
197
|
+
if "Outputs" in stack_info["Stacks"][0].keys():
|
198
|
+
for y in range(len(stack_info["Stacks"][0]["Outputs"])):
|
199
|
+
logging.info(
|
200
|
+
f"Output Key {stack_info['Stacks'][0]['Outputs'][y]['OutputKey']} "
|
201
|
+
f"for stack {CFNresponse[0]['StackName']} is {stack_info['Stacks'][0]['Outputs'][y]['OutputValue']}"
|
202
|
+
)
|
203
|
+
if stack_info["Stacks"][0]["Outputs"][y]["OutputKey"] == "AccountID":
|
204
|
+
AccountID = stack_info["Stacks"][0]["Outputs"][y]["OutputValue"]
|
205
|
+
if AccountID in AccountHistogram.keys():
|
206
|
+
AccountStatus = AccountHistogram[AccountID]
|
207
|
+
else:
|
208
|
+
AccountStatus = "Closed"
|
209
|
+
logging.info(f"{Fore.RED}Found the Account ID: {AccountID}{Fore.RESET}")
|
210
|
+
if AccountID in SuspendedAccounts:
|
211
|
+
logging.error(
|
212
|
+
f"{Fore.RED}Account ID {AccountID} has been suspended{Fore.RESET}"
|
213
|
+
)
|
214
|
+
break
|
215
|
+
else:
|
216
|
+
logging.error("Outputs key present, but no account ID")
|
217
|
+
AccountID = None
|
218
|
+
AccountStatus = None
|
219
|
+
else:
|
220
|
+
logging.error("No Outputs key present")
|
221
|
+
AccountID = "None"
|
222
|
+
AccountStatus = "None"
|
223
|
+
CFNStackName = CFNresponse[0]["StackName"]
|
224
|
+
CFNStackStatus = CFNresponse[0]["StackStatus"]
|
225
|
+
# AccountEmail should have been assigned in the 'Parameters' if-then above
|
226
|
+
# AccountID should have been assigned in the 'Outputs' if-then above
|
227
|
+
# AccountStatus should have been assigned in the 'Outputs' if-then above
|
228
|
+
else: # This takes effect when CFNResponse can't find any stacks with the Service Catalog Product ID
|
229
|
+
CFNStackName = CFNStackStatus = AccountID = AccountEmail = AccountStatus = None
|
230
|
+
logging.error(
|
231
|
+
f"AccountID: {AccountID} | AccountEmail: {AccountEmail} | CFNStackName: {CFNStackName} | CFNStackStatus: {CFNStackStatus} | SC Product: {c_sc_product['SCPName']}"
|
232
|
+
)
|
233
|
+
SCP2Stacks.append(
|
234
|
+
{
|
235
|
+
"SCProductName": c_sc_product["SCPName"],
|
236
|
+
"SCProductId": c_sc_product["SCPId"],
|
237
|
+
"SCStatus": c_sc_product["SCPStatus"],
|
238
|
+
"ProvisioningArtifactName": c_sc_product["ProvisioningArtifactName"],
|
239
|
+
"CFNStackName": CFNStackName,
|
240
|
+
"CFNStackStatus": CFNStackStatus,
|
241
|
+
"AccountEmail": AccountEmail,
|
242
|
+
"AccountID": AccountID,
|
243
|
+
"AccountStatus": AccountStatus,
|
244
|
+
}
|
245
|
+
)
|
246
|
+
except ClientError as my_Error:
|
247
|
+
if str(my_Error).find("ValidationError") > 0:
|
248
|
+
print("Validation Failure ")
|
249
|
+
print(
|
250
|
+
f"Validation Failure in profile {pProfile} looking for stack {CFNresponse[0]['StackName']} with status of {CFNresponse[0]['StackStatus']}"
|
251
|
+
)
|
252
|
+
elif str(my_Error).find("AccessDenied") > 0:
|
253
|
+
print(f"{pProfile}: Access Denied Failure ")
|
254
|
+
else:
|
255
|
+
print(f"{pProfile}: Other kind of failure ")
|
256
|
+
print(my_Error)
|
257
|
+
finally:
|
258
|
+
logging.info(
|
259
|
+
f"Finished finding product {c_sc_product['SCPName']} - {c_PlaceCount} / {c_PlacesToLook}"
|
260
|
+
)
|
261
|
+
pbar.update()
|
262
|
+
self.queue.task_done()
|
263
|
+
|
264
|
+
if fRegion is None:
|
265
|
+
fRegion = ["us-east-1"]
|
266
|
+
checkqueue = Queue()
|
267
|
+
|
268
|
+
# AllSubnets = []
|
269
|
+
PlaceCount = 0
|
270
|
+
PlacesToLook = WorkerThreads = min(len(f_SCProducts), 10)
|
271
|
+
|
272
|
+
pbar = tqdm(
|
273
|
+
desc=f"Reconciling SC Products with CloudFormation Stacks in accounts",
|
274
|
+
leave=True,
|
275
|
+
total=len(f_SCProducts),
|
276
|
+
unit=" products",
|
277
|
+
)
|
278
|
+
|
279
|
+
# Create and start the worker threads
|
280
|
+
for x in range(WorkerThreads):
|
281
|
+
worker = CheckProducts(checkqueue)
|
282
|
+
# Setting daemon to True will let the main thread exit even though the workers are blocking
|
283
|
+
worker.daemon = True
|
284
|
+
worker.start()
|
285
|
+
|
286
|
+
for SCProduct in SCProducts:
|
287
|
+
logging.info(f"Checking service catalog product: {SCProduct['SCPName']}")
|
288
|
+
try:
|
289
|
+
# print(f"{ERASE_LINE}Queuing account {credential['AccountId']} in region {region}", end='\r')
|
290
|
+
checkqueue.put((SCProduct, fRegion, fstacksetname, PlacesToLook, PlaceCount))
|
291
|
+
PlaceCount += 1
|
292
|
+
except ClientError as my_Error:
|
293
|
+
if "AuthFailure" in str(my_Error):
|
294
|
+
logging.error(f"Authorization Failure accessing account {faws_acct.acct_number} in {fRegion} region")
|
295
|
+
logging.warning(f"It's possible that the region {fRegion} hasn't been opted-into")
|
296
|
+
pass
|
297
|
+
checkqueue.join()
|
298
|
+
pbar.close()
|
299
|
+
return SCP2Stacks
|
300
|
+
|
301
|
+
|
302
|
+
##########################
|
303
|
+
|
304
|
+
|
305
|
+
"""
|
306
|
+
TODO:
|
307
|
+
- Need to add a parameter that allows the user to close / terminate the SC products for only the closed / suspended accounts (very safe)
|
308
|
+
- Add a parameter that refuses to close/ terminate accounts with active VPCs?
|
309
|
+
"""
|
310
|
+
|
311
|
+
"""
|
312
|
+
Significant Variable Explanation:
|
313
|
+
'AcctList' holds a list of accounts within this Org.
|
314
|
+
'SCresponse' holds a native list of Service Catalog Provisioned Products supplied by the native API.
|
315
|
+
'SCProducts' holds a refined list of the Service Catalog Provisioned Products from the 'SCresponse' list, but only the fields we're interested in.
|
316
|
+
** TODO: I duplicated this listing, in case later I decided to add some additional useful fields to the dict.
|
317
|
+
'SCP2Stacks' holds a list of the CloudFormation Stacks in this account that *match* the Provisioned Products.
|
318
|
+
** TODO: This list should hold *all* stacks and then we could find stacks for accounts that no longer exist.
|
319
|
+
'AccountHistogram' holds the list of accounts (the account numbers are the keys in this dict) and the number of SC products that are created for this account is the value of that key.
|
320
|
+
"""
|
321
|
+
begin_time = time()
|
322
|
+
|
323
|
+
ERASE_LINE = "\x1b[2K"
|
324
|
+
|
325
|
+
print()
|
326
|
+
|
327
|
+
SCP2Stacks = []
|
328
|
+
SCProducts = []
|
329
|
+
try:
|
330
|
+
if pProfile is None:
|
331
|
+
aws_acct = aws_acct_access()
|
332
|
+
else:
|
333
|
+
aws_acct = aws_acct_access(fProfile=pProfile, fRegion=pRegion)
|
334
|
+
if aws_acct.ErrorType is not None and aws_acct.ErrorType.lower() == "invalid region":
|
335
|
+
raise UnknownRegionError(region_name=pRegion, error_msg=aws_acct.ErrorType)
|
336
|
+
session_aws = aws_acct.session
|
337
|
+
except UnknownCredentialError as my_Error:
|
338
|
+
print(
|
339
|
+
f"Single profile '{pProfile}' doesn't seem to exist, or work. Please check your credentials-1.\n"
|
340
|
+
f"Error Message: {my_Error}"
|
341
|
+
)
|
342
|
+
print()
|
343
|
+
sys.exit(1)
|
344
|
+
except ProfileNotFound as my_Error:
|
345
|
+
print(
|
346
|
+
f"Single profile '{pProfile}' doesn't seem to exist, or work. Please check your credentials-1.\n"
|
347
|
+
f"Error Message: {my_Error}"
|
348
|
+
)
|
349
|
+
print()
|
350
|
+
sys.exit(1)
|
351
|
+
except AttributeError as my_Error:
|
352
|
+
print(
|
353
|
+
f"Single profile '{pProfile}' doesn't seem to exist, or work. Please check your credentials-2.\n"
|
354
|
+
f"Error Message: {my_Error}"
|
355
|
+
)
|
356
|
+
print()
|
357
|
+
sys.exit(1)
|
358
|
+
except (ConnectionError, UnknownRegionError) as my_Error:
|
359
|
+
print(
|
360
|
+
f"There is no single region '{pRegion}'. Please re-run and specify a valid AWS region for this Org account.\n"
|
361
|
+
f"Error Message: {my_Error}"
|
362
|
+
)
|
363
|
+
print()
|
364
|
+
sys.exit(1)
|
365
|
+
client_org = aws_acct.session.client("organizations")
|
366
|
+
client_cfn = aws_acct.session.client("cloudformation")
|
367
|
+
|
368
|
+
AccountHistogram = {}
|
369
|
+
SuspendedAccounts = []
|
370
|
+
ClosedAccts = []
|
371
|
+
|
372
|
+
|
373
|
+
def main():
|
374
|
+
client_sc = aws_acct.session.client("servicecatalog")
|
375
|
+
ErroredSCProductExists = False
|
376
|
+
# Creates AccountHistogram which tracks which accounts have SC Products built for them, and which do not.
|
377
|
+
# The Histogram is initialized with ALL accounts and their status.
|
378
|
+
# It's then later overwritten with the count of how many SC Products relate to that account.
|
379
|
+
# So the accounts which only have a status as their value - are missing the SC Product
|
380
|
+
for account in aws_acct.ChildAccounts:
|
381
|
+
AccountHistogram[account["AccountId"]] = account["AccountStatus"]
|
382
|
+
if account["AccountStatus"] == "SUSPENDED":
|
383
|
+
SuspendedAccounts.append(account["AccountId"])
|
384
|
+
|
385
|
+
# Find the provisioned product ids
|
386
|
+
AVM_prod_id = None
|
387
|
+
if pFragment is None:
|
388
|
+
result = client_sc.search_products_as_admin()
|
389
|
+
prod_ids = result["ProductViewDetails"]
|
390
|
+
# while 'NextPageToken' in result.keys() and 'NextPageToken' is not None:
|
391
|
+
# result = client_sc.search_products_as_admin()
|
392
|
+
# prod_ids.extend(result['ProductViewDetails'])
|
393
|
+
# if verbose < 50:
|
394
|
+
# print(f"{ERASE_LINE}Found {len(result['ProductViewDetails'])} products | Total found: {len(prod_ids)}", end='\r')
|
395
|
+
# print()
|
396
|
+
for product in prod_ids:
|
397
|
+
if product["ProductViewSummary"]["Name"].find("Account-Vending-Machine") > 0:
|
398
|
+
AVM_prod_id = product["ProductViewSummary"]["ProductId"]
|
399
|
+
elif pFragment is not None and not pExact:
|
400
|
+
result = client_sc.search_products_as_admin()
|
401
|
+
prod_ids = result["ProductViewDetails"]
|
402
|
+
# while 'NextPageToken' in result.keys() and 'NextPageToken' is not None:
|
403
|
+
# result = client_sc.search_products_as_admin()
|
404
|
+
# prod_ids.extend(result['ProductViewDetails'])
|
405
|
+
# if verbose < 50:
|
406
|
+
# print(f"{ERASE_LINE}Found {len(result['ProductViewDetails'])} products | Total found: {len(prod_ids)}", end='\r')
|
407
|
+
for product in prod_ids:
|
408
|
+
if (
|
409
|
+
product["ProductViewSummary"]["Name"].find(pFragment) > -1
|
410
|
+
or product["ProductViewSummary"]["ProductId"].find(pFragment) > -1
|
411
|
+
):
|
412
|
+
AVM_prod_id = product["ProductViewSummary"]["ProductId"]
|
413
|
+
elif pFragment is not None and pExact:
|
414
|
+
AVM_prod_id = pFragment
|
415
|
+
|
416
|
+
# Finds Service Catalog Products and reconciles them to the account they belong to
|
417
|
+
try:
|
418
|
+
# The function below takes the parent account (object),
|
419
|
+
# the stack statuses we're trying to find, the number of products to find at once, and possibly the product id (if provided)
|
420
|
+
SCresponse = Inventory_Modules.find_sc_products3(aws_acct, "All", 10, AVM_prod_id)
|
421
|
+
logging.warning("A list of the SC Products found:")
|
422
|
+
for i in range(len(SCresponse)):
|
423
|
+
logging.warning(f"SC Product Name {SCresponse[i]['Name']} | SC Product Status {SCresponse[i]['Status']}")
|
424
|
+
SCProducts.append(
|
425
|
+
{
|
426
|
+
"SCPName": SCresponse[i]["Name"],
|
427
|
+
"SCPId": SCresponse[i]["Id"],
|
428
|
+
"SCPStatus": SCresponse[i]["Status"],
|
429
|
+
"SCPRecordId": SCresponse[i]["LastRecordId"],
|
430
|
+
"ProvisioningArtifactName": SCresponse[i]["ProvisioningArtifactName"],
|
431
|
+
}
|
432
|
+
)
|
433
|
+
if SCresponse[i]["Status"] == "ERROR" or SCresponse[i]["Status"] == "TAINTED":
|
434
|
+
ErroredSCProductExists = True
|
435
|
+
|
436
|
+
CFNStacks = Inventory_Modules.find_stacks3(aws_acct, pRegion, f"SC-{aws_acct.acct_number}")
|
437
|
+
|
438
|
+
if pTiming:
|
439
|
+
print(
|
440
|
+
f"{Fore.GREEN}Finding stacks in your account has taken {time() - begin_time:.2f} seconds now...{Fore.RESET}"
|
441
|
+
)
|
442
|
+
milestone1 = time()
|
443
|
+
|
444
|
+
SCresponse = None
|
445
|
+
|
446
|
+
# TODO: Create a queue - place the SCProducts on that queue, one by one, and let this code run in a multi-thread worker
|
447
|
+
# def find_account_stacksets(faws_acct, f_SCProducts, fRegion=None, fstacksetname=None):
|
448
|
+
CFNresponse = find_account_stacksets(aws_acct, SCProducts, pRegion, pFragment)
|
449
|
+
|
450
|
+
# TODO: We should list out Suspended accounts in the SCP2Stacks readout at the end - in case any accounts have
|
451
|
+
# both a provisioned product, but are also suspended.
|
452
|
+
# Do any of the account numbers show up more than once in this list?
|
453
|
+
# We initialize the listing from the full list of accounts in the Org.
|
454
|
+
|
455
|
+
if pTiming:
|
456
|
+
print(
|
457
|
+
f"{Fore.GREEN}Reconciling products to the CloudFormation stacks took {time() - milestone1:.2f} seconds{Fore.RESET}"
|
458
|
+
)
|
459
|
+
|
460
|
+
# TODO: This might not be a good idea, if it misses the stacks which are associated with accounts no longer within the Org.
|
461
|
+
# We add a one to each account which is represented within the Stacks listing. This allows us to catch duplicates
|
462
|
+
# and also accounts which do not have a stack associated.
|
463
|
+
# Note it does *not* help us catch stacks associated with an account that's been removed.
|
464
|
+
for i in range(len(CFNresponse)):
|
465
|
+
if CFNresponse[i]["AccountID"] == "None":
|
466
|
+
continue
|
467
|
+
elif not CFNresponse[i]["AccountID"] in AccountHistogram.keys():
|
468
|
+
CFNresponse[i]["AccountStatus"] = "CLOSED"
|
469
|
+
ClosedAccts.append(CFNresponse[i]["AccountID"])
|
470
|
+
else:
|
471
|
+
# This means that the value is still either "ACTIVE" or "SUSPENDED"
|
472
|
+
if isinstance(AccountHistogram[CFNresponse[i]["AccountID"]], str):
|
473
|
+
AccountHistogram[CFNresponse[i]["AccountID"]] = 1
|
474
|
+
else:
|
475
|
+
AccountHistogram[CFNresponse[i]["AccountID"]] += 1
|
476
|
+
|
477
|
+
display_dict = {
|
478
|
+
"AccountID": {"DisplayOrder": 1, "Heading": "Account Id", "Condition": ["None"]},
|
479
|
+
"SCProductName": {"DisplayOrder": 2, "Heading": "SC Product Name"},
|
480
|
+
"ProvisioningArtifactName": {"DisplayOrder": 3, "Heading": "Version"},
|
481
|
+
"CFNStackName": {"DisplayOrder": 4, "Heading": "CFN Stack Name"},
|
482
|
+
"SCStatus": {"DisplayOrder": 5, "Heading": "SC Status", "Condition": ["ERROR", "TAINTED"]},
|
483
|
+
"CFNStackStatus": {"DisplayOrder": 6, "Heading": "CFN Stack Status"},
|
484
|
+
"AccountStatus": {"DisplayOrder": 7, "Heading": "Account Status", "Condition": ["CLOSED"]},
|
485
|
+
"AccountEmail": {"DisplayOrder": 8, "Heading": "Account Email"},
|
486
|
+
}
|
487
|
+
display_results(CFNresponse, display_dict, "None", pFileName)
|
488
|
+
|
489
|
+
except ClientError as my_Error:
|
490
|
+
if "AuthFailure" in str(my_Error):
|
491
|
+
print(f"{pProfile}: Authorization Failure ")
|
492
|
+
elif str(my_Error).find("AccessDenied") > 0:
|
493
|
+
print(f"{pProfile}: Access Denied Failure ")
|
494
|
+
else:
|
495
|
+
print(f"{pProfile}: Other kind of failure ")
|
496
|
+
print(my_Error)
|
497
|
+
|
498
|
+
print()
|
499
|
+
for acctnum in AccountHistogram.keys():
|
500
|
+
if AccountHistogram[acctnum] == 1:
|
501
|
+
pass # This is the desired state, so no user output is needed.
|
502
|
+
elif AccountHistogram[acctnum] == "SUSPENDED":
|
503
|
+
print(
|
504
|
+
f"{Fore.RED}While there is no SC Product associated, account number {acctnum} appears to be a suspended account.{Fore.RESET}"
|
505
|
+
)
|
506
|
+
elif (
|
507
|
+
AccountHistogram[acctnum] == "ACTIVE"
|
508
|
+
): # This compare needs to be separate from below, since we can't compare a string with a "<" operator
|
509
|
+
print(
|
510
|
+
f"Account Number {Fore.RED}{acctnum}{Fore.RESET} appears to have no SC Product associated with it. This can be a problem"
|
511
|
+
)
|
512
|
+
elif AccountHistogram[acctnum] < 1:
|
513
|
+
print(
|
514
|
+
f"Account Number {Fore.RED}{acctnum}{Fore.RESET} appears to have no SC Product associated with it. This can be a problem"
|
515
|
+
)
|
516
|
+
elif AccountHistogram[acctnum] > 1:
|
517
|
+
print(
|
518
|
+
f"Account Number {Fore.RED}{acctnum}{Fore.RESET} appears to have multiple SC Products associated with it. This can be a problem"
|
519
|
+
)
|
520
|
+
|
521
|
+
if ErroredSCProductExists:
|
522
|
+
print()
|
523
|
+
print("You probably want to remove the following SC Products:")
|
524
|
+
session_sc = aws_acct.session
|
525
|
+
client_sc = session_sc.client("servicecatalog")
|
526
|
+
for i in range(len(CFNresponse)):
|
527
|
+
if (
|
528
|
+
(CFNresponse[i]["SCStatus"] == "ERROR") or (CFNresponse[i]["CFNStackName"] == "None")
|
529
|
+
) and not DeletionRun:
|
530
|
+
print(
|
531
|
+
f"aws servicecatalog terminate-provisioned-product --provisioned-product-id {CFNresponse[i]['SCProductId']} --ignore-errors",
|
532
|
+
end="",
|
533
|
+
)
|
534
|
+
# Finishes the line for display, based on whether they used a profile or not to run this command
|
535
|
+
if pProfile is None:
|
536
|
+
print()
|
537
|
+
elif pProfile is not None:
|
538
|
+
print(f" --profile {pProfile}")
|
539
|
+
elif (
|
540
|
+
(CFNresponse[i]["SCStatus"] == "ERROR") or (CFNresponse[i]["CFNStackName"] == "None")
|
541
|
+
) and DeletionRun:
|
542
|
+
print(
|
543
|
+
f"Deleting Service Catalog Provisioned Product {CFNresponse[i]['SCProductName']} from account {aws_acct.acct_number}"
|
544
|
+
)
|
545
|
+
StackDelete = client_sc.terminate_provisioned_product(
|
546
|
+
ProvisionedProductId=CFNresponse[i]["SCProductId"],
|
547
|
+
IgnoreErrors=True,
|
548
|
+
)
|
549
|
+
logging.error("Result of Deletion: %s", StackDelete["RecordDetail"]["Status"])
|
550
|
+
if len(StackDelete["RecordDetail"]["RecordErrors"]) > 0:
|
551
|
+
logging.error("Error code: %s", StackDelete["RecordDetail"]["RecordErrors"][0]["Code"])
|
552
|
+
logging.error(
|
553
|
+
"Error description: %s", StackDelete["RecordDetail"]["RecordErrors"][0]["Description"]
|
554
|
+
)
|
555
|
+
|
556
|
+
print()
|
557
|
+
for i in AccountHistogram:
|
558
|
+
logging.info(
|
559
|
+
f"There are {AccountHistogram[i] if isinstance(AccountHistogram[i], int) else '0'} active SC products for Account ID: {i}"
|
560
|
+
)
|
561
|
+
end_time = time()
|
562
|
+
duration = end_time - begin_time
|
563
|
+
if pTiming:
|
564
|
+
print(f"{Fore.GREEN}This script took {duration:.2f} seconds{Fore.RESET}")
|
565
|
+
print(f"We found {len(aws_acct.ChildAccounts)} accounts within the Org")
|
566
|
+
print(f"We found {len(SCProducts)} Service Catalog Products")
|
567
|
+
print(f"We found {len(SuspendedAccounts)} Suspended accounts")
|
568
|
+
print(f"We found {len(ClosedAccts)} Closed Accounts that still have an SC product")
|
569
|
+
# print("We found {} Service Catalog Products with no account attached".format('Some Number'))
|
570
|
+
print("Thanks for using this script...")
|
571
|
+
print()
|
572
|
+
|
573
|
+
|
574
|
+
if __name__ == "__main__":
|
575
|
+
main()
|