aws-inventory-manager 0.13.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aws-inventory-manager might be problematic. Click here for more details.
- aws_inventory_manager-0.13.2.dist-info/LICENSE +21 -0
- aws_inventory_manager-0.13.2.dist-info/METADATA +1226 -0
- aws_inventory_manager-0.13.2.dist-info/RECORD +145 -0
- aws_inventory_manager-0.13.2.dist-info/WHEEL +5 -0
- aws_inventory_manager-0.13.2.dist-info/entry_points.txt +2 -0
- aws_inventory_manager-0.13.2.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 +12 -0
- src/cli/config.py +130 -0
- src/cli/main.py +3626 -0
- src/config_service/__init__.py +21 -0
- src/config_service/collector.py +346 -0
- src/config_service/detector.py +256 -0
- src/config_service/resource_type_mapping.py +328 -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 +206 -0
- src/delta/differ.py +185 -0
- src/delta/formatters.py +272 -0
- src/delta/models.py +154 -0
- src/delta/reporter.py +234 -0
- src/models/__init__.py +21 -0
- src/models/config_diff.py +135 -0
- src/models/cost_report.py +87 -0
- src/models/deletion_operation.py +104 -0
- src/models/deletion_record.py +97 -0
- src/models/delta_report.py +122 -0
- src/models/efs_resource.py +80 -0
- src/models/elasticache_resource.py +90 -0
- src/models/group.py +318 -0
- src/models/inventory.py +133 -0
- src/models/protection_rule.py +123 -0
- src/models/report.py +288 -0
- src/models/resource.py +111 -0
- src/models/security_finding.py +102 -0
- src/models/snapshot.py +122 -0
- src/restore/__init__.py +20 -0
- src/restore/audit.py +175 -0
- src/restore/cleaner.py +461 -0
- src/restore/config.py +209 -0
- src/restore/deleter.py +976 -0
- src/restore/dependency.py +254 -0
- src/restore/safety.py +115 -0
- src/security/__init__.py +0 -0
- src/security/checks/__init__.py +0 -0
- src/security/checks/base.py +56 -0
- src/security/checks/ec2_checks.py +88 -0
- src/security/checks/elasticache_checks.py +149 -0
- src/security/checks/iam_checks.py +102 -0
- src/security/checks/rds_checks.py +140 -0
- src/security/checks/s3_checks.py +95 -0
- src/security/checks/secrets_checks.py +96 -0
- src/security/checks/sg_checks.py +142 -0
- src/security/cis_mapper.py +97 -0
- src/security/models.py +53 -0
- src/security/reporter.py +174 -0
- src/security/scanner.py +87 -0
- src/snapshot/__init__.py +6 -0
- src/snapshot/capturer.py +451 -0
- src/snapshot/filter.py +259 -0
- src/snapshot/inventory_storage.py +236 -0
- src/snapshot/report_formatter.py +250 -0
- src/snapshot/reporter.py +189 -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/efs_collector.py +102 -0
- src/snapshot/resource_collectors/eks.py +200 -0
- src/snapshot/resource_collectors/elasticache_collector.py +79 -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 +139 -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 +82 -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 +351 -0
- src/storage/__init__.py +21 -0
- src/storage/audit_store.py +419 -0
- src/storage/database.py +294 -0
- src/storage/group_store.py +749 -0
- src/storage/inventory_store.py +320 -0
- src/storage/resource_store.py +413 -0
- src/storage/schema.py +288 -0
- src/storage/snapshot_store.py +346 -0
- src/utils/__init__.py +12 -0
- src/utils/export.py +305 -0
- src/utils/hash.py +60 -0
- src/utils/logging.py +63 -0
- src/utils/pagination.py +41 -0
- src/utils/paths.py +51 -0
- src/utils/progress.py +41 -0
- src/utils/unsupported_resources.py +306 -0
- src/web/__init__.py +5 -0
- src/web/app.py +97 -0
- src/web/dependencies.py +69 -0
- src/web/routes/__init__.py +1 -0
- src/web/routes/api/__init__.py +18 -0
- src/web/routes/api/charts.py +156 -0
- src/web/routes/api/cleanup.py +186 -0
- src/web/routes/api/filters.py +253 -0
- src/web/routes/api/groups.py +305 -0
- src/web/routes/api/inventories.py +80 -0
- src/web/routes/api/queries.py +202 -0
- src/web/routes/api/resources.py +379 -0
- src/web/routes/api/snapshots.py +314 -0
- src/web/routes/api/views.py +260 -0
- src/web/routes/pages.py +198 -0
- src/web/services/__init__.py +1 -0
- src/web/templates/base.html +949 -0
- src/web/templates/components/navbar.html +31 -0
- src/web/templates/components/sidebar.html +104 -0
- src/web/templates/pages/audit_logs.html +86 -0
- src/web/templates/pages/cleanup.html +279 -0
- src/web/templates/pages/dashboard.html +227 -0
- src/web/templates/pages/diff.html +175 -0
- src/web/templates/pages/error.html +30 -0
- src/web/templates/pages/groups.html +721 -0
- src/web/templates/pages/queries.html +246 -0
- src/web/templates/pages/resources.html +2251 -0
- src/web/templates/pages/snapshot_detail.html +271 -0
- src/web/templates/pages/snapshots.html +429 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""WAF resource collector."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ...models.resource import Resource
|
|
6
|
+
from ...utils.hash import compute_config_hash
|
|
7
|
+
from .base import BaseResourceCollector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WAFCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS WAF (Web Application Firewall) resources."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "waf"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect WAF resources.
|
|
19
|
+
|
|
20
|
+
Collects WAFv2 Web ACLs (regional and global/CloudFront).
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
List of WAF Web ACL resources
|
|
24
|
+
"""
|
|
25
|
+
resources = []
|
|
26
|
+
|
|
27
|
+
# Collect regional Web ACLs
|
|
28
|
+
resources.extend(self._collect_regional_web_acls())
|
|
29
|
+
|
|
30
|
+
# Collect CloudFront Web ACLs (global, only from us-east-1)
|
|
31
|
+
if self.region == "us-east-1":
|
|
32
|
+
resources.extend(self._collect_cloudfront_web_acls())
|
|
33
|
+
|
|
34
|
+
self.logger.debug(f"Collected {len(resources)} WAF Web ACLs in {self.region}")
|
|
35
|
+
return resources
|
|
36
|
+
|
|
37
|
+
def _collect_regional_web_acls(self) -> List[Resource]:
|
|
38
|
+
"""Collect regional WAFv2 Web ACLs.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of regional Web ACL resources
|
|
42
|
+
"""
|
|
43
|
+
resources = []
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
client = self._create_client("wafv2")
|
|
47
|
+
|
|
48
|
+
paginator = client.get_paginator("list_web_acls")
|
|
49
|
+
for page in paginator.paginate(Scope="REGIONAL"):
|
|
50
|
+
for web_acl_summary in page.get("WebACLs", []):
|
|
51
|
+
web_acl_name = web_acl_summary["Name"]
|
|
52
|
+
web_acl_id = web_acl_summary["Id"]
|
|
53
|
+
web_acl_arn = web_acl_summary["ARN"]
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Get detailed Web ACL info
|
|
57
|
+
web_acl_response = client.get_web_acl(Name=web_acl_name, Scope="REGIONAL", Id=web_acl_id)
|
|
58
|
+
web_acl = web_acl_response.get("WebACL", {})
|
|
59
|
+
|
|
60
|
+
# Get tags
|
|
61
|
+
tags = {}
|
|
62
|
+
try:
|
|
63
|
+
tag_response = client.list_tags_for_resource(ResourceARN=web_acl_arn)
|
|
64
|
+
for tag_info in tag_response.get("TagInfoForResource", {}).get("TagList", []):
|
|
65
|
+
tags[tag_info["Key"]] = tag_info["Value"]
|
|
66
|
+
except Exception as e:
|
|
67
|
+
self.logger.debug(f"Could not get tags for Web ACL {web_acl_name}: {e}")
|
|
68
|
+
|
|
69
|
+
# WAFv2 doesn't provide creation timestamp
|
|
70
|
+
created_at = None
|
|
71
|
+
|
|
72
|
+
# Remove large rule definitions for config hash
|
|
73
|
+
config = {k: v for k, v in web_acl.items() if k not in ["Rules"]}
|
|
74
|
+
config["RuleCount"] = len(web_acl.get("Rules", []))
|
|
75
|
+
|
|
76
|
+
# Create resource
|
|
77
|
+
resource = Resource(
|
|
78
|
+
arn=web_acl_arn,
|
|
79
|
+
resource_type="AWS::WAFv2::WebACL::Regional",
|
|
80
|
+
name=web_acl_name,
|
|
81
|
+
region=self.region,
|
|
82
|
+
tags=tags,
|
|
83
|
+
config_hash=compute_config_hash(config),
|
|
84
|
+
created_at=created_at,
|
|
85
|
+
raw_config=config,
|
|
86
|
+
)
|
|
87
|
+
resources.append(resource)
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
self.logger.debug(f"Error processing Web ACL {web_acl_name}: {e}")
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
self.logger.error(f"Error collecting regional WAF Web ACLs in {self.region}: {e}")
|
|
95
|
+
|
|
96
|
+
return resources
|
|
97
|
+
|
|
98
|
+
def _collect_cloudfront_web_acls(self) -> List[Resource]:
|
|
99
|
+
"""Collect CloudFront (global) WAFv2 Web ACLs.
|
|
100
|
+
|
|
101
|
+
Only collects when in us-east-1 since CloudFront resources are global.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of CloudFront Web ACL resources
|
|
105
|
+
"""
|
|
106
|
+
resources = []
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
client = self._create_client("wafv2")
|
|
110
|
+
|
|
111
|
+
paginator = client.get_paginator("list_web_acls")
|
|
112
|
+
for page in paginator.paginate(Scope="CLOUDFRONT"):
|
|
113
|
+
for web_acl_summary in page.get("WebACLs", []):
|
|
114
|
+
web_acl_name = web_acl_summary["Name"]
|
|
115
|
+
web_acl_id = web_acl_summary["Id"]
|
|
116
|
+
web_acl_arn = web_acl_summary["ARN"]
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
# Get detailed Web ACL info
|
|
120
|
+
web_acl_response = client.get_web_acl(Name=web_acl_name, Scope="CLOUDFRONT", Id=web_acl_id)
|
|
121
|
+
web_acl = web_acl_response.get("WebACL", {})
|
|
122
|
+
|
|
123
|
+
# Get tags
|
|
124
|
+
tags = {}
|
|
125
|
+
try:
|
|
126
|
+
tag_response = client.list_tags_for_resource(ResourceARN=web_acl_arn)
|
|
127
|
+
for tag_info in tag_response.get("TagInfoForResource", {}).get("TagList", []):
|
|
128
|
+
tags[tag_info["Key"]] = tag_info["Value"]
|
|
129
|
+
except Exception as e:
|
|
130
|
+
self.logger.debug(f"Could not get tags for CloudFront Web ACL {web_acl_name}: {e}")
|
|
131
|
+
|
|
132
|
+
# WAFv2 doesn't provide creation timestamp
|
|
133
|
+
created_at = None
|
|
134
|
+
|
|
135
|
+
# Remove large rule definitions for config hash
|
|
136
|
+
config = {k: v for k, v in web_acl.items() if k not in ["Rules"]}
|
|
137
|
+
config["RuleCount"] = len(web_acl.get("Rules", []))
|
|
138
|
+
|
|
139
|
+
# Create resource
|
|
140
|
+
resource = Resource(
|
|
141
|
+
arn=web_acl_arn,
|
|
142
|
+
resource_type="AWS::WAFv2::WebACL::CloudFront",
|
|
143
|
+
name=web_acl_name,
|
|
144
|
+
region="global", # CloudFront is global
|
|
145
|
+
tags=tags,
|
|
146
|
+
config_hash=compute_config_hash(config),
|
|
147
|
+
created_at=created_at,
|
|
148
|
+
raw_config=config,
|
|
149
|
+
)
|
|
150
|
+
resources.append(resource)
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
self.logger.debug(f"Error processing CloudFront Web ACL {web_acl_name}: {e}")
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
self.logger.error(f"Error collecting CloudFront WAF Web ACLs: {e}")
|
|
158
|
+
|
|
159
|
+
return resources
|
src/snapshot/storage.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Snapshot storage manager for saving and loading snapshots.
|
|
2
|
+
|
|
3
|
+
This module provides the main interface for snapshot persistence
|
|
4
|
+
using SQLite as the primary storage backend.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import gzip
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional, Union
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from ..models.snapshot import Snapshot
|
|
16
|
+
from ..storage import Database, SnapshotStore
|
|
17
|
+
from ..utils.paths import get_snapshot_storage_path
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SnapshotStorage:
|
|
23
|
+
"""Manages snapshot persistence using SQLite backend."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, storage_dir: Optional[Union[str, Path]] = None):
|
|
26
|
+
"""Initialize snapshot storage.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
storage_dir: Directory to store snapshots (default: ~/.snapshots via get_snapshot_storage_path())
|
|
30
|
+
"""
|
|
31
|
+
self.storage_dir = get_snapshot_storage_path(storage_dir)
|
|
32
|
+
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
self.active_file = self.storage_dir / ".active"
|
|
34
|
+
self.index_file = self.storage_dir / ".index.yaml"
|
|
35
|
+
|
|
36
|
+
# Initialize SQLite database
|
|
37
|
+
self.db = Database(storage_path=self.storage_dir)
|
|
38
|
+
self.db.ensure_schema()
|
|
39
|
+
|
|
40
|
+
# Initialize snapshot store
|
|
41
|
+
self._store = SnapshotStore(self.db)
|
|
42
|
+
|
|
43
|
+
def save_snapshot(self, snapshot: Snapshot, compress: bool = False) -> Path:
|
|
44
|
+
"""Save snapshot to SQLite database.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
snapshot: Snapshot instance to save
|
|
48
|
+
compress: Ignored (kept for backward compatibility)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Path to database file (for compatibility)
|
|
52
|
+
"""
|
|
53
|
+
# Save to SQLite
|
|
54
|
+
self._store.save(snapshot)
|
|
55
|
+
|
|
56
|
+
# Set as active if requested
|
|
57
|
+
if snapshot.is_active:
|
|
58
|
+
self._store.set_active(snapshot.name)
|
|
59
|
+
# Also update legacy active file for compatibility
|
|
60
|
+
self.active_file.write_text(snapshot.name)
|
|
61
|
+
|
|
62
|
+
logger.debug(f"Saved snapshot '{snapshot.name}' with {len(snapshot.resources)} resources to SQLite")
|
|
63
|
+
return self.db.db_path
|
|
64
|
+
|
|
65
|
+
def load_snapshot(self, snapshot_name: str) -> Snapshot:
|
|
66
|
+
"""Load snapshot from SQLite database.
|
|
67
|
+
|
|
68
|
+
Falls back to YAML files if not in database (for backward compatibility).
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
snapshot_name: Name of snapshot to load
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Snapshot instance
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
FileNotFoundError: If snapshot doesn't exist
|
|
78
|
+
"""
|
|
79
|
+
# Try SQLite first
|
|
80
|
+
snapshot = self._store.load(snapshot_name)
|
|
81
|
+
if snapshot:
|
|
82
|
+
logger.debug(f"Loaded snapshot '{snapshot_name}' from SQLite")
|
|
83
|
+
return snapshot
|
|
84
|
+
|
|
85
|
+
# Fall back to YAML for backward compatibility
|
|
86
|
+
snapshot = self._load_from_yaml(snapshot_name)
|
|
87
|
+
if snapshot:
|
|
88
|
+
# Migrate to SQLite on load
|
|
89
|
+
logger.info(f"Migrating snapshot '{snapshot_name}' from YAML to SQLite")
|
|
90
|
+
self._store.save(snapshot)
|
|
91
|
+
return snapshot
|
|
92
|
+
|
|
93
|
+
raise FileNotFoundError(f"Snapshot '{snapshot_name}' not found")
|
|
94
|
+
|
|
95
|
+
def _load_from_yaml(self, snapshot_name: str) -> Optional[Snapshot]:
|
|
96
|
+
"""Load snapshot from legacy YAML file.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
snapshot_name: Name of snapshot to load
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Snapshot instance or None if not found
|
|
103
|
+
"""
|
|
104
|
+
# Try compressed first
|
|
105
|
+
filepath_gz = self.storage_dir / f"{snapshot_name}.yaml.gz"
|
|
106
|
+
if filepath_gz.exists():
|
|
107
|
+
with gzip.open(filepath_gz, "rt", encoding="utf-8") as f:
|
|
108
|
+
snapshot_dict = yaml.safe_load(f)
|
|
109
|
+
logger.debug(f"Loaded compressed snapshot from {filepath_gz}")
|
|
110
|
+
return Snapshot.from_dict(snapshot_dict)
|
|
111
|
+
|
|
112
|
+
# Try uncompressed
|
|
113
|
+
filepath = self.storage_dir / f"{snapshot_name}.yaml"
|
|
114
|
+
if filepath.exists():
|
|
115
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
116
|
+
snapshot_dict = yaml.safe_load(f)
|
|
117
|
+
logger.debug(f"Loaded snapshot from {filepath}")
|
|
118
|
+
return Snapshot.from_dict(snapshot_dict)
|
|
119
|
+
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def list_snapshots(self) -> List[Dict[str, Any]]:
|
|
123
|
+
"""List all available snapshots with metadata.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
List of snapshot metadata dictionaries
|
|
127
|
+
"""
|
|
128
|
+
# Get snapshots from SQLite
|
|
129
|
+
snapshots = self._store.list_all()
|
|
130
|
+
|
|
131
|
+
# Get active snapshot name
|
|
132
|
+
active_name = self.get_active_snapshot_name()
|
|
133
|
+
|
|
134
|
+
# Convert to expected format and add is_active flag
|
|
135
|
+
result = []
|
|
136
|
+
for snap in snapshots:
|
|
137
|
+
result.append(
|
|
138
|
+
{
|
|
139
|
+
"name": snap["name"],
|
|
140
|
+
"filepath": str(self.db.db_path),
|
|
141
|
+
"size_mb": 0, # Not applicable for SQLite
|
|
142
|
+
"modified": snap["created_at"],
|
|
143
|
+
"is_active": (snap["name"] == active_name),
|
|
144
|
+
"created_at": snap["created_at"],
|
|
145
|
+
"account_id": snap["account_id"],
|
|
146
|
+
"regions": snap["regions"],
|
|
147
|
+
"resource_count": snap["resource_count"],
|
|
148
|
+
"service_counts": snap["service_counts"],
|
|
149
|
+
"inventory_name": snap.get("inventory_name", "default"),
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
logger.debug(f"Found {len(result)} snapshots")
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
def delete_snapshot(self, snapshot_name: str) -> bool:
|
|
157
|
+
"""Delete a snapshot.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
snapshot_name: Name of snapshot to delete
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if deleted successfully
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
ValueError: If trying to delete active snapshot
|
|
167
|
+
FileNotFoundError: If snapshot doesn't exist
|
|
168
|
+
"""
|
|
169
|
+
# Check if it's the active snapshot
|
|
170
|
+
if snapshot_name == self.get_active_snapshot_name():
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"Cannot delete active snapshot '{snapshot_name}'. " "Set another snapshot as active first."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Delete from SQLite
|
|
176
|
+
if self._store.delete(snapshot_name):
|
|
177
|
+
logger.debug(f"Deleted snapshot '{snapshot_name}' from SQLite")
|
|
178
|
+
|
|
179
|
+
# Also delete YAML files if they exist (cleanup)
|
|
180
|
+
self._delete_yaml_files(snapshot_name)
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
# Try deleting YAML files directly (legacy)
|
|
184
|
+
if self._delete_yaml_files(snapshot_name):
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
raise FileNotFoundError(f"Snapshot '{snapshot_name}' not found")
|
|
188
|
+
|
|
189
|
+
def _delete_yaml_files(self, snapshot_name: str) -> bool:
|
|
190
|
+
"""Delete legacy YAML files for a snapshot.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
snapshot_name: Name of snapshot
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
True if any files were deleted
|
|
197
|
+
"""
|
|
198
|
+
deleted = False
|
|
199
|
+
|
|
200
|
+
filepath_gz = self.storage_dir / f"{snapshot_name}.yaml.gz"
|
|
201
|
+
if filepath_gz.exists():
|
|
202
|
+
filepath_gz.unlink()
|
|
203
|
+
deleted = True
|
|
204
|
+
logger.debug(f"Deleted {filepath_gz}")
|
|
205
|
+
|
|
206
|
+
filepath = self.storage_dir / f"{snapshot_name}.yaml"
|
|
207
|
+
if filepath.exists():
|
|
208
|
+
filepath.unlink()
|
|
209
|
+
deleted = True
|
|
210
|
+
logger.debug(f"Deleted {filepath}")
|
|
211
|
+
|
|
212
|
+
return deleted
|
|
213
|
+
|
|
214
|
+
def get_active_snapshot_name(self) -> Optional[str]:
|
|
215
|
+
"""Get the name of the currently active snapshot.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Active snapshot name, or None if no active snapshot
|
|
219
|
+
"""
|
|
220
|
+
# Try SQLite first
|
|
221
|
+
active = self._store.get_active()
|
|
222
|
+
if active:
|
|
223
|
+
return active
|
|
224
|
+
|
|
225
|
+
# Fall back to legacy file
|
|
226
|
+
if self.active_file.exists():
|
|
227
|
+
return self.active_file.read_text().strip()
|
|
228
|
+
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
def set_active_snapshot(self, snapshot_name: str) -> None:
|
|
232
|
+
"""Set a snapshot as the active baseline.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
snapshot_name: Name of snapshot to set as active
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
FileNotFoundError: If snapshot doesn't exist
|
|
239
|
+
"""
|
|
240
|
+
# Verify snapshot exists (in SQLite or YAML)
|
|
241
|
+
if not self._store.exists(snapshot_name):
|
|
242
|
+
# Try loading from YAML (will migrate to SQLite)
|
|
243
|
+
try:
|
|
244
|
+
self.load_snapshot(snapshot_name)
|
|
245
|
+
except FileNotFoundError:
|
|
246
|
+
raise FileNotFoundError(f"Cannot set active: snapshot '{snapshot_name}' not found")
|
|
247
|
+
|
|
248
|
+
# Update in SQLite
|
|
249
|
+
self._store.set_active(snapshot_name)
|
|
250
|
+
|
|
251
|
+
# Update legacy active file for compatibility
|
|
252
|
+
self.active_file.write_text(snapshot_name)
|
|
253
|
+
logger.debug(f"Set active snapshot to: {snapshot_name}")
|
|
254
|
+
|
|
255
|
+
def snapshot_exists(self, snapshot_name: str) -> bool:
|
|
256
|
+
"""Check if a snapshot exists.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
snapshot_name: Name of snapshot
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
True if snapshot exists
|
|
263
|
+
"""
|
|
264
|
+
# Check SQLite
|
|
265
|
+
if self._store.exists(snapshot_name):
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
# Check YAML files
|
|
269
|
+
filepath_gz = self.storage_dir / f"{snapshot_name}.yaml.gz"
|
|
270
|
+
filepath = self.storage_dir / f"{snapshot_name}.yaml"
|
|
271
|
+
return filepath_gz.exists() or filepath.exists()
|
|
272
|
+
|
|
273
|
+
def exists(self, snapshot_name: str) -> bool:
|
|
274
|
+
"""Alias for snapshot_exists for consistency.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
snapshot_name: Name of snapshot
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
True if snapshot exists
|
|
281
|
+
"""
|
|
282
|
+
return self.snapshot_exists(snapshot_name)
|
|
283
|
+
|
|
284
|
+
def rename_snapshot(self, old_name: str, new_name: str) -> bool:
|
|
285
|
+
"""Rename a snapshot.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
old_name: Current snapshot name
|
|
289
|
+
new_name: New snapshot name
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
True if renamed successfully
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
FileNotFoundError: If old_name doesn't exist
|
|
296
|
+
ValueError: If new_name already exists
|
|
297
|
+
"""
|
|
298
|
+
if not self.snapshot_exists(old_name):
|
|
299
|
+
raise FileNotFoundError(f"Snapshot '{old_name}' not found")
|
|
300
|
+
|
|
301
|
+
if self.snapshot_exists(new_name):
|
|
302
|
+
raise ValueError(f"Snapshot '{new_name}' already exists")
|
|
303
|
+
|
|
304
|
+
# Rename in SQLite
|
|
305
|
+
if self._store.exists(old_name):
|
|
306
|
+
self._store.rename(old_name, new_name)
|
|
307
|
+
|
|
308
|
+
# Update active snapshot reference if needed
|
|
309
|
+
if self.get_active_snapshot_name() == old_name:
|
|
310
|
+
self._store.set_active(new_name)
|
|
311
|
+
self.active_file.write_text(new_name)
|
|
312
|
+
|
|
313
|
+
logger.debug(f"Renamed snapshot '{old_name}' to '{new_name}' in SQLite")
|
|
314
|
+
return True
|
|
315
|
+
|
|
316
|
+
# Handle YAML files (legacy)
|
|
317
|
+
renamed = False
|
|
318
|
+
for ext in [".yaml.gz", ".yaml"]:
|
|
319
|
+
old_path = self.storage_dir / f"{old_name}{ext}"
|
|
320
|
+
new_path = self.storage_dir / f"{new_name}{ext}"
|
|
321
|
+
if old_path.exists():
|
|
322
|
+
old_path.rename(new_path)
|
|
323
|
+
renamed = True
|
|
324
|
+
logger.debug(f"Renamed {old_path} to {new_path}")
|
|
325
|
+
break
|
|
326
|
+
|
|
327
|
+
# Update active if needed
|
|
328
|
+
if renamed and self.get_active_snapshot_name() == old_name:
|
|
329
|
+
self.active_file.write_text(new_name)
|
|
330
|
+
|
|
331
|
+
return renamed
|
|
332
|
+
|
|
333
|
+
# Legacy index methods (kept for backward compatibility)
|
|
334
|
+
def _update_index(self, snapshot: Snapshot) -> None:
|
|
335
|
+
"""Update the snapshot index file (no-op for SQLite)."""
|
|
336
|
+
pass # Index is now managed by SQLite
|
|
337
|
+
|
|
338
|
+
def _remove_from_index(self, snapshot_name: str) -> None:
|
|
339
|
+
"""Remove snapshot from index (no-op for SQLite)."""
|
|
340
|
+
pass # Index is now managed by SQLite
|
|
341
|
+
|
|
342
|
+
def _load_index(self) -> Dict[str, Any]:
|
|
343
|
+
"""Load snapshot index from file (deprecated)."""
|
|
344
|
+
if self.index_file.exists():
|
|
345
|
+
with open(self.index_file, "r") as f:
|
|
346
|
+
return yaml.safe_load(f) or {}
|
|
347
|
+
return {}
|
|
348
|
+
|
|
349
|
+
def _save_index(self, index: Dict[str, Any]) -> None:
|
|
350
|
+
"""Save snapshot index to file (deprecated)."""
|
|
351
|
+
pass # Index is now managed by SQLite
|
src/storage/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""SQLite storage layer for AWS Inventory Manager."""
|
|
2
|
+
|
|
3
|
+
from .audit_store import AuditStore
|
|
4
|
+
from .database import Database, json_deserialize, json_serialize
|
|
5
|
+
from .group_store import GroupStore
|
|
6
|
+
from .inventory_store import InventoryStore
|
|
7
|
+
from .resource_store import ResourceStore
|
|
8
|
+
from .schema import SCHEMA_VERSION
|
|
9
|
+
from .snapshot_store import SnapshotStore
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Database",
|
|
13
|
+
"SCHEMA_VERSION",
|
|
14
|
+
"SnapshotStore",
|
|
15
|
+
"ResourceStore",
|
|
16
|
+
"InventoryStore",
|
|
17
|
+
"AuditStore",
|
|
18
|
+
"GroupStore",
|
|
19
|
+
"json_serialize",
|
|
20
|
+
"json_deserialize",
|
|
21
|
+
]
|