mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-30 00:27:11 +09:00
Every NOT NULL FK from the KNOWN_DANGLING_ON_DELETE audit now declares its parent-side relationship, so the delete-cascade policy door covers it: AI conversations/pending actions, API keys, dashboards, event subscriptions, login links, sent invitations, resource grants (users); env vars + history, previews + settings, WAF policy (applications); surveys, doctor results, proxy stack, tunnels (servers); manifests (projects); DDNS hosts (zones); exposed services (tunnels); drills (backup policies); rollouts (agent versions); site plugin installs (WordPress sites); queue messages (groups). The ratchet set is now EMPTY and stays that way. One new deliberate exception: Workspace.projects — projects carry environments and app groupings, so delete_workspace now refuses (409) while any remain instead of silently destroying them (previously it crashed with IntegrityError). Migration 095 sweeps rows already orphaned on existing installs by pre-fix hard-deletes — including an application's leftover env-var secrets, which a reused SQLite row id could have attached to a future app. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
76 lines
3.3 KiB
Python
76 lines
3.3 KiB
Python
from datetime import datetime
|
|
|
|
from app import db
|
|
from app.models.json_column_mixin import JsonColumnMixin
|
|
|
|
|
|
class RestoreDrill(JsonColumnMixin, db.Model):
|
|
"""A single restore *drill* — a real restore of a policy's latest restorable
|
|
point into a scratch location (temp dir / scratch DB), probe-verified, then
|
|
torn down. Never touches the live target.
|
|
|
|
A successful drill is the strongest proof a backup is restorable: it promotes
|
|
the drilled :class:`BackupRun` to ``verify_level='drilled'`` and stamps the
|
|
policy's ``last_drill_*`` cache. Drilling the latest point implicitly proves
|
|
the whole chain (incrementals are useless without their base).
|
|
|
|
See docs/plans/23_BACKUP_TRUST_RESTORE_DRILLS_PLAN.md (Decision 4).
|
|
"""
|
|
|
|
__tablename__ = 'restore_drills'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
policy_id = db.Column(db.Integer, db.ForeignKey('backup_policies.id'), nullable=False, index=True)
|
|
# Parent-side backref so the delete-cascade policy covers the NOT NULL FK:
|
|
# drill history dies with its policy.
|
|
policy = db.relationship('BackupPolicy',
|
|
backref=db.backref('restore_drills', lazy='dynamic'))
|
|
# The run whose restore-point (chain endpoint) was drilled.
|
|
run_id = db.Column(db.Integer, db.ForeignKey('backup_runs.id'), nullable=True, index=True)
|
|
job_id = db.Column(db.String(36), nullable=True, index=True)
|
|
|
|
# 'running' | 'success' | 'failed' | 'skipped_no_space'
|
|
status = db.Column(db.String(24), nullable=False, default='running')
|
|
trigger = db.Column(db.String(20), nullable=True) # 'manual' | 'scheduled'
|
|
|
|
started_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
finished_at = db.Column(db.DateTime, nullable=True)
|
|
duration_seconds = db.Column(db.Integer, nullable=True)
|
|
|
|
# Space accounting (operator's own disk — raw bytes, never a fee).
|
|
bytes_required = db.Column(db.BigInteger, nullable=True) # estimated extracted size
|
|
bytes_free = db.Column(db.BigInteger, nullable=True) # free space at precheck
|
|
bytes_restored = db.Column(db.BigInteger, nullable=True) # actually written to scratch
|
|
|
|
scratch_ref = db.Column(db.Text, nullable=True) # scratch dir path / scratch DB name
|
|
probes_json = db.Column(db.Text, default='{}') # probe results + error tail
|
|
error = db.Column(db.Text, nullable=True)
|
|
|
|
def get_probes(self):
|
|
return self._json_read('probes_json')
|
|
|
|
def set_probes(self, value):
|
|
self._json_write('probes_json', value, falsy='{}')
|
|
|
|
def to_dict(self):
|
|
return {
|
|
'id': self.id,
|
|
'policy_id': self.policy_id,
|
|
'run_id': self.run_id,
|
|
'job_id': self.job_id,
|
|
'status': self.status,
|
|
'trigger': self.trigger,
|
|
'started_at': self.started_at.isoformat() if self.started_at else None,
|
|
'finished_at': self.finished_at.isoformat() if self.finished_at else None,
|
|
'duration_seconds': self.duration_seconds,
|
|
'bytes_required': self.bytes_required,
|
|
'bytes_free': self.bytes_free,
|
|
'bytes_restored': self.bytes_restored,
|
|
'scratch_ref': self.scratch_ref,
|
|
'probes': self.get_probes(),
|
|
'error': self.error,
|
|
}
|
|
|
|
def __repr__(self):
|
|
return f'<RestoreDrill {self.id} policy={self.policy_id} {self.status}>'
|