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) pobsync_home = models.CharField(max_length=512, default="/opt/pobsync") data = models.JSONField(default=dict, blank=True) 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) config = models.JSONField(default=dict, blank=True) class Meta: ordering = ["host"] def __str__(self) -> str: return self.host 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" 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) 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) 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) user = models.CharField(max_length=64, default="root") enabled = models.BooleanField(default=True) prune = models.BooleanField(default=False) prune_max_delete = models.PositiveIntegerField(default=10) prune_protect_bases = models.BooleanField(default=False) class Meta: ordering = ["host__host"] def __str__(self) -> str: return f"{self.host} {self.cron_expr}"