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) 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) 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) 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) 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 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}"