Files
pobsync/src/pobsync_backend/models.py

219 lines
8.5 KiB
Python
Raw Normal View History

from __future__ import annotations
from django.db import models
class TimestampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class GlobalConfig(TimestampedModel):
name = models.CharField(max_length=64, default="default", unique=True)
backup_root = models.CharField(max_length=512)
default_ssh_credential = models.ForeignKey(
"SshCredential",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="global_configs",
)
ssh_user = models.CharField(max_length=64, default="root")
ssh_port = models.PositiveIntegerField(default=22)
ssh_options = models.JSONField(default=list, blank=True)
rsync_binary = models.CharField(max_length=128, default="rsync")
rsync_args = models.JSONField(default=list, blank=True)
rsync_extra_args = models.JSONField(default=list, blank=True)
rsync_timeout_seconds = models.PositiveIntegerField(default=0)
rsync_bwlimit_kbps = models.PositiveIntegerField(default=0)
default_source_root = models.CharField(max_length=512, default="/")
default_destination_subdir = models.CharField(max_length=512, default="", blank=True)
excludes_default = models.JSONField(default=list, blank=True)
retention_daily = models.PositiveIntegerField(default=14)
retention_weekly = models.PositiveIntegerField(default=8)
retention_monthly = models.PositiveIntegerField(default=12)
retention_yearly = models.PositiveIntegerField(default=0)
class Meta:
verbose_name = "global config"
verbose_name_plural = "global configs"
def __str__(self) -> str:
return self.name
class HostConfig(TimestampedModel):
host = models.CharField(max_length=255, unique=True)
address = models.CharField(max_length=255)
enabled = models.BooleanField(default=True)
ssh_credential = models.ForeignKey(
"SshCredential",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="hosts",
)
ssh_user = models.CharField(max_length=64, blank=True)
ssh_port = models.PositiveIntegerField(null=True, blank=True)
source_root = models.CharField(max_length=512, blank=True)
includes = models.JSONField(default=list, blank=True)
excludes_add = models.JSONField(default=list, blank=True)
excludes_replace = models.JSONField(null=True, blank=True)
rsync_extra_args = models.JSONField(default=list, blank=True)
2026-05-23 00:59:55 +02:00
rsync_bwlimit_kbps = models.PositiveIntegerField(null=True, blank=True)
retention_daily = models.PositiveIntegerField(default=14)
retention_weekly = models.PositiveIntegerField(default=8)
retention_monthly = models.PositiveIntegerField(default=12)
retention_yearly = models.PositiveIntegerField(default=0)
config = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ["host"]
def __str__(self) -> str:
return self.host
class SshCredential(TimestampedModel):
name = models.CharField(max_length=128, unique=True)
private_key = models.TextField(blank=True, default="")
public_key = models.TextField(blank=True)
key_path = models.CharField(max_length=1024, blank=True)
key_type = models.CharField(max_length=32, default="ed25519")
fingerprint = models.CharField(max_length=255, blank=True)
generated = models.BooleanField(default=False)
known_hosts = models.TextField(blank=True)
notes = models.TextField(blank=True)
class Meta:
ordering = ["name"]
def __str__(self) -> str:
return self.name
class BackupRun(models.Model):
class RunType(models.TextChoices):
SCHEDULED = "scheduled", "Scheduled"
MANUAL = "manual", "Manual"
class Status(models.TextChoices):
QUEUED = "queued", "Queued"
RUNNING = "running", "Running"
SUCCESS = "success", "Success"
WARNING = "warning", "Warning"
FAILED = "failed", "Failed"
CANCELLED = "cancelled", "Cancelled"
host = models.ForeignKey(HostConfig, on_delete=models.PROTECT, related_name="runs")
run_type = models.CharField(max_length=16, choices=RunType.choices, default=RunType.SCHEDULED)
status = models.CharField(max_length=16, choices=Status.choices, default=Status.QUEUED)
started_at = models.DateTimeField(null=True, blank=True)
ended_at = models.DateTimeField(null=True, blank=True)
snapshot_path = models.CharField(max_length=1024, blank=True)
snapshot = models.ForeignKey(
"SnapshotRecord",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="backup_runs",
)
base_path = models.CharField(max_length=1024, blank=True)
rsync_exit_code = models.IntegerField(null=True, blank=True)
result = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
reviewed_at = models.DateTimeField(null=True, blank=True)
reviewed_by = models.CharField(max_length=150, blank=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return f"{self.host} {self.run_type} {self.status}"
class SnapshotRecord(models.Model):
class Kind(models.TextChoices):
SCHEDULED = "scheduled", "Scheduled"
MANUAL = "manual", "Manual"
INCOMPLETE = "incomplete", "Incomplete"
host = models.ForeignKey(HostConfig, on_delete=models.CASCADE, related_name="snapshots")
kind = models.CharField(max_length=16, choices=Kind.choices)
dirname = models.CharField(max_length=255)
path = models.CharField(max_length=1024)
2026-05-19 11:19:22 +02:00
base = models.ForeignKey(
"self",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="derived_snapshots",
)
base_kind = models.CharField(max_length=16, blank=True)
base_dirname = models.CharField(max_length=255, blank=True)
base_path = models.CharField(max_length=1024, blank=True)
base_snapshot_id = models.CharField(max_length=64, blank=True)
status = models.CharField(max_length=32, blank=True)
started_at = models.DateTimeField(null=True, blank=True)
ended_at = models.DateTimeField(null=True, blank=True)
metadata = models.JSONField(default=dict, blank=True)
discovered_at = models.DateTimeField(auto_now_add=True)
reviewed_at = models.DateTimeField(null=True, blank=True)
reviewed_by = models.CharField(max_length=150, blank=True)
class Meta:
constraints = [
models.UniqueConstraint(fields=["host", "kind", "dirname"], name="unique_snapshot_per_host_kind"),
]
ordering = ["host__host", "-started_at", "dirname"]
def __str__(self) -> str:
return f"{self.host}/{self.kind}/{self.dirname}"
class PurgedSnapshot(models.Model):
class Action(models.TextChoices):
MANUAL = "manual", "Manual"
SCHEDULED = "scheduled", "Scheduled"
CLI = "cli", "CLI"
INCOMPLETE_CLEANUP = "incomplete_cleanup", "Incomplete cleanup"
host = models.ForeignKey(HostConfig, on_delete=models.SET_NULL, null=True, blank=True, related_name="purged_snapshots")
host_name = models.CharField(max_length=255)
kind = models.CharField(max_length=16)
dirname = models.CharField(max_length=255)
path = models.CharField(max_length=1024)
reason = models.CharField(max_length=512, blank=True)
action = models.CharField(max_length=32, choices=Action.choices)
triggered_by = models.CharField(max_length=150, blank=True)
metadata = models.JSONField(default=dict, blank=True)
purged_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-purged_at", "host_name", "dirname"]
def __str__(self) -> str:
return f"{self.host_name}/{self.kind}/{self.dirname}"
class ScheduleConfig(TimestampedModel):
host = models.OneToOneField(HostConfig, on_delete=models.CASCADE, related_name="schedule")
cron_expr = models.CharField(max_length=128)
enabled = models.BooleanField(default=True)
prune = models.BooleanField(default=False)
prune_max_delete = models.PositiveIntegerField(default=10)
prune_protect_bases = models.BooleanField(default=False)
last_due_key = models.CharField(max_length=32, blank=True)
last_started_at = models.DateTimeField(null=True, blank=True)
last_finished_at = models.DateTimeField(null=True, blank=True)
last_status = models.CharField(max_length=16, blank=True)
class Meta:
ordering = ["host__host"]
def __str__(self) -> str:
return f"{self.host} {self.cron_expr}"