aws-inventory-manager 0.2.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.
Potentially problematic release.
This version of aws-inventory-manager might be problematic. Click here for more details.
- aws_inventory_manager-0.2.0.dist-info/METADATA +508 -0
- aws_inventory_manager-0.2.0.dist-info/RECORD +65 -0
- aws_inventory_manager-0.2.0.dist-info/WHEEL +5 -0
- aws_inventory_manager-0.2.0.dist-info/entry_points.txt +2 -0
- aws_inventory_manager-0.2.0.dist-info/licenses/LICENSE +21 -0
- aws_inventory_manager-0.2.0.dist-info/top_level.txt +1 -0
- src/__init__.py +3 -0
- src/aws/__init__.py +11 -0
- src/aws/client.py +128 -0
- src/aws/credentials.py +191 -0
- src/aws/rate_limiter.py +177 -0
- src/cli/__init__.py +5 -0
- src/cli/config.py +130 -0
- src/cli/main.py +1450 -0
- src/cost/__init__.py +5 -0
- src/cost/analyzer.py +226 -0
- src/cost/explorer.py +209 -0
- src/cost/reporter.py +237 -0
- src/delta/__init__.py +5 -0
- src/delta/calculator.py +180 -0
- src/delta/reporter.py +225 -0
- src/models/__init__.py +17 -0
- src/models/cost_report.py +87 -0
- src/models/delta_report.py +111 -0
- src/models/inventory.py +124 -0
- src/models/resource.py +99 -0
- src/models/snapshot.py +108 -0
- src/snapshot/__init__.py +6 -0
- src/snapshot/capturer.py +347 -0
- src/snapshot/filter.py +245 -0
- src/snapshot/inventory_storage.py +264 -0
- src/snapshot/resource_collectors/__init__.py +5 -0
- src/snapshot/resource_collectors/apigateway.py +140 -0
- src/snapshot/resource_collectors/backup.py +136 -0
- src/snapshot/resource_collectors/base.py +81 -0
- src/snapshot/resource_collectors/cloudformation.py +55 -0
- src/snapshot/resource_collectors/cloudwatch.py +109 -0
- src/snapshot/resource_collectors/codebuild.py +69 -0
- src/snapshot/resource_collectors/codepipeline.py +82 -0
- src/snapshot/resource_collectors/dynamodb.py +65 -0
- src/snapshot/resource_collectors/ec2.py +240 -0
- src/snapshot/resource_collectors/ecs.py +215 -0
- src/snapshot/resource_collectors/eks.py +200 -0
- src/snapshot/resource_collectors/elb.py +126 -0
- src/snapshot/resource_collectors/eventbridge.py +156 -0
- src/snapshot/resource_collectors/iam.py +188 -0
- src/snapshot/resource_collectors/kms.py +111 -0
- src/snapshot/resource_collectors/lambda_func.py +112 -0
- src/snapshot/resource_collectors/rds.py +109 -0
- src/snapshot/resource_collectors/route53.py +86 -0
- src/snapshot/resource_collectors/s3.py +105 -0
- src/snapshot/resource_collectors/secretsmanager.py +70 -0
- src/snapshot/resource_collectors/sns.py +68 -0
- src/snapshot/resource_collectors/sqs.py +72 -0
- src/snapshot/resource_collectors/ssm.py +160 -0
- src/snapshot/resource_collectors/stepfunctions.py +74 -0
- src/snapshot/resource_collectors/vpcendpoints.py +79 -0
- src/snapshot/resource_collectors/waf.py +159 -0
- src/snapshot/storage.py +259 -0
- src/utils/__init__.py +12 -0
- src/utils/export.py +87 -0
- src/utils/hash.py +60 -0
- src/utils/logging.py +63 -0
- src/utils/paths.py +51 -0
- src/utils/progress.py +41 -0
src/delta/calculator.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Delta calculator for comparing snapshots."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from ..models.delta_report import DeltaReport, ResourceChange
|
|
8
|
+
from ..models.resource import Resource
|
|
9
|
+
from ..models.snapshot import Snapshot
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DeltaCalculator:
|
|
15
|
+
"""Calculate differences between two snapshots."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, reference_snapshot: Snapshot, current_snapshot: Snapshot):
|
|
18
|
+
"""Initialize delta calculator.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
reference_snapshot: The reference snapshot to compare against
|
|
22
|
+
current_snapshot: The current snapshot
|
|
23
|
+
"""
|
|
24
|
+
self.reference = reference_snapshot
|
|
25
|
+
self.current = current_snapshot
|
|
26
|
+
|
|
27
|
+
# Index resources by ARN for fast lookup
|
|
28
|
+
self.reference_index = {r.arn: r for r in reference_snapshot.resources}
|
|
29
|
+
self.current_index = {r.arn: r for r in current_snapshot.resources}
|
|
30
|
+
|
|
31
|
+
def calculate(
|
|
32
|
+
self,
|
|
33
|
+
resource_type_filter: Optional[List[str]] = None,
|
|
34
|
+
region_filter: Optional[List[str]] = None,
|
|
35
|
+
) -> DeltaReport:
|
|
36
|
+
"""Calculate delta between reference and current snapshots.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
resource_type_filter: Optional list of resource types to include
|
|
40
|
+
region_filter: Optional list of regions to include
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
DeltaReport with added, deleted, and modified resources
|
|
44
|
+
"""
|
|
45
|
+
logger.info("Calculating delta between reference and current snapshots")
|
|
46
|
+
|
|
47
|
+
added_resources = []
|
|
48
|
+
deleted_resources = []
|
|
49
|
+
modified_resources = []
|
|
50
|
+
|
|
51
|
+
reference_arns = set(self.reference_index.keys())
|
|
52
|
+
current_arns = set(self.current_index.keys())
|
|
53
|
+
|
|
54
|
+
# Find added resources (in current but not in reference)
|
|
55
|
+
added_arns = current_arns - reference_arns
|
|
56
|
+
for arn in added_arns:
|
|
57
|
+
resource = self.current_index[arn]
|
|
58
|
+
if self._matches_filters(resource, resource_type_filter, region_filter):
|
|
59
|
+
added_resources.append(resource)
|
|
60
|
+
|
|
61
|
+
# Find deleted resources (in reference but not in current)
|
|
62
|
+
deleted_arns = reference_arns - current_arns
|
|
63
|
+
for arn in deleted_arns:
|
|
64
|
+
resource = self.reference_index[arn]
|
|
65
|
+
if self._matches_filters(resource, resource_type_filter, region_filter):
|
|
66
|
+
deleted_resources.append(resource)
|
|
67
|
+
|
|
68
|
+
# Find modified resources (in both but with different config)
|
|
69
|
+
common_arns = reference_arns & current_arns
|
|
70
|
+
for arn in common_arns:
|
|
71
|
+
reference_resource = self.reference_index[arn]
|
|
72
|
+
current_resource = self.current_index[arn]
|
|
73
|
+
|
|
74
|
+
if self._matches_filters(current_resource, resource_type_filter, region_filter):
|
|
75
|
+
# Compare config hashes to detect modifications
|
|
76
|
+
if reference_resource.config_hash != current_resource.config_hash:
|
|
77
|
+
change = ResourceChange(
|
|
78
|
+
resource=current_resource,
|
|
79
|
+
baseline_resource=reference_resource,
|
|
80
|
+
change_type="modified",
|
|
81
|
+
old_config_hash=reference_resource.config_hash,
|
|
82
|
+
new_config_hash=current_resource.config_hash,
|
|
83
|
+
)
|
|
84
|
+
modified_resources.append(change)
|
|
85
|
+
|
|
86
|
+
# Create delta report
|
|
87
|
+
report = DeltaReport(
|
|
88
|
+
generated_at=datetime.now(timezone.utc),
|
|
89
|
+
baseline_snapshot_name=self.reference.name,
|
|
90
|
+
current_snapshot_name=self.current.name,
|
|
91
|
+
added_resources=added_resources,
|
|
92
|
+
deleted_resources=deleted_resources,
|
|
93
|
+
modified_resources=modified_resources,
|
|
94
|
+
baseline_resource_count=len(self.reference.resources),
|
|
95
|
+
current_resource_count=len(self.current.resources),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
logger.info(
|
|
99
|
+
f"Delta calculated: {len(added_resources)} added, "
|
|
100
|
+
f"{len(deleted_resources)} deleted, {len(modified_resources)} modified"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return report
|
|
104
|
+
|
|
105
|
+
def _matches_filters(
|
|
106
|
+
self,
|
|
107
|
+
resource: Resource,
|
|
108
|
+
resource_type_filter: Optional[List[str]],
|
|
109
|
+
region_filter: Optional[List[str]],
|
|
110
|
+
) -> bool:
|
|
111
|
+
"""Check if resource matches the specified filters.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
resource: Resource to check
|
|
115
|
+
resource_type_filter: Optional list of resource types to include
|
|
116
|
+
region_filter: Optional list of regions to include
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
True if resource matches all filters
|
|
120
|
+
"""
|
|
121
|
+
# Check resource type filter
|
|
122
|
+
if resource_type_filter:
|
|
123
|
+
if resource.resource_type not in resource_type_filter:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
# Check region filter
|
|
127
|
+
if region_filter:
|
|
128
|
+
if resource.region not in region_filter:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def compare_to_current_state(
|
|
135
|
+
reference_snapshot: Snapshot,
|
|
136
|
+
profile_name: Optional[str] = None,
|
|
137
|
+
regions: Optional[List[str]] = None,
|
|
138
|
+
resource_type_filter: Optional[List[str]] = None,
|
|
139
|
+
region_filter: Optional[List[str]] = None,
|
|
140
|
+
) -> DeltaReport:
|
|
141
|
+
"""Compare reference snapshot to current AWS state.
|
|
142
|
+
|
|
143
|
+
This is a convenience function that captures current state and calculates delta.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
reference_snapshot: The reference snapshot to compare against
|
|
147
|
+
profile_name: AWS profile name (optional)
|
|
148
|
+
regions: Regions to scan (defaults to reference snapshot regions)
|
|
149
|
+
resource_type_filter: Optional list of resource types to include in delta
|
|
150
|
+
region_filter: Optional list of regions to include in delta
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
DeltaReport with changes
|
|
154
|
+
"""
|
|
155
|
+
from ..aws.credentials import get_account_id
|
|
156
|
+
from ..snapshot.capturer import create_snapshot
|
|
157
|
+
|
|
158
|
+
# Use reference snapshot regions if not specified
|
|
159
|
+
if not regions:
|
|
160
|
+
regions = reference_snapshot.regions
|
|
161
|
+
|
|
162
|
+
# Get account ID
|
|
163
|
+
account_id = get_account_id(profile_name)
|
|
164
|
+
|
|
165
|
+
# Capture current state (don't save it, just use for comparison)
|
|
166
|
+
logger.info("Capturing current AWS state for comparison...")
|
|
167
|
+
current_snapshot = create_snapshot(
|
|
168
|
+
name=f"temp-current-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}",
|
|
169
|
+
regions=regions,
|
|
170
|
+
account_id=account_id,
|
|
171
|
+
profile_name=profile_name,
|
|
172
|
+
set_active=False,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Calculate delta
|
|
176
|
+
calculator = DeltaCalculator(reference_snapshot, current_snapshot)
|
|
177
|
+
return calculator.calculate(
|
|
178
|
+
resource_type_filter=resource_type_filter,
|
|
179
|
+
region_filter=region_filter,
|
|
180
|
+
)
|
src/delta/reporter.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Delta report formatting and display."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ..models.delta_report import DeltaReport
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DeltaReporter:
|
|
13
|
+
"""Format and display delta reports."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, console: Optional[Console] = None):
|
|
16
|
+
"""Initialize delta reporter.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
console: Rich console instance (creates new one if not provided)
|
|
20
|
+
"""
|
|
21
|
+
self.console = console or Console()
|
|
22
|
+
|
|
23
|
+
def display(self, report: DeltaReport, show_details: bool = False) -> None:
|
|
24
|
+
"""Display delta report to console.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
report: DeltaReport to display
|
|
28
|
+
show_details: Whether to show detailed resource information
|
|
29
|
+
"""
|
|
30
|
+
# Header
|
|
31
|
+
self.console.print()
|
|
32
|
+
self.console.print(
|
|
33
|
+
Panel(
|
|
34
|
+
f"[bold]Resource Delta Report[/bold]\n"
|
|
35
|
+
f"Reference Snapshot: {report.baseline_snapshot_name}\n"
|
|
36
|
+
f"Generated: {report.generated_at.strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
|
37
|
+
style="cyan",
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
self.console.print()
|
|
41
|
+
|
|
42
|
+
# Check if there are any changes
|
|
43
|
+
if not report.has_changes:
|
|
44
|
+
self.console.print(
|
|
45
|
+
"[green]✓ No changes detected - environment matches reference snapshot[/green]", style="bold"
|
|
46
|
+
)
|
|
47
|
+
self.console.print()
|
|
48
|
+
self.console.print(f"Total resources: {report.baseline_resource_count}")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
# Summary statistics
|
|
52
|
+
self._display_summary(report)
|
|
53
|
+
|
|
54
|
+
# Group changes by service
|
|
55
|
+
grouped = report.group_by_service()
|
|
56
|
+
|
|
57
|
+
# Display changes by service
|
|
58
|
+
for service_type in sorted(grouped.keys()):
|
|
59
|
+
changes = grouped[service_type]
|
|
60
|
+
if any(changes.values()):
|
|
61
|
+
self._display_service_changes(service_type, changes, show_details)
|
|
62
|
+
|
|
63
|
+
def _display_summary(self, report: DeltaReport) -> None:
|
|
64
|
+
"""Display summary statistics."""
|
|
65
|
+
table = Table(title="Summary", show_header=True, header_style="bold magenta")
|
|
66
|
+
table.add_column("Change Type", style="cyan", width=15)
|
|
67
|
+
table.add_column("Count", justify="right", style="yellow", width=10)
|
|
68
|
+
|
|
69
|
+
added_count = len(report.added_resources)
|
|
70
|
+
deleted_count = len(report.deleted_resources)
|
|
71
|
+
modified_count = len(report.modified_resources)
|
|
72
|
+
unchanged_count = report.unchanged_count
|
|
73
|
+
|
|
74
|
+
if added_count > 0:
|
|
75
|
+
table.add_row("➕ Added", f"[green]{added_count}[/green]")
|
|
76
|
+
if deleted_count > 0:
|
|
77
|
+
table.add_row("➖ Deleted", f"[red]{deleted_count}[/red]")
|
|
78
|
+
if modified_count > 0:
|
|
79
|
+
table.add_row("🔄 Modified", f"[yellow]{modified_count}[/yellow]")
|
|
80
|
+
if unchanged_count > 0:
|
|
81
|
+
table.add_row("✓ Unchanged", str(unchanged_count))
|
|
82
|
+
|
|
83
|
+
table.add_row("━" * 15, "━" * 10, style="dim")
|
|
84
|
+
table.add_row("[bold]Total Changes", f"[bold]{report.total_changes}")
|
|
85
|
+
|
|
86
|
+
self.console.print(table)
|
|
87
|
+
self.console.print()
|
|
88
|
+
|
|
89
|
+
def _display_service_changes(self, service_type: str, changes: dict, show_details: bool) -> None:
|
|
90
|
+
"""Display changes for a specific service type.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
service_type: AWS resource type (e.g., 'AWS::EC2::Instance')
|
|
94
|
+
changes: Dictionary with 'added', 'deleted', 'modified' lists
|
|
95
|
+
show_details: Whether to show detailed information
|
|
96
|
+
"""
|
|
97
|
+
# Count total changes for this service
|
|
98
|
+
total = len(changes["added"]) + len(changes["deleted"]) + len(changes["modified"])
|
|
99
|
+
if total == 0:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
self.console.print(f"[bold cyan]{service_type}[/bold cyan] ({total} changes)")
|
|
103
|
+
|
|
104
|
+
# Create table for this service
|
|
105
|
+
table = Table(show_header=True, box=None, padding=(0, 2))
|
|
106
|
+
table.add_column("Change", width=8)
|
|
107
|
+
table.add_column("Name", style="white")
|
|
108
|
+
table.add_column("Region", width=15)
|
|
109
|
+
table.add_column("ARN" if show_details else "Tags", style="dim")
|
|
110
|
+
|
|
111
|
+
# Added resources
|
|
112
|
+
for resource in changes["added"]:
|
|
113
|
+
tags_str = self._format_tags(resource.tags)
|
|
114
|
+
arn_or_tags = resource.arn if show_details else tags_str
|
|
115
|
+
table.add_row("[green]➕ Add[/green]", resource.name, resource.region, arn_or_tags)
|
|
116
|
+
|
|
117
|
+
# Deleted resources
|
|
118
|
+
for resource in changes["deleted"]:
|
|
119
|
+
tags_str = self._format_tags(resource.tags)
|
|
120
|
+
arn_or_tags = resource.arn if show_details else tags_str
|
|
121
|
+
table.add_row("[red]➖ Del[/red]", resource.name, resource.region, arn_or_tags)
|
|
122
|
+
|
|
123
|
+
# Modified resources
|
|
124
|
+
for change in changes["modified"]:
|
|
125
|
+
tags_str = self._format_tags(change.resource.tags)
|
|
126
|
+
arn_or_tags = change.resource.arn if show_details else tags_str
|
|
127
|
+
table.add_row("[yellow]🔄 Mod[/yellow]", change.resource.name, change.resource.region, arn_or_tags)
|
|
128
|
+
|
|
129
|
+
self.console.print(table)
|
|
130
|
+
self.console.print()
|
|
131
|
+
|
|
132
|
+
def _format_tags(self, tags: dict) -> str:
|
|
133
|
+
"""Format tags dictionary as a string.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
tags: Dictionary of tag key-value pairs
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Formatted string like "Env=prod, App=web"
|
|
140
|
+
"""
|
|
141
|
+
if not tags:
|
|
142
|
+
return "-"
|
|
143
|
+
|
|
144
|
+
# Show up to 3 most important tags
|
|
145
|
+
important_keys = ["Name", "Environment", "Project", "Team", "Application"]
|
|
146
|
+
selected_tags = []
|
|
147
|
+
|
|
148
|
+
# First add important tags if present
|
|
149
|
+
for key in important_keys:
|
|
150
|
+
if key in tags:
|
|
151
|
+
selected_tags.append(f"{key}={tags[key]}")
|
|
152
|
+
|
|
153
|
+
# Add other tags up to limit
|
|
154
|
+
for key, value in tags.items():
|
|
155
|
+
if key not in important_keys and len(selected_tags) < 3:
|
|
156
|
+
selected_tags.append(f"{key}={value}")
|
|
157
|
+
|
|
158
|
+
return ", ".join(selected_tags[:3])
|
|
159
|
+
|
|
160
|
+
def export_json(self, report: DeltaReport, filepath: str) -> None:
|
|
161
|
+
"""Export delta report to JSON file.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
report: DeltaReport to export
|
|
165
|
+
filepath: Destination file path
|
|
166
|
+
"""
|
|
167
|
+
from ..utils.export import export_to_json
|
|
168
|
+
|
|
169
|
+
export_to_json(report.to_dict(), filepath)
|
|
170
|
+
self.console.print(f"[green]✓ Delta report exported to {filepath}[/green]")
|
|
171
|
+
|
|
172
|
+
def export_csv(self, report: DeltaReport, filepath: str) -> None:
|
|
173
|
+
"""Export delta report to CSV file.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
report: DeltaReport to export
|
|
177
|
+
filepath: Destination file path
|
|
178
|
+
"""
|
|
179
|
+
from ..utils.export import export_to_csv
|
|
180
|
+
|
|
181
|
+
# Flatten the report into rows
|
|
182
|
+
rows = []
|
|
183
|
+
|
|
184
|
+
for resource in report.added_resources:
|
|
185
|
+
rows.append(
|
|
186
|
+
{
|
|
187
|
+
"change_type": "added",
|
|
188
|
+
"resource_type": resource.resource_type,
|
|
189
|
+
"name": resource.name,
|
|
190
|
+
"arn": resource.arn,
|
|
191
|
+
"region": resource.region,
|
|
192
|
+
"tags": str(resource.tags),
|
|
193
|
+
"created_at": resource.created_at.isoformat() if resource.created_at else None,
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
for resource in report.deleted_resources:
|
|
198
|
+
rows.append(
|
|
199
|
+
{
|
|
200
|
+
"change_type": "deleted",
|
|
201
|
+
"resource_type": resource.resource_type,
|
|
202
|
+
"name": resource.name,
|
|
203
|
+
"arn": resource.arn,
|
|
204
|
+
"region": resource.region,
|
|
205
|
+
"tags": str(resource.tags),
|
|
206
|
+
"created_at": resource.created_at.isoformat() if resource.created_at else None,
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
for change in report.modified_resources:
|
|
211
|
+
rows.append(
|
|
212
|
+
{
|
|
213
|
+
"change_type": "modified",
|
|
214
|
+
"resource_type": change.resource.resource_type,
|
|
215
|
+
"name": change.resource.name,
|
|
216
|
+
"arn": change.resource.arn,
|
|
217
|
+
"region": change.resource.region,
|
|
218
|
+
"tags": str(change.resource.tags),
|
|
219
|
+
"old_hash": change.old_config_hash,
|
|
220
|
+
"new_hash": change.new_config_hash,
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
export_to_csv(rows, filepath)
|
|
225
|
+
self.console.print(f"[green]✓ Delta report exported to {filepath}[/green]")
|
src/models/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Data models for AWS Baseline Snapshot tool."""
|
|
2
|
+
|
|
3
|
+
from .cost_report import CostBreakdown, CostReport
|
|
4
|
+
from .delta_report import DeltaReport, ResourceChange
|
|
5
|
+
from .inventory import Inventory
|
|
6
|
+
from .resource import Resource
|
|
7
|
+
from .snapshot import Snapshot
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Snapshot",
|
|
11
|
+
"Resource",
|
|
12
|
+
"DeltaReport",
|
|
13
|
+
"ResourceChange",
|
|
14
|
+
"CostReport",
|
|
15
|
+
"CostBreakdown",
|
|
16
|
+
"Inventory",
|
|
17
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Cost report models for cost analysis and tracking."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class CostBreakdown:
|
|
10
|
+
"""Represents cost breakdown for baseline or non-baseline resources."""
|
|
11
|
+
|
|
12
|
+
total: float
|
|
13
|
+
by_service: Dict[str, float] = field(default_factory=dict)
|
|
14
|
+
percentage: float = 0.0
|
|
15
|
+
|
|
16
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
17
|
+
"""Convert to dictionary for serialization."""
|
|
18
|
+
return {
|
|
19
|
+
"total": self.total,
|
|
20
|
+
"by_service": self.by_service,
|
|
21
|
+
"percentage": self.percentage,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CostReport:
|
|
27
|
+
"""Represents cost analysis separating baseline vs non-baseline costs."""
|
|
28
|
+
|
|
29
|
+
generated_at: datetime
|
|
30
|
+
baseline_snapshot_name: str
|
|
31
|
+
period_start: datetime
|
|
32
|
+
period_end: datetime
|
|
33
|
+
baseline_costs: CostBreakdown
|
|
34
|
+
non_baseline_costs: CostBreakdown
|
|
35
|
+
total_cost: float
|
|
36
|
+
data_complete: bool = True
|
|
37
|
+
data_through: Optional[datetime] = None
|
|
38
|
+
lag_days: int = 0
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
41
|
+
"""Convert to dictionary for serialization."""
|
|
42
|
+
return {
|
|
43
|
+
"generated_at": self.generated_at.isoformat(),
|
|
44
|
+
"baseline_snapshot_name": self.baseline_snapshot_name,
|
|
45
|
+
"period_start": self.period_start.isoformat(),
|
|
46
|
+
"period_end": self.period_end.isoformat(),
|
|
47
|
+
"baseline_costs": self.baseline_costs.to_dict(),
|
|
48
|
+
"non_baseline_costs": self.non_baseline_costs.to_dict(),
|
|
49
|
+
"total_cost": self.total_cost,
|
|
50
|
+
"data_complete": self.data_complete,
|
|
51
|
+
"data_through": self.data_through.isoformat() if self.data_through else None,
|
|
52
|
+
"lag_days": self.lag_days,
|
|
53
|
+
"summary": {
|
|
54
|
+
"baseline_total": self.baseline_costs.total,
|
|
55
|
+
"baseline_percentage": self.baseline_costs.percentage,
|
|
56
|
+
"non_baseline_total": self.non_baseline_costs.total,
|
|
57
|
+
"non_baseline_percentage": self.non_baseline_costs.percentage,
|
|
58
|
+
"total": self.total_cost,
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def baseline_percentage(self) -> float:
|
|
64
|
+
"""Get baseline cost percentage."""
|
|
65
|
+
return self.baseline_costs.percentage
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def non_baseline_percentage(self) -> float:
|
|
69
|
+
"""Get non-baseline cost percentage."""
|
|
70
|
+
return self.non_baseline_costs.percentage
|
|
71
|
+
|
|
72
|
+
def get_top_services(self, limit: int = 5, baseline: bool = True) -> Dict[str, float]:
|
|
73
|
+
"""Get top N services by cost.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
limit: Number of top services to return
|
|
77
|
+
baseline: If True, return baseline services; if False, non-baseline
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Dictionary of service name to cost, sorted by cost descending
|
|
81
|
+
"""
|
|
82
|
+
services = self.baseline_costs.by_service if baseline else self.non_baseline_costs.by_service
|
|
83
|
+
|
|
84
|
+
# Sort by cost descending
|
|
85
|
+
sorted_services = sorted(services.items(), key=lambda x: x[1], reverse=True)
|
|
86
|
+
|
|
87
|
+
return dict(sorted_services[:limit])
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Delta report models for tracking resource changes."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ResourceChange:
|
|
10
|
+
"""Represents a modified resource in a delta report."""
|
|
11
|
+
|
|
12
|
+
resource: Any # Current Resource instance
|
|
13
|
+
baseline_resource: Any # Reference Resource instance (keeping field name for compatibility)
|
|
14
|
+
change_type: str # 'modified'
|
|
15
|
+
old_config_hash: str
|
|
16
|
+
new_config_hash: str
|
|
17
|
+
changes_summary: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
20
|
+
"""Convert to dictionary for serialization."""
|
|
21
|
+
return {
|
|
22
|
+
"arn": self.resource.arn,
|
|
23
|
+
"resource_type": self.resource.resource_type,
|
|
24
|
+
"name": self.resource.name,
|
|
25
|
+
"region": self.resource.region,
|
|
26
|
+
"change_type": self.change_type,
|
|
27
|
+
"tags": self.resource.tags,
|
|
28
|
+
"old_config_hash": self.old_config_hash,
|
|
29
|
+
"new_config_hash": self.new_config_hash,
|
|
30
|
+
"changes_summary": self.changes_summary,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class DeltaReport:
|
|
36
|
+
"""Represents differences between two snapshots."""
|
|
37
|
+
|
|
38
|
+
generated_at: datetime
|
|
39
|
+
baseline_snapshot_name: str # Reference snapshot name (keeping field name for compatibility)
|
|
40
|
+
current_snapshot_name: str
|
|
41
|
+
added_resources: List[Any] = field(default_factory=list) # List[Resource]
|
|
42
|
+
deleted_resources: List[Any] = field(default_factory=list) # List[Resource]
|
|
43
|
+
modified_resources: List[ResourceChange] = field(default_factory=list)
|
|
44
|
+
baseline_resource_count: int = 0 # Reference snapshot count (keeping field name for compatibility)
|
|
45
|
+
current_resource_count: int = 0
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
48
|
+
"""Convert to dictionary for serialization."""
|
|
49
|
+
return {
|
|
50
|
+
"generated_at": self.generated_at.isoformat(),
|
|
51
|
+
"baseline_snapshot_name": self.baseline_snapshot_name,
|
|
52
|
+
"current_snapshot_name": self.current_snapshot_name,
|
|
53
|
+
"added_resources": [r.to_dict() for r in self.added_resources],
|
|
54
|
+
"deleted_resources": [r.to_dict() for r in self.deleted_resources],
|
|
55
|
+
"modified_resources": [r.to_dict() for r in self.modified_resources],
|
|
56
|
+
"baseline_resource_count": self.baseline_resource_count,
|
|
57
|
+
"current_resource_count": self.current_resource_count,
|
|
58
|
+
"summary": {
|
|
59
|
+
"added": len(self.added_resources),
|
|
60
|
+
"deleted": len(self.deleted_resources),
|
|
61
|
+
"modified": len(self.modified_resources),
|
|
62
|
+
"unchanged": self.unchanged_count,
|
|
63
|
+
"total_changes": self.total_changes,
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def total_changes(self) -> int:
|
|
69
|
+
"""Total number of changes detected."""
|
|
70
|
+
return len(self.added_resources) + len(self.deleted_resources) + len(self.modified_resources)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def unchanged_count(self) -> int:
|
|
74
|
+
"""Number of unchanged resources."""
|
|
75
|
+
# Resources that existed in reference snapshot and still exist unchanged
|
|
76
|
+
return self.baseline_resource_count - len(self.deleted_resources) - len(self.modified_resources)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def has_changes(self) -> bool:
|
|
80
|
+
"""Whether any changes were detected."""
|
|
81
|
+
return self.total_changes > 0
|
|
82
|
+
|
|
83
|
+
def group_by_service(self) -> Dict[str, Dict[str, List]]:
|
|
84
|
+
"""Group changes by service type.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Dictionary mapping service type to changes dict with 'added', 'deleted', 'modified' lists
|
|
88
|
+
"""
|
|
89
|
+
from typing import Any, Dict, List
|
|
90
|
+
|
|
91
|
+
grouped: Dict[str, Dict[str, List[Any]]] = {}
|
|
92
|
+
|
|
93
|
+
for resource in self.added_resources:
|
|
94
|
+
service = resource.resource_type
|
|
95
|
+
if service not in grouped:
|
|
96
|
+
grouped[service] = {"added": [], "deleted": [], "modified": []}
|
|
97
|
+
grouped[service]["added"].append(resource)
|
|
98
|
+
|
|
99
|
+
for resource in self.deleted_resources:
|
|
100
|
+
service = resource.resource_type
|
|
101
|
+
if service not in grouped:
|
|
102
|
+
grouped[service] = {"added": [], "deleted": [], "modified": []}
|
|
103
|
+
grouped[service]["deleted"].append(resource)
|
|
104
|
+
|
|
105
|
+
for change in self.modified_resources:
|
|
106
|
+
service = change.resource.resource_type
|
|
107
|
+
if service not in grouped:
|
|
108
|
+
grouped[service] = {"added": [], "deleted": [], "modified": []}
|
|
109
|
+
grouped[service]["modified"].append(change)
|
|
110
|
+
|
|
111
|
+
return grouped
|