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,192 @@
|
|
1
|
+
"""
|
2
|
+
AWS account models for inventory operations.
|
3
|
+
|
4
|
+
This module provides Pydantic models representing AWS accounts and
|
5
|
+
organization structures with proper validation and type safety.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from dataclasses import dataclass, field
|
9
|
+
from datetime import datetime
|
10
|
+
from enum import Enum
|
11
|
+
from typing import Dict, List, Optional, Set
|
12
|
+
|
13
|
+
from pydantic import BaseModel, Field, validator
|
14
|
+
|
15
|
+
|
16
|
+
class AccountStatus(str, Enum):
|
17
|
+
"""AWS account status enumeration."""
|
18
|
+
|
19
|
+
ACTIVE = "ACTIVE"
|
20
|
+
SUSPENDED = "SUSPENDED"
|
21
|
+
PENDING_CLOSURE = "PENDING_CLOSURE"
|
22
|
+
|
23
|
+
|
24
|
+
class OrganizationUnitType(str, Enum):
|
25
|
+
"""Organization unit type enumeration."""
|
26
|
+
|
27
|
+
ROOT = "ROOT"
|
28
|
+
ORGANIZATIONAL_UNIT = "ORGANIZATIONAL_UNIT"
|
29
|
+
|
30
|
+
|
31
|
+
@dataclass
|
32
|
+
class AWSCredentials:
|
33
|
+
"""AWS credentials for account access."""
|
34
|
+
|
35
|
+
access_key_id: Optional[str] = None
|
36
|
+
secret_access_key: Optional[str] = None
|
37
|
+
session_token: Optional[str] = None
|
38
|
+
profile_name: Optional[str] = None
|
39
|
+
role_arn: Optional[str] = None
|
40
|
+
external_id: Optional[str] = None
|
41
|
+
|
42
|
+
def __post_init__(self):
|
43
|
+
"""Validate credentials configuration."""
|
44
|
+
has_keys = self.access_key_id and self.secret_access_key
|
45
|
+
has_profile = self.profile_name
|
46
|
+
has_role = self.role_arn
|
47
|
+
|
48
|
+
if not any([has_keys, has_profile, has_role]):
|
49
|
+
raise ValueError("Must provide either access keys, profile name, or role ARN")
|
50
|
+
|
51
|
+
|
52
|
+
class AWSAccount(BaseModel):
|
53
|
+
"""
|
54
|
+
Represents an AWS account with metadata and access information.
|
55
|
+
|
56
|
+
This model provides a comprehensive representation of an AWS account
|
57
|
+
including organization context, contact information, and access credentials.
|
58
|
+
"""
|
59
|
+
|
60
|
+
account_id: str = Field(..., pattern=r"^\d{12}$", description="12-digit AWS account ID")
|
61
|
+
|
62
|
+
account_name: Optional[str] = Field(None, description="Human-readable account name")
|
63
|
+
|
64
|
+
email: Optional[str] = Field(None, description="Root email address for the account")
|
65
|
+
|
66
|
+
status: AccountStatus = Field(AccountStatus.ACTIVE, description="Current account status")
|
67
|
+
|
68
|
+
creation_date: Optional[datetime] = Field(None, description="Account creation timestamp")
|
69
|
+
|
70
|
+
# Organization information
|
71
|
+
organization_id: Optional[str] = Field(None, description="AWS Organizations ID if account is in an organization")
|
72
|
+
|
73
|
+
organizational_unit_id: Optional[str] = Field(None, description="Organizational Unit ID containing this account")
|
74
|
+
|
75
|
+
organizational_unit_name: Optional[str] = Field(None, description="Organizational Unit name")
|
76
|
+
|
77
|
+
is_management_account: bool = Field(False, description="Whether this is the organization management account")
|
78
|
+
|
79
|
+
# Access and permissions
|
80
|
+
default_region: str = Field("us-east-1", description="Default AWS region for operations")
|
81
|
+
|
82
|
+
available_regions: Set[str] = Field(default_factory=set, description="Set of available/enabled regions")
|
83
|
+
|
84
|
+
# Metadata
|
85
|
+
tags: Dict[str, str] = Field(default_factory=dict, description="Account-level tags")
|
86
|
+
|
87
|
+
discovered_services: Set[str] = Field(default_factory=set, description="AWS services discovered in this account")
|
88
|
+
|
89
|
+
last_inventory_date: Optional[datetime] = Field(None, description="Last successful inventory collection timestamp")
|
90
|
+
|
91
|
+
# Cost and billing (if available)
|
92
|
+
monthly_cost_estimate: Optional[float] = Field(None, description="Monthly cost estimate in USD")
|
93
|
+
|
94
|
+
cost_center: Optional[str] = Field(None, description="Cost center or billing code")
|
95
|
+
|
96
|
+
class Config:
|
97
|
+
"""Pydantic configuration."""
|
98
|
+
|
99
|
+
use_enum_values = True
|
100
|
+
extra = "forbid"
|
101
|
+
json_encoders = {datetime: lambda v: v.isoformat(), set: lambda v: list(v)}
|
102
|
+
|
103
|
+
@validator("account_id")
|
104
|
+
def validate_account_id(cls, v):
|
105
|
+
"""Validate account ID format."""
|
106
|
+
if not v.isdigit() or len(v) != 12:
|
107
|
+
raise ValueError("Account ID must be exactly 12 digits")
|
108
|
+
return v
|
109
|
+
|
110
|
+
@validator("available_regions")
|
111
|
+
def validate_regions(cls, v):
|
112
|
+
"""Validate region format."""
|
113
|
+
valid_region_pattern = r"^[a-z]{2,3}-[a-z]+-\d+$"
|
114
|
+
import re
|
115
|
+
|
116
|
+
for region in v:
|
117
|
+
if not re.match(valid_region_pattern, region):
|
118
|
+
raise ValueError(f"Invalid region format: {region}")
|
119
|
+
return v
|
120
|
+
|
121
|
+
def is_in_organization(self) -> bool:
|
122
|
+
"""Check if account is part of an AWS Organization."""
|
123
|
+
return self.organization_id is not None
|
124
|
+
|
125
|
+
def add_discovered_service(self, service: str) -> None:
|
126
|
+
"""Add a service to the discovered services set."""
|
127
|
+
self.discovered_services.add(service)
|
128
|
+
|
129
|
+
def get_region_count(self) -> int:
|
130
|
+
"""Get the number of available regions."""
|
131
|
+
return len(self.available_regions)
|
132
|
+
|
133
|
+
def to_dict(self) -> Dict:
|
134
|
+
"""Convert to dictionary representation."""
|
135
|
+
return self.dict()
|
136
|
+
|
137
|
+
def __str__(self) -> str:
|
138
|
+
"""String representation."""
|
139
|
+
name = self.account_name or "Unnamed"
|
140
|
+
return f"AWSAccount({self.account_id}, {name})"
|
141
|
+
|
142
|
+
|
143
|
+
class OrganizationAccount(AWSAccount):
|
144
|
+
"""
|
145
|
+
Extended account model for organization management accounts.
|
146
|
+
|
147
|
+
Provides additional capabilities for managing organization-wide
|
148
|
+
operations and cross-account resource discovery.
|
149
|
+
"""
|
150
|
+
|
151
|
+
child_accounts: List[AWSAccount] = Field(
|
152
|
+
default_factory=list, description="List of child accounts in this organization"
|
153
|
+
)
|
154
|
+
|
155
|
+
organizational_units: Dict[str, str] = Field(default_factory=dict, description="Mapping of OU IDs to OU names")
|
156
|
+
|
157
|
+
service_control_policies: List[str] = Field(
|
158
|
+
default_factory=list, description="List of SCP IDs attached to the organization"
|
159
|
+
)
|
160
|
+
|
161
|
+
feature_set: str = Field("ALL", description="Organization feature set (ALL or CONSOLIDATED_BILLING)")
|
162
|
+
|
163
|
+
def add_child_account(self, account: AWSAccount) -> None:
|
164
|
+
"""Add a child account to the organization."""
|
165
|
+
account.organization_id = self.organization_id
|
166
|
+
self.child_accounts.append(account)
|
167
|
+
|
168
|
+
def get_child_account(self, account_id: str) -> Optional[AWSAccount]:
|
169
|
+
"""Get a specific child account by ID."""
|
170
|
+
for account in self.child_accounts:
|
171
|
+
if account.account_id == account_id:
|
172
|
+
return account
|
173
|
+
return None
|
174
|
+
|
175
|
+
def get_accounts_by_ou(self, ou_id: str) -> List[AWSAccount]:
|
176
|
+
"""Get all accounts in a specific organizational unit."""
|
177
|
+
return [account for account in self.child_accounts if account.organizational_unit_id == ou_id]
|
178
|
+
|
179
|
+
def get_total_accounts(self) -> int:
|
180
|
+
"""Get total number of accounts in organization (including management)."""
|
181
|
+
return len(self.child_accounts) + 1
|
182
|
+
|
183
|
+
def get_organization_summary(self) -> Dict:
|
184
|
+
"""Get organization summary statistics."""
|
185
|
+
return {
|
186
|
+
"management_account": self.account_id,
|
187
|
+
"total_accounts": self.get_total_accounts(),
|
188
|
+
"organizational_units": len(self.organizational_units),
|
189
|
+
"service_control_policies": len(self.service_control_policies),
|
190
|
+
"feature_set": self.feature_set,
|
191
|
+
"active_accounts": sum(1 for acc in self.child_accounts if acc.status == AccountStatus.ACTIVE),
|
192
|
+
}
|
@@ -0,0 +1,309 @@
|
|
1
|
+
"""
|
2
|
+
Inventory result models for collection aggregation.
|
3
|
+
|
4
|
+
This module provides models for representing the results of inventory
|
5
|
+
collection operations, including metadata, statistics, and aggregations.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from dataclasses import dataclass, field
|
9
|
+
from datetime import datetime, timedelta
|
10
|
+
from enum import Enum
|
11
|
+
from typing import Any, Dict, List, Optional, Set
|
12
|
+
|
13
|
+
from pydantic import BaseModel, Field, validator
|
14
|
+
|
15
|
+
from runbooks.inventory.models.account import AWSAccount
|
16
|
+
from runbooks.inventory.models.resource import AWSResource
|
17
|
+
|
18
|
+
|
19
|
+
class InventoryStatus(str, Enum):
|
20
|
+
"""Inventory collection status."""
|
21
|
+
|
22
|
+
SUCCESS = "success"
|
23
|
+
PARTIAL_SUCCESS = "partial_success"
|
24
|
+
FAILED = "failed"
|
25
|
+
IN_PROGRESS = "in_progress"
|
26
|
+
CANCELLED = "cancelled"
|
27
|
+
|
28
|
+
|
29
|
+
class CollectionScope(str, Enum):
|
30
|
+
"""Scope of inventory collection."""
|
31
|
+
|
32
|
+
SINGLE_ACCOUNT = "single_account"
|
33
|
+
ORGANIZATION = "organization"
|
34
|
+
MULTI_ACCOUNT = "multi_account"
|
35
|
+
CUSTOM = "custom"
|
36
|
+
|
37
|
+
|
38
|
+
@dataclass
|
39
|
+
class CollectionError:
|
40
|
+
"""Error information from collection operations."""
|
41
|
+
|
42
|
+
account_id: str
|
43
|
+
region: str
|
44
|
+
service: str
|
45
|
+
error_code: str
|
46
|
+
error_message: str
|
47
|
+
timestamp: datetime
|
48
|
+
retry_count: int = 0
|
49
|
+
|
50
|
+
def is_retryable(self) -> bool:
|
51
|
+
"""Check if this error type is retryable."""
|
52
|
+
retryable_codes = {"Throttling", "RequestLimitExceeded", "ServiceUnavailable", "InternalError", "NetworkError"}
|
53
|
+
return self.error_code in retryable_codes
|
54
|
+
|
55
|
+
|
56
|
+
@dataclass
|
57
|
+
class InventoryStatistics:
|
58
|
+
"""Statistical summary of inventory collection."""
|
59
|
+
|
60
|
+
# Resource counts
|
61
|
+
total_resources: int = 0
|
62
|
+
resources_by_type: Dict[str, int] = field(default_factory=dict)
|
63
|
+
resources_by_account: Dict[str, int] = field(default_factory=dict)
|
64
|
+
resources_by_region: Dict[str, int] = field(default_factory=dict)
|
65
|
+
resources_by_service: Dict[str, int] = field(default_factory=dict)
|
66
|
+
|
67
|
+
# State distribution
|
68
|
+
active_resources: int = 0
|
69
|
+
inactive_resources: int = 0
|
70
|
+
billable_resources: int = 0
|
71
|
+
|
72
|
+
# Cost information
|
73
|
+
total_estimated_cost: float = 0.0
|
74
|
+
cost_by_account: Dict[str, float] = field(default_factory=dict)
|
75
|
+
cost_by_service: Dict[str, float] = field(default_factory=dict)
|
76
|
+
|
77
|
+
# Collection metrics
|
78
|
+
accounts_scanned: int = 0
|
79
|
+
regions_scanned: int = 0
|
80
|
+
services_scanned: int = 0
|
81
|
+
api_calls_made: int = 0
|
82
|
+
|
83
|
+
# Error tracking
|
84
|
+
total_errors: int = 0
|
85
|
+
errors_by_service: Dict[str, int] = field(default_factory=dict)
|
86
|
+
|
87
|
+
def add_resource(self, resource: AWSResource) -> None:
|
88
|
+
"""Add a resource to the statistics."""
|
89
|
+
self.total_resources += 1
|
90
|
+
|
91
|
+
# Update type count
|
92
|
+
resource_type = resource.resource_type
|
93
|
+
self.resources_by_type[resource_type] = self.resources_by_type.get(resource_type, 0) + 1
|
94
|
+
|
95
|
+
# Update account count
|
96
|
+
account_id = resource.account_id
|
97
|
+
self.resources_by_account[account_id] = self.resources_by_account.get(account_id, 0) + 1
|
98
|
+
|
99
|
+
# Update region count
|
100
|
+
region = resource.region
|
101
|
+
self.resources_by_region[region] = self.resources_by_region.get(region, 0) + 1
|
102
|
+
|
103
|
+
# Update service count
|
104
|
+
service = resource.get_service_name()
|
105
|
+
self.resources_by_service[service] = self.resources_by_service.get(service, 0) + 1
|
106
|
+
|
107
|
+
# Update state counts
|
108
|
+
if resource.is_active():
|
109
|
+
self.active_resources += 1
|
110
|
+
else:
|
111
|
+
self.inactive_resources += 1
|
112
|
+
|
113
|
+
if resource.is_billable():
|
114
|
+
self.billable_resources += 1
|
115
|
+
|
116
|
+
# Update cost information
|
117
|
+
cost = resource.get_cost_estimate()
|
118
|
+
if cost > 0:
|
119
|
+
self.total_estimated_cost += cost
|
120
|
+
|
121
|
+
self.cost_by_account[account_id] = self.cost_by_account.get(account_id, 0.0) + cost
|
122
|
+
|
123
|
+
self.cost_by_service[service] = self.cost_by_service.get(service, 0.0) + cost
|
124
|
+
|
125
|
+
def add_error(self, error: CollectionError) -> None:
|
126
|
+
"""Add an error to the statistics."""
|
127
|
+
self.total_errors += 1
|
128
|
+
|
129
|
+
service = error.service
|
130
|
+
self.errors_by_service[service] = self.errors_by_service.get(service, 0) + 1
|
131
|
+
|
132
|
+
def get_success_rate(self) -> float:
|
133
|
+
"""Calculate collection success rate."""
|
134
|
+
total_operations = self.total_resources + self.total_errors
|
135
|
+
if total_operations == 0:
|
136
|
+
return 0.0
|
137
|
+
return (self.total_resources / total_operations) * 100
|
138
|
+
|
139
|
+
def get_top_resource_types(self, limit: int = 10) -> List[tuple]:
|
140
|
+
"""Get top resource types by count."""
|
141
|
+
return sorted(self.resources_by_type.items(), key=lambda x: x[1], reverse=True)[:limit]
|
142
|
+
|
143
|
+
def get_most_expensive_services(self, limit: int = 10) -> List[tuple]:
|
144
|
+
"""Get most expensive services by cost."""
|
145
|
+
return sorted(self.cost_by_service.items(), key=lambda x: x[1], reverse=True)[:limit]
|
146
|
+
|
147
|
+
|
148
|
+
class InventoryMetadata(BaseModel):
|
149
|
+
"""
|
150
|
+
Metadata about an inventory collection operation.
|
151
|
+
|
152
|
+
Provides comprehensive information about when, how, and what
|
153
|
+
was collected during an inventory operation.
|
154
|
+
"""
|
155
|
+
|
156
|
+
# Collection identification
|
157
|
+
collection_id: str = Field(..., description="Unique identifier for this collection")
|
158
|
+
|
159
|
+
collection_name: Optional[str] = Field(None, description="Human-readable name for this collection")
|
160
|
+
|
161
|
+
# Timing information
|
162
|
+
start_time: datetime = Field(..., description="Collection start timestamp")
|
163
|
+
|
164
|
+
end_time: Optional[datetime] = Field(None, description="Collection completion timestamp")
|
165
|
+
|
166
|
+
duration: Optional[timedelta] = Field(None, description="Total collection duration")
|
167
|
+
|
168
|
+
# Scope and configuration
|
169
|
+
scope: CollectionScope = Field(..., description="Scope of this collection")
|
170
|
+
|
171
|
+
target_accounts: List[str] = Field(default_factory=list, description="Account IDs that were targeted")
|
172
|
+
|
173
|
+
target_regions: List[str] = Field(default_factory=list, description="Regions that were scanned")
|
174
|
+
|
175
|
+
target_resource_types: Set[str] = Field(default_factory=set, description="Resource types that were collected")
|
176
|
+
|
177
|
+
# Collection parameters
|
178
|
+
include_costs: bool = Field(False, description="Whether cost information was collected")
|
179
|
+
|
180
|
+
parallel_execution: bool = Field(True, description="Whether collection used parallel processing")
|
181
|
+
|
182
|
+
max_workers: Optional[int] = Field(None, description="Maximum number of worker threads used")
|
183
|
+
|
184
|
+
# Collector information
|
185
|
+
collector_version: str = Field(..., description="Version of collector used")
|
186
|
+
|
187
|
+
collector_config: Dict[str, Any] = Field(default_factory=dict, description="Collector configuration parameters")
|
188
|
+
|
189
|
+
# Status and results
|
190
|
+
status: InventoryStatus = Field(InventoryStatus.IN_PROGRESS, description="Current collection status")
|
191
|
+
|
192
|
+
status_message: Optional[str] = Field(None, description="Additional status information")
|
193
|
+
|
194
|
+
class Config:
|
195
|
+
"""Pydantic configuration."""
|
196
|
+
|
197
|
+
use_enum_values = True
|
198
|
+
json_encoders = {
|
199
|
+
datetime: lambda v: v.isoformat(),
|
200
|
+
timedelta: lambda v: v.total_seconds(),
|
201
|
+
set: lambda v: list(v),
|
202
|
+
}
|
203
|
+
|
204
|
+
def mark_completed(self, status: InventoryStatus = InventoryStatus.SUCCESS) -> None:
|
205
|
+
"""Mark collection as completed."""
|
206
|
+
self.end_time = datetime.utcnow()
|
207
|
+
self.duration = self.end_time - self.start_time
|
208
|
+
self.status = status
|
209
|
+
|
210
|
+
def get_duration_seconds(self) -> float:
|
211
|
+
"""Get collection duration in seconds."""
|
212
|
+
if self.duration:
|
213
|
+
return self.duration.total_seconds()
|
214
|
+
if self.end_time:
|
215
|
+
return (self.end_time - self.start_time).total_seconds()
|
216
|
+
return (datetime.utcnow() - self.start_time).total_seconds()
|
217
|
+
|
218
|
+
|
219
|
+
class InventoryResult(BaseModel):
|
220
|
+
"""
|
221
|
+
Complete results from an inventory collection operation.
|
222
|
+
|
223
|
+
Aggregates all resources discovered, statistics, errors, and metadata
|
224
|
+
from a complete inventory collection run.
|
225
|
+
"""
|
226
|
+
|
227
|
+
# Core data
|
228
|
+
metadata: InventoryMetadata = Field(..., description="Collection metadata and configuration")
|
229
|
+
|
230
|
+
resources: List[AWSResource] = Field(default_factory=list, description="All discovered resources")
|
231
|
+
|
232
|
+
accounts: List[AWSAccount] = Field(default_factory=list, description="Account information for scanned accounts")
|
233
|
+
|
234
|
+
# Statistics and analytics
|
235
|
+
statistics: InventoryStatistics = Field(
|
236
|
+
default_factory=InventoryStatistics, description="Statistical summary of collection"
|
237
|
+
)
|
238
|
+
|
239
|
+
# Error tracking
|
240
|
+
errors: List[CollectionError] = Field(default_factory=list, description="Errors encountered during collection")
|
241
|
+
|
242
|
+
# Performance metrics
|
243
|
+
performance_metrics: Dict[str, Any] = Field(default_factory=dict, description="Performance and timing metrics")
|
244
|
+
|
245
|
+
class Config:
|
246
|
+
"""Pydantic configuration."""
|
247
|
+
|
248
|
+
json_encoders = {datetime: lambda v: v.isoformat(), timedelta: lambda v: v.total_seconds()}
|
249
|
+
|
250
|
+
def add_resource(self, resource: AWSResource) -> None:
|
251
|
+
"""Add a resource to the results."""
|
252
|
+
self.resources.append(resource)
|
253
|
+
self.statistics.add_resource(resource)
|
254
|
+
|
255
|
+
def add_error(self, error: CollectionError) -> None:
|
256
|
+
"""Add an error to the results."""
|
257
|
+
self.errors.append(error)
|
258
|
+
self.statistics.add_error(error)
|
259
|
+
|
260
|
+
def get_resources_by_type(self, resource_type: str) -> List[AWSResource]:
|
261
|
+
"""Get all resources of a specific type."""
|
262
|
+
return [r for r in self.resources if r.resource_type == resource_type]
|
263
|
+
|
264
|
+
def get_resources_by_account(self, account_id: str) -> List[AWSResource]:
|
265
|
+
"""Get all resources from a specific account."""
|
266
|
+
return [r for r in self.resources if r.account_id == account_id]
|
267
|
+
|
268
|
+
def get_resources_by_region(self, region: str) -> List[AWSResource]:
|
269
|
+
"""Get all resources from a specific region."""
|
270
|
+
return [r for r in self.resources if r.region == region]
|
271
|
+
|
272
|
+
def get_active_resources(self) -> List[AWSResource]:
|
273
|
+
"""Get all active resources."""
|
274
|
+
return [r for r in self.resources if r.is_active()]
|
275
|
+
|
276
|
+
def get_billable_resources(self) -> List[AWSResource]:
|
277
|
+
"""Get all billable resources."""
|
278
|
+
return [r for r in self.resources if r.is_billable()]
|
279
|
+
|
280
|
+
def get_resources_with_tag(self, key: str, value: Optional[str] = None) -> List[AWSResource]:
|
281
|
+
"""Get resources that have a specific tag."""
|
282
|
+
return [r for r in self.resources if r.has_tag(key, value)]
|
283
|
+
|
284
|
+
def get_total_cost_estimate(self) -> float:
|
285
|
+
"""Get total estimated monthly cost."""
|
286
|
+
return self.statistics.total_estimated_cost
|
287
|
+
|
288
|
+
def get_collection_summary(self) -> Dict[str, Any]:
|
289
|
+
"""Get a summary of the collection results."""
|
290
|
+
return {
|
291
|
+
"collection_id": self.metadata.collection_id,
|
292
|
+
"status": self.metadata.status,
|
293
|
+
"duration_seconds": self.metadata.get_duration_seconds(),
|
294
|
+
"total_resources": self.statistics.total_resources,
|
295
|
+
"total_accounts": len(self.accounts),
|
296
|
+
"total_regions": self.statistics.regions_scanned,
|
297
|
+
"total_errors": self.statistics.total_errors,
|
298
|
+
"success_rate": self.statistics.get_success_rate(),
|
299
|
+
"total_cost_estimate": self.statistics.total_estimated_cost,
|
300
|
+
"scope": self.metadata.scope,
|
301
|
+
}
|
302
|
+
|
303
|
+
def to_dict(self) -> Dict[str, Any]:
|
304
|
+
"""Convert to dictionary representation."""
|
305
|
+
return self.dict()
|
306
|
+
|
307
|
+
def __len__(self) -> int:
|
308
|
+
"""Return number of resources collected."""
|
309
|
+
return len(self.resources)
|