backuplint 1.0.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.
- backuplint/__init__.py +3 -0
- backuplint/__main__.py +6 -0
- backuplint/audit.py +293 -0
- backuplint/classify.py +109 -0
- backuplint/cli.py +1444 -0
- backuplint/compose.py +215 -0
- backuplint/config.py +531 -0
- backuplint/coverage.py +386 -0
- backuplint/database.py +39 -0
- backuplint/engine/__init__.py +22 -0
- backuplint/engine/base.py +43 -0
- backuplint/engine/factory.py +56 -0
- backuplint/engine/restic_engine.py +139 -0
- backuplint/engine/types.py +43 -0
- backuplint/errors.py +11 -0
- backuplint/events.py +253 -0
- backuplint/fleet/__init__.py +19 -0
- backuplint/fleet/agent.py +515 -0
- backuplint/fleet/capabilities.py +266 -0
- backuplint/fleet/certs.py +346 -0
- backuplint/fleet/compat.py +104 -0
- backuplint/fleet/controller.py +784 -0
- backuplint/fleet/controller_store.py +1476 -0
- backuplint/fleet/dashboard/__init__.py +14 -0
- backuplint/fleet/dashboard/assurance.py +478 -0
- backuplint/fleet/dashboard/auth.py +215 -0
- backuplint/fleet/dashboard/config.py +31 -0
- backuplint/fleet/dashboard/format.py +173 -0
- backuplint/fleet/dashboard/http.py +562 -0
- backuplint/fleet/dashboard/query.py +1288 -0
- backuplint/fleet/dashboard/static/logo-mark-64.png +0 -0
- backuplint/fleet/dashboard/static/logo-mark-white-64.png +0 -0
- backuplint/fleet/dashboard/static/logo-mark.svg +4 -0
- backuplint/fleet/dashboard/ui.py +1583 -0
- backuplint/fleet/delivery.py +260 -0
- backuplint/fleet/policy_apply.py +346 -0
- backuplint/fleet/policy_store.py +1199 -0
- backuplint/fleet/presence.py +44 -0
- backuplint/fleet/protocol.py +178 -0
- backuplint/fleet/provisioning.py +114 -0
- backuplint/fleet/queue_policy.py +319 -0
- backuplint/fleet/telemetry.py +264 -0
- backuplint/fleet_config.py +44 -0
- backuplint/install/__init__.py +50 -0
- backuplint/install/deployment.py +29 -0
- backuplint/install/detect.py +162 -0
- backuplint/install/diagnostics.py +132 -0
- backuplint/install/distro.py +85 -0
- backuplint/install/executor.py +580 -0
- backuplint/install/features.py +231 -0
- backuplint/install/layout.py +296 -0
- backuplint/install/manifest.py +137 -0
- backuplint/install/packages.py +165 -0
- backuplint/install/plan.py +226 -0
- backuplint/install/profile.py +275 -0
- backuplint/install/roles.py +99 -0
- backuplint/install/support.py +86 -0
- backuplint/install/systemd_units.py +148 -0
- backuplint/install/wizard.py +143 -0
- backuplint/integrity_state.py +206 -0
- backuplint/models.py +45 -0
- backuplint/paths.py +74 -0
- backuplint/policy/__init__.py +27 -0
- backuplint/policy/cli.py +400 -0
- backuplint/policy/errors.py +7 -0
- backuplint/policy/gitops.py +89 -0
- backuplint/policy/resolve.py +79 -0
- backuplint/policy/rollout.py +87 -0
- backuplint/policy/schema.py +213 -0
- backuplint/policy/settings.py +220 -0
- backuplint/process.py +44 -0
- backuplint/reporting.py +387 -0
- backuplint/restic.py +494 -0
- backuplint/restic_errors.py +175 -0
- backuplint/restore_dest.py +256 -0
- backuplint/restore_verify.py +416 -0
- backuplint/schedule_config.py +181 -0
- backuplint/schedule_lock.py +96 -0
- backuplint/schedule_store.py +416 -0
- backuplint/scheduler.py +591 -0
- backuplint/secrets/__init__.py +65 -0
- backuplint/secrets/errors.py +35 -0
- backuplint/secrets/file_checks.py +193 -0
- backuplint/secrets/ref.py +190 -0
- backuplint/secrets/resolve.py +146 -0
- backuplint/secrets/value.py +53 -0
- backuplint/siem/__init__.py +35 -0
- backuplint/siem/config.py +375 -0
- backuplint/siem/event.py +571 -0
- backuplint/siem/exporter.py +224 -0
- backuplint/siem/queue.py +602 -0
- backuplint/siem/runtime.py +241 -0
- backuplint/siem/telemetry.py +129 -0
- backuplint/siem/transport/__init__.py +6 -0
- backuplint/siem/transport/base.py +78 -0
- backuplint/siem/transport/https_json.py +186 -0
- backuplint/sqlite_backup.py +530 -0
- backuplint/sqlite_util.py +51 -0
- backuplint/timeutil.py +100 -0
- backuplint/volumes.py +43 -0
- backuplint-1.0.0.dist-info/METADATA +318 -0
- backuplint-1.0.0.dist-info/RECORD +106 -0
- backuplint-1.0.0.dist-info/WHEEL +5 -0
- backuplint-1.0.0.dist-info/entry_points.txt +2 -0
- backuplint-1.0.0.dist-info/licenses/LICENSE +21 -0
- backuplint-1.0.0.dist-info/top_level.txt +1 -0
backuplint/__init__.py
ADDED
backuplint/__main__.py
ADDED
backuplint/audit.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Application orchestration for a BackupLint scan (CLI-independent)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from backuplint.compose import discover_mounts
|
|
9
|
+
from backuplint.config import (
|
|
10
|
+
BackupLintConfig,
|
|
11
|
+
ConfigError,
|
|
12
|
+
IntegrityConfig,
|
|
13
|
+
IntegrityMode,
|
|
14
|
+
RestoreVerificationMode,
|
|
15
|
+
default_config_path,
|
|
16
|
+
load_config,
|
|
17
|
+
parse_integrity_mode,
|
|
18
|
+
parse_restore_verification_mode,
|
|
19
|
+
)
|
|
20
|
+
from backuplint.coverage import (
|
|
21
|
+
CoverageFinding,
|
|
22
|
+
CoverageStatus,
|
|
23
|
+
evaluate_coverage,
|
|
24
|
+
evaluate_coverage_restic,
|
|
25
|
+
)
|
|
26
|
+
from backuplint.engine import BackupEngineError, get_backup_engine
|
|
27
|
+
from backuplint.integrity_state import (
|
|
28
|
+
IntegrityStateError,
|
|
29
|
+
is_integrity_success_stale,
|
|
30
|
+
last_success_for_repository,
|
|
31
|
+
load_integrity_state,
|
|
32
|
+
record_integrity_success,
|
|
33
|
+
)
|
|
34
|
+
from backuplint.models import ServiceMounts
|
|
35
|
+
from backuplint.reporting import AuditSummary, summarize_findings
|
|
36
|
+
from backuplint.restic import (
|
|
37
|
+
IntegrityCheckResult,
|
|
38
|
+
IntegrityStatus,
|
|
39
|
+
ResticError,
|
|
40
|
+
)
|
|
41
|
+
from backuplint.restore_verify import (
|
|
42
|
+
RestoreVerificationResult,
|
|
43
|
+
RestoreVerificationStatus,
|
|
44
|
+
verify_restore,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class AuditOutcome:
|
|
50
|
+
"""Result of a full audit orchestration pass."""
|
|
51
|
+
|
|
52
|
+
findings: list[CoverageFinding]
|
|
53
|
+
summary: AuditSummary
|
|
54
|
+
integrity: IntegrityCheckResult | None
|
|
55
|
+
restore: RestoreVerificationResult | None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_integrity_mode(
|
|
59
|
+
*,
|
|
60
|
+
configured: IntegrityMode,
|
|
61
|
+
cli_override: str | None,
|
|
62
|
+
restic_configured: bool,
|
|
63
|
+
) -> IntegrityMode:
|
|
64
|
+
"""CLI option overrides config; config overrides the safe default (off)."""
|
|
65
|
+
if cli_override is not None:
|
|
66
|
+
mode = parse_integrity_mode(cli_override)
|
|
67
|
+
else:
|
|
68
|
+
mode = configured
|
|
69
|
+
if mode is not IntegrityMode.OFF and not restic_configured:
|
|
70
|
+
raise ConfigError(
|
|
71
|
+
"Integrity checking requires a 'restic' configuration section."
|
|
72
|
+
)
|
|
73
|
+
return mode
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_restore_mode(
|
|
77
|
+
*,
|
|
78
|
+
configured: RestoreVerificationMode,
|
|
79
|
+
cli_override: str | None,
|
|
80
|
+
restic_configured: bool,
|
|
81
|
+
) -> RestoreVerificationMode:
|
|
82
|
+
"""CLI option overrides config; config overrides the safe default (off)."""
|
|
83
|
+
if cli_override is not None:
|
|
84
|
+
mode = parse_restore_verification_mode(cli_override)
|
|
85
|
+
else:
|
|
86
|
+
mode = configured
|
|
87
|
+
if mode is not RestoreVerificationMode.OFF and not restic_configured:
|
|
88
|
+
raise ConfigError(
|
|
89
|
+
"Restore verification requires a 'restic' configuration section."
|
|
90
|
+
)
|
|
91
|
+
return mode
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def relevant_restore_paths(findings: list[CoverageFinding]) -> tuple[str, ...]:
|
|
95
|
+
"""Host paths eligible for restore verification (historical selection rules)."""
|
|
96
|
+
paths: list[str] = []
|
|
97
|
+
seen: set[str] = set()
|
|
98
|
+
for finding in findings:
|
|
99
|
+
if finding.status not in {CoverageStatus.PROTECTED, CoverageStatus.STALE}:
|
|
100
|
+
continue
|
|
101
|
+
if not finding.host_path:
|
|
102
|
+
continue
|
|
103
|
+
if finding.host_path in seen:
|
|
104
|
+
continue
|
|
105
|
+
seen.add(finding.host_path)
|
|
106
|
+
paths.append(finding.host_path)
|
|
107
|
+
return tuple(paths)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def stale_integrity_from_state(
|
|
111
|
+
*,
|
|
112
|
+
repository: str,
|
|
113
|
+
integrity_cfg: IntegrityConfig,
|
|
114
|
+
) -> IntegrityCheckResult | None:
|
|
115
|
+
"""When live checks are off, optionally WARN from remembered success age."""
|
|
116
|
+
if integrity_cfg.max_age is None or integrity_cfg.state_file is None:
|
|
117
|
+
return None
|
|
118
|
+
try:
|
|
119
|
+
records = load_integrity_state(integrity_cfg.state_file)
|
|
120
|
+
except IntegrityStateError as exc:
|
|
121
|
+
raise ResticError(exc.message) from exc
|
|
122
|
+
record = last_success_for_repository(records, repository)
|
|
123
|
+
if is_integrity_success_stale(record, max_age=integrity_cfg.max_age):
|
|
124
|
+
return IntegrityCheckResult(
|
|
125
|
+
mode=IntegrityMode.OFF,
|
|
126
|
+
status=IntegrityStatus.STALE,
|
|
127
|
+
duration_seconds=0.0,
|
|
128
|
+
message=(
|
|
129
|
+
"last successful integrity verification is older than configured max_age"
|
|
130
|
+
),
|
|
131
|
+
requested=False,
|
|
132
|
+
)
|
|
133
|
+
return IntegrityCheckResult(
|
|
134
|
+
mode=record.mode if record is not None else IntegrityMode.OFF,
|
|
135
|
+
status=IntegrityStatus.NOT_REQUESTED,
|
|
136
|
+
duration_seconds=0.0,
|
|
137
|
+
message="Integrity verification was not requested.",
|
|
138
|
+
requested=False,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def run_audit(
|
|
143
|
+
compose_file: Path,
|
|
144
|
+
*,
|
|
145
|
+
config_path: Path | None = None,
|
|
146
|
+
integrity_cli: str | None = None,
|
|
147
|
+
restore_verify_cli: str | None = None,
|
|
148
|
+
) -> AuditOutcome:
|
|
149
|
+
"""Discover mounts, evaluate coverage/integrity/restore, return summary."""
|
|
150
|
+
resolved_config = (
|
|
151
|
+
config_path if config_path is not None else default_config_path(compose_file)
|
|
152
|
+
)
|
|
153
|
+
backup_config = load_config(resolved_config)
|
|
154
|
+
services = discover_mounts(
|
|
155
|
+
compose_file, extra_files=backup_config.compose_files
|
|
156
|
+
)
|
|
157
|
+
return run_audit_with_config(
|
|
158
|
+
services=services,
|
|
159
|
+
backup_config=backup_config,
|
|
160
|
+
integrity_cli=integrity_cli,
|
|
161
|
+
restore_verify_cli=restore_verify_cli,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def run_audit_with_config(
|
|
166
|
+
*,
|
|
167
|
+
services: list[ServiceMounts],
|
|
168
|
+
backup_config: BackupLintConfig,
|
|
169
|
+
integrity_cli: str | None = None,
|
|
170
|
+
restore_verify_cli: str | None = None,
|
|
171
|
+
) -> AuditOutcome:
|
|
172
|
+
configured_mode = (
|
|
173
|
+
backup_config.restic.integrity.mode
|
|
174
|
+
if backup_config.restic is not None
|
|
175
|
+
else IntegrityMode.OFF
|
|
176
|
+
)
|
|
177
|
+
integrity_mode = resolve_integrity_mode(
|
|
178
|
+
configured=configured_mode,
|
|
179
|
+
cli_override=integrity_cli,
|
|
180
|
+
restic_configured=backup_config.restic is not None,
|
|
181
|
+
)
|
|
182
|
+
configured_restore = (
|
|
183
|
+
backup_config.restic.restore_verification.mode
|
|
184
|
+
if backup_config.restic is not None
|
|
185
|
+
else RestoreVerificationMode.OFF
|
|
186
|
+
)
|
|
187
|
+
restore_mode = resolve_restore_mode(
|
|
188
|
+
configured=configured_restore,
|
|
189
|
+
cli_override=restore_verify_cli,
|
|
190
|
+
restic_configured=backup_config.restic is not None,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
integrity_result: IntegrityCheckResult | None = None
|
|
194
|
+
restore_result: RestoreVerificationResult | None = None
|
|
195
|
+
|
|
196
|
+
if backup_config.restic is not None:
|
|
197
|
+
engine = get_backup_engine(backup_config.restic)
|
|
198
|
+
try:
|
|
199
|
+
snapshots = engine.list_snapshots()
|
|
200
|
+
except BackupEngineError as exc:
|
|
201
|
+
# Preserve historical CLI/scheduler raise semantics (ResticError).
|
|
202
|
+
raise ResticError(exc.message) from exc
|
|
203
|
+
findings = evaluate_coverage_restic(
|
|
204
|
+
services,
|
|
205
|
+
snapshots,
|
|
206
|
+
max_backup_age=backup_config.max_backup_age,
|
|
207
|
+
)
|
|
208
|
+
integrity_cfg = backup_config.restic.integrity
|
|
209
|
+
if integrity_mode is not IntegrityMode.OFF:
|
|
210
|
+
# Explicit live check — never silently downgrade deep to standard.
|
|
211
|
+
try:
|
|
212
|
+
integrity_result = engine.integrity_check(integrity_mode)
|
|
213
|
+
except BackupEngineError as exc:
|
|
214
|
+
raise ResticError(exc.message) from exc
|
|
215
|
+
if integrity_result.status is IntegrityStatus.ERROR:
|
|
216
|
+
raise ResticError(integrity_result.message)
|
|
217
|
+
if (
|
|
218
|
+
integrity_result.status is IntegrityStatus.PASSED
|
|
219
|
+
and integrity_cfg.state_file is not None
|
|
220
|
+
):
|
|
221
|
+
try:
|
|
222
|
+
record_integrity_success(
|
|
223
|
+
integrity_cfg.state_file,
|
|
224
|
+
repository=backup_config.restic.repository,
|
|
225
|
+
mode=integrity_result.mode,
|
|
226
|
+
checked_at=integrity_result.checked_at,
|
|
227
|
+
restic_version=integrity_result.restic_version,
|
|
228
|
+
)
|
|
229
|
+
except IntegrityStateError as exc:
|
|
230
|
+
raise ResticError(exc.message) from exc
|
|
231
|
+
else:
|
|
232
|
+
integrity_result = stale_integrity_from_state(
|
|
233
|
+
repository=backup_config.restic.repository,
|
|
234
|
+
integrity_cfg=integrity_cfg,
|
|
235
|
+
)
|
|
236
|
+
if integrity_result is None:
|
|
237
|
+
integrity_result = IntegrityCheckResult(
|
|
238
|
+
mode=IntegrityMode.OFF,
|
|
239
|
+
status=IntegrityStatus.NOT_REQUESTED,
|
|
240
|
+
duration_seconds=0.0,
|
|
241
|
+
message="Integrity verification was not requested.",
|
|
242
|
+
requested=False,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
restore_cfg = backup_config.restic.restore_verification
|
|
246
|
+
if restore_mode is not RestoreVerificationMode.OFF:
|
|
247
|
+
paths = relevant_restore_paths(findings)
|
|
248
|
+
live_binds = tuple(Path(p) for p in paths)
|
|
249
|
+
timeout_seconds = (
|
|
250
|
+
restore_cfg.timeout.total_seconds()
|
|
251
|
+
if restore_cfg.timeout is not None
|
|
252
|
+
else None
|
|
253
|
+
)
|
|
254
|
+
restore_result = verify_restore(
|
|
255
|
+
mode=restore_mode,
|
|
256
|
+
repository=backup_config.restic.repository,
|
|
257
|
+
snapshots=snapshots,
|
|
258
|
+
relevant_paths=paths,
|
|
259
|
+
password_file=backup_config.restic.password_file,
|
|
260
|
+
password_ref=backup_config.restic.password,
|
|
261
|
+
timeout=timeout_seconds,
|
|
262
|
+
expected_paths=restore_cfg.expected_paths,
|
|
263
|
+
live_bind_paths=live_binds,
|
|
264
|
+
)
|
|
265
|
+
if restore_result.status is RestoreVerificationStatus.ERROR:
|
|
266
|
+
raise ResticError(restore_result.message)
|
|
267
|
+
else:
|
|
268
|
+
restore_result = RestoreVerificationResult(
|
|
269
|
+
mode=RestoreVerificationMode.OFF,
|
|
270
|
+
status=RestoreVerificationStatus.NOT_REQUESTED,
|
|
271
|
+
duration_seconds=0.0,
|
|
272
|
+
message="Restore verification was not requested.",
|
|
273
|
+
requested=False,
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
findings = evaluate_coverage(services, backup_config.backup_paths)
|
|
277
|
+
restore_result = RestoreVerificationResult(
|
|
278
|
+
mode=RestoreVerificationMode.OFF,
|
|
279
|
+
status=RestoreVerificationStatus.NOT_REQUESTED,
|
|
280
|
+
duration_seconds=0.0,
|
|
281
|
+
message="Restore verification was not requested.",
|
|
282
|
+
requested=False,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
summary = summarize_findings(
|
|
286
|
+
findings, integrity=integrity_result, restore=restore_result
|
|
287
|
+
)
|
|
288
|
+
return AuditOutcome(
|
|
289
|
+
findings=findings,
|
|
290
|
+
summary=summary,
|
|
291
|
+
integrity=integrity_result,
|
|
292
|
+
restore=restore_result,
|
|
293
|
+
)
|
backuplint/classify.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Classify discovered mounts as persistent, cache, temporary, or unknown."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
from backuplint.models import Mount, MountType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StorageClass(StrEnum):
|
|
11
|
+
PERSISTENT = "persistent"
|
|
12
|
+
CACHE = "cache"
|
|
13
|
+
TEMPORARY = "temporary"
|
|
14
|
+
INFRASTRUCTURE = "infrastructure"
|
|
15
|
+
UNKNOWN = "unknown"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Conservative, explicit rules. Prefer unknown over silent ignore.
|
|
19
|
+
# These are container mount target patterns, not host temp-file usage.
|
|
20
|
+
_CACHE_TARGET_HINTS = (
|
|
21
|
+
"/cache",
|
|
22
|
+
"/.cache",
|
|
23
|
+
"/var/cache",
|
|
24
|
+
"/tmp/cache", # nosec B108
|
|
25
|
+
)
|
|
26
|
+
_TEMPORARY_TARGET_HINTS = (
|
|
27
|
+
"/tmp", # nosec B108
|
|
28
|
+
"/var/tmp", # nosec B108
|
|
29
|
+
"/dev/shm", # nosec B108
|
|
30
|
+
"/run",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Obvious non-data OS/runtime binds. Not a blanket /etc/* exclusion.
|
|
34
|
+
_INFRA_EXACT = frozenset(
|
|
35
|
+
{
|
|
36
|
+
"/etc/localtime",
|
|
37
|
+
"/etc/timezone",
|
|
38
|
+
"/var/run/docker.sock",
|
|
39
|
+
"/run/docker.sock",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
_INFRA_RO_SOURCE_PREFIXES = (
|
|
43
|
+
"/etc/ssl/certs",
|
|
44
|
+
"/etc/pki",
|
|
45
|
+
"/usr/share/ca-certificates",
|
|
46
|
+
"/usr/share/zoneinfo",
|
|
47
|
+
)
|
|
48
|
+
_INFRA_RO_TARGET_PREFIXES = (
|
|
49
|
+
"/etc/ssl/certs",
|
|
50
|
+
"/etc/pki",
|
|
51
|
+
"/usr/share/ca-certificates",
|
|
52
|
+
"/usr/share/zoneinfo",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _norm(path: str) -> str:
|
|
57
|
+
text = (path or "").strip()
|
|
58
|
+
if not text:
|
|
59
|
+
return ""
|
|
60
|
+
return text.rstrip("/") or "/"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_infrastructure_bind(mount: Mount) -> bool:
|
|
64
|
+
"""True only for explicit OS/runtime binds, never named volumes."""
|
|
65
|
+
if mount.type is not MountType.BIND:
|
|
66
|
+
return False
|
|
67
|
+
source = _norm(mount.source).lower()
|
|
68
|
+
target = _norm(mount.target).lower()
|
|
69
|
+
if source in _INFRA_EXACT or target in _INFRA_EXACT:
|
|
70
|
+
return True
|
|
71
|
+
if not mount.read_only:
|
|
72
|
+
return False
|
|
73
|
+
source_hit = any(
|
|
74
|
+
source == prefix or source.startswith(prefix + "/")
|
|
75
|
+
for prefix in _INFRA_RO_SOURCE_PREFIXES
|
|
76
|
+
)
|
|
77
|
+
target_hit = any(
|
|
78
|
+
target == prefix or target.startswith(prefix + "/")
|
|
79
|
+
for prefix in _INFRA_RO_TARGET_PREFIXES
|
|
80
|
+
)
|
|
81
|
+
return source_hit and target_hit
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def classify_mount(mount: Mount) -> StorageClass:
|
|
85
|
+
"""Classify a single mount using explicit, understandable rules."""
|
|
86
|
+
if mount.type is MountType.TMPFS:
|
|
87
|
+
return StorageClass.TEMPORARY
|
|
88
|
+
|
|
89
|
+
if _is_infrastructure_bind(mount):
|
|
90
|
+
return StorageClass.INFRASTRUCTURE
|
|
91
|
+
|
|
92
|
+
target = mount.target.rstrip("/") or "/"
|
|
93
|
+
target_lower = target.lower()
|
|
94
|
+
|
|
95
|
+
for hint in _CACHE_TARGET_HINTS:
|
|
96
|
+
if target_lower == hint or target_lower.endswith(hint):
|
|
97
|
+
return StorageClass.CACHE
|
|
98
|
+
|
|
99
|
+
for hint in _TEMPORARY_TARGET_HINTS:
|
|
100
|
+
if target_lower == hint or target_lower.startswith(hint + "/"):
|
|
101
|
+
# /tmp/cache already handled above as cache.
|
|
102
|
+
return StorageClass.TEMPORARY
|
|
103
|
+
|
|
104
|
+
if mount.type in {MountType.BIND, MountType.VOLUME}:
|
|
105
|
+
# Config/data-style targets and generic bind/volume mounts are treated
|
|
106
|
+
# as persistent unless matched by a temporary/cache/infrastructure rule.
|
|
107
|
+
return StorageClass.PERSISTENT
|
|
108
|
+
|
|
109
|
+
return StorageClass.UNKNOWN
|