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.

Files changed (145) hide show
  1. aws_inventory_manager-0.13.2.dist-info/LICENSE +21 -0
  2. aws_inventory_manager-0.13.2.dist-info/METADATA +1226 -0
  3. aws_inventory_manager-0.13.2.dist-info/RECORD +145 -0
  4. aws_inventory_manager-0.13.2.dist-info/WHEEL +5 -0
  5. aws_inventory_manager-0.13.2.dist-info/entry_points.txt +2 -0
  6. aws_inventory_manager-0.13.2.dist-info/top_level.txt +1 -0
  7. src/__init__.py +3 -0
  8. src/aws/__init__.py +11 -0
  9. src/aws/client.py +128 -0
  10. src/aws/credentials.py +191 -0
  11. src/aws/rate_limiter.py +177 -0
  12. src/cli/__init__.py +12 -0
  13. src/cli/config.py +130 -0
  14. src/cli/main.py +3626 -0
  15. src/config_service/__init__.py +21 -0
  16. src/config_service/collector.py +346 -0
  17. src/config_service/detector.py +256 -0
  18. src/config_service/resource_type_mapping.py +328 -0
  19. src/cost/__init__.py +5 -0
  20. src/cost/analyzer.py +226 -0
  21. src/cost/explorer.py +209 -0
  22. src/cost/reporter.py +237 -0
  23. src/delta/__init__.py +5 -0
  24. src/delta/calculator.py +206 -0
  25. src/delta/differ.py +185 -0
  26. src/delta/formatters.py +272 -0
  27. src/delta/models.py +154 -0
  28. src/delta/reporter.py +234 -0
  29. src/models/__init__.py +21 -0
  30. src/models/config_diff.py +135 -0
  31. src/models/cost_report.py +87 -0
  32. src/models/deletion_operation.py +104 -0
  33. src/models/deletion_record.py +97 -0
  34. src/models/delta_report.py +122 -0
  35. src/models/efs_resource.py +80 -0
  36. src/models/elasticache_resource.py +90 -0
  37. src/models/group.py +318 -0
  38. src/models/inventory.py +133 -0
  39. src/models/protection_rule.py +123 -0
  40. src/models/report.py +288 -0
  41. src/models/resource.py +111 -0
  42. src/models/security_finding.py +102 -0
  43. src/models/snapshot.py +122 -0
  44. src/restore/__init__.py +20 -0
  45. src/restore/audit.py +175 -0
  46. src/restore/cleaner.py +461 -0
  47. src/restore/config.py +209 -0
  48. src/restore/deleter.py +976 -0
  49. src/restore/dependency.py +254 -0
  50. src/restore/safety.py +115 -0
  51. src/security/__init__.py +0 -0
  52. src/security/checks/__init__.py +0 -0
  53. src/security/checks/base.py +56 -0
  54. src/security/checks/ec2_checks.py +88 -0
  55. src/security/checks/elasticache_checks.py +149 -0
  56. src/security/checks/iam_checks.py +102 -0
  57. src/security/checks/rds_checks.py +140 -0
  58. src/security/checks/s3_checks.py +95 -0
  59. src/security/checks/secrets_checks.py +96 -0
  60. src/security/checks/sg_checks.py +142 -0
  61. src/security/cis_mapper.py +97 -0
  62. src/security/models.py +53 -0
  63. src/security/reporter.py +174 -0
  64. src/security/scanner.py +87 -0
  65. src/snapshot/__init__.py +6 -0
  66. src/snapshot/capturer.py +451 -0
  67. src/snapshot/filter.py +259 -0
  68. src/snapshot/inventory_storage.py +236 -0
  69. src/snapshot/report_formatter.py +250 -0
  70. src/snapshot/reporter.py +189 -0
  71. src/snapshot/resource_collectors/__init__.py +5 -0
  72. src/snapshot/resource_collectors/apigateway.py +140 -0
  73. src/snapshot/resource_collectors/backup.py +136 -0
  74. src/snapshot/resource_collectors/base.py +81 -0
  75. src/snapshot/resource_collectors/cloudformation.py +55 -0
  76. src/snapshot/resource_collectors/cloudwatch.py +109 -0
  77. src/snapshot/resource_collectors/codebuild.py +69 -0
  78. src/snapshot/resource_collectors/codepipeline.py +82 -0
  79. src/snapshot/resource_collectors/dynamodb.py +65 -0
  80. src/snapshot/resource_collectors/ec2.py +240 -0
  81. src/snapshot/resource_collectors/ecs.py +215 -0
  82. src/snapshot/resource_collectors/efs_collector.py +102 -0
  83. src/snapshot/resource_collectors/eks.py +200 -0
  84. src/snapshot/resource_collectors/elasticache_collector.py +79 -0
  85. src/snapshot/resource_collectors/elb.py +126 -0
  86. src/snapshot/resource_collectors/eventbridge.py +156 -0
  87. src/snapshot/resource_collectors/iam.py +188 -0
  88. src/snapshot/resource_collectors/kms.py +111 -0
  89. src/snapshot/resource_collectors/lambda_func.py +139 -0
  90. src/snapshot/resource_collectors/rds.py +109 -0
  91. src/snapshot/resource_collectors/route53.py +86 -0
  92. src/snapshot/resource_collectors/s3.py +105 -0
  93. src/snapshot/resource_collectors/secretsmanager.py +70 -0
  94. src/snapshot/resource_collectors/sns.py +68 -0
  95. src/snapshot/resource_collectors/sqs.py +82 -0
  96. src/snapshot/resource_collectors/ssm.py +160 -0
  97. src/snapshot/resource_collectors/stepfunctions.py +74 -0
  98. src/snapshot/resource_collectors/vpcendpoints.py +79 -0
  99. src/snapshot/resource_collectors/waf.py +159 -0
  100. src/snapshot/storage.py +351 -0
  101. src/storage/__init__.py +21 -0
  102. src/storage/audit_store.py +419 -0
  103. src/storage/database.py +294 -0
  104. src/storage/group_store.py +749 -0
  105. src/storage/inventory_store.py +320 -0
  106. src/storage/resource_store.py +413 -0
  107. src/storage/schema.py +288 -0
  108. src/storage/snapshot_store.py +346 -0
  109. src/utils/__init__.py +12 -0
  110. src/utils/export.py +305 -0
  111. src/utils/hash.py +60 -0
  112. src/utils/logging.py +63 -0
  113. src/utils/pagination.py +41 -0
  114. src/utils/paths.py +51 -0
  115. src/utils/progress.py +41 -0
  116. src/utils/unsupported_resources.py +306 -0
  117. src/web/__init__.py +5 -0
  118. src/web/app.py +97 -0
  119. src/web/dependencies.py +69 -0
  120. src/web/routes/__init__.py +1 -0
  121. src/web/routes/api/__init__.py +18 -0
  122. src/web/routes/api/charts.py +156 -0
  123. src/web/routes/api/cleanup.py +186 -0
  124. src/web/routes/api/filters.py +253 -0
  125. src/web/routes/api/groups.py +305 -0
  126. src/web/routes/api/inventories.py +80 -0
  127. src/web/routes/api/queries.py +202 -0
  128. src/web/routes/api/resources.py +379 -0
  129. src/web/routes/api/snapshots.py +314 -0
  130. src/web/routes/api/views.py +260 -0
  131. src/web/routes/pages.py +198 -0
  132. src/web/services/__init__.py +1 -0
  133. src/web/templates/base.html +949 -0
  134. src/web/templates/components/navbar.html +31 -0
  135. src/web/templates/components/sidebar.html +104 -0
  136. src/web/templates/pages/audit_logs.html +86 -0
  137. src/web/templates/pages/cleanup.html +279 -0
  138. src/web/templates/pages/dashboard.html +227 -0
  139. src/web/templates/pages/diff.html +175 -0
  140. src/web/templates/pages/error.html +30 -0
  141. src/web/templates/pages/groups.html +721 -0
  142. src/web/templates/pages/queries.html +246 -0
  143. src/web/templates/pages/resources.html +2251 -0
  144. src/web/templates/pages/snapshot_detail.html +271 -0
  145. 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
@@ -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
@@ -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
+ ]