Drop the unused GlobalConfig.data field and remove the remaining YAML config path helpers from PobsyncPaths. Keep HostConfig.config as runtime state for preflight data, and relabel it in the admin so it no longer reads as legacy compatibility storage.
189 lines
7.1 KiB
Python
189 lines
7.1 KiB
Python
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}"
|