Compare commits
2 Commits
86eee0f916
...
39b6cf3469
| Author | SHA1 | Date | |
|---|---|---|---|
| 39b6cf3469 | |||
| 124421b85c |
@@ -55,6 +55,7 @@ Common overrides:
|
||||
```
|
||||
sudo scripts/install-systemd \
|
||||
--backup-root /mnt/backups/pobsync \
|
||||
--time-zone Europe/Amsterdam \
|
||||
--allowed-hosts backup.example.com,localhost,127.0.0.1 \
|
||||
--csrf-trusted-origins https://backup.example.com
|
||||
```
|
||||
@@ -64,6 +65,9 @@ installer to rewrite an existing `/etc/pobsync/pobsync.env`.
|
||||
Use `--non-interactive` for scripted installs. Use `--verbose` when you want to see the underlying apt, pip, Django, and
|
||||
systemd output.
|
||||
|
||||
Schedules are evaluated in `POBSYNC_TIME_ZONE`. The installer defaults this to the server timezone when it can detect
|
||||
one, otherwise `UTC`; override it with `--time-zone Europe/Amsterdam` or by editing `/etc/pobsync/pobsync.env`.
|
||||
|
||||
For MariaDB support, add:
|
||||
|
||||
```
|
||||
|
||||
@@ -17,6 +17,7 @@ if [ -n "${POBSYNC_BACKUP_ROOT:-}" ]; then
|
||||
BACKUP_ROOT_EXPLICIT=1
|
||||
fi
|
||||
WEB_BIND=${POBSYNC_WEB_BIND:-127.0.0.1:8010}
|
||||
TIME_ZONE=${POBSYNC_TIME_ZONE:-}
|
||||
FORCE_ENV=0
|
||||
INSTALL_OS_PACKAGES=1
|
||||
WITH_NGINX=0
|
||||
@@ -74,6 +75,10 @@ while [ "$#" -gt 0 ]; do
|
||||
WEB_BIND=$2
|
||||
shift 2
|
||||
;;
|
||||
--time-zone)
|
||||
TIME_ZONE=$2
|
||||
shift 2
|
||||
;;
|
||||
--force-env)
|
||||
FORCE_ENV=1
|
||||
shift
|
||||
@@ -148,6 +153,38 @@ if [ -f "$ENV_FILE" ] && [ "$FORCE_ENV" -ne 1 ] && [ "$BACKUP_ROOT_EXPLICIT" -ne
|
||||
fi
|
||||
fi
|
||||
|
||||
detect_time_zone() {
|
||||
if [ -n "$TIME_ZONE" ]; then
|
||||
printf '%s\n' "$TIME_ZONE"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -n "${POBSYNC_TIME_ZONE:-}" ]; then
|
||||
printf '%s\n' "$POBSYNC_TIME_ZONE"
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v timedatectl >/dev/null 2>&1; then
|
||||
detected=$(timedatectl show -p Timezone --value 2>/dev/null || true)
|
||||
if [ -n "$detected" ]; then
|
||||
printf '%s\n' "$detected"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f /etc/timezone ]; then
|
||||
detected=$(sed -n '1p' /etc/timezone | tr -d '[:space:]')
|
||||
if [ -n "$detected" ]; then
|
||||
printf '%s\n' "$detected"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
printf 'UTC\n'
|
||||
}
|
||||
|
||||
TIME_ZONE=$(detect_time_zone)
|
||||
|
||||
run_step() {
|
||||
label=$1
|
||||
shift
|
||||
@@ -261,6 +298,7 @@ if [ "$INTERACTIVE" -eq 1 ]; then
|
||||
SERVICE_GROUP=$(prompt_value "Service group" "$SERVICE_GROUP")
|
||||
BACKUP_ROOT=$(prompt_value "Backup storage path" "$BACKUP_ROOT")
|
||||
WEB_BIND=$(prompt_value "Gunicorn bind address" "$WEB_BIND")
|
||||
TIME_ZONE=$(prompt_value "Scheduler time zone" "$TIME_ZONE")
|
||||
ALLOWED_HOSTS=$(prompt_value "Allowed hosts" "$ALLOWED_HOSTS")
|
||||
CSRF_TRUSTED_ORIGINS=$(prompt_value "CSRF trusted origins, comma-separated or blank" "$CSRF_TRUSTED_ORIGINS")
|
||||
INSTALL_OS_PACKAGES=$(prompt_yes_no "Install required OS packages with apt-get" "$INSTALL_OS_PACKAGES")
|
||||
@@ -331,6 +369,12 @@ if ! command -v python3 >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! env POBSYNC_INSTALL_TIME_ZONE="$TIME_ZONE" python3 -c "import os; from zoneinfo import ZoneInfo; ZoneInfo(os.environ['POBSYNC_INSTALL_TIME_ZONE'])" >/dev/null 2>&1; then
|
||||
echo "Invalid time zone: $TIME_ZONE" >&2
|
||||
echo "Use an IANA timezone such as UTC or Europe/Amsterdam." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v rsync >/dev/null 2>&1; then
|
||||
echo "rsync is required." >&2
|
||||
exit 1
|
||||
@@ -416,6 +460,7 @@ POBSYNC_DJANGO_CSRF_TRUSTED_ORIGINS=$CSRF_TRUSTED_ORIGINS
|
||||
|
||||
POBSYNC_HOME=/var/lib/pobsync
|
||||
POBSYNC_BACKUP_ROOT=$BACKUP_ROOT
|
||||
POBSYNC_TIME_ZONE=$TIME_ZONE
|
||||
POBSYNC_SQLITE_PATH=/var/lib/pobsync/pobsync.sqlite3
|
||||
POBSYNC_STATIC_ROOT=/var/lib/pobsync/static
|
||||
|
||||
|
||||
@@ -54,23 +54,23 @@ def collect_dashboard_stats(*, hosts: Iterable[HostConfig], global_config: Globa
|
||||
def collect_host_stats(*, host: HostConfig, limit: int = 8) -> dict[str, Any]:
|
||||
runs = list(host.runs.select_related("snapshot").filter(status=BackupRun.Status.SUCCESS).order_by("-started_at", "-created_at")[:50])
|
||||
real_runs = [_run_summary(run) for run in runs if _is_real_run(run)]
|
||||
real_runs = [run for run in real_runs if run["has_stats"]][:limit]
|
||||
trend_runs = [run for run in real_runs if run["has_stats"]][:limit]
|
||||
latest_snapshot = host.snapshots.order_by("-started_at", "-discovered_at", "-id").first()
|
||||
latest_snapshot_stats = _snapshot_summary(latest_snapshot) if latest_snapshot else {}
|
||||
|
||||
literal_values = [_int_at(run, "rsync", "literal_data_bytes") for run in real_runs]
|
||||
literal_values = [_int_at(run, "rsync", "literal_data_bytes") for run in trend_runs]
|
||||
literal_values = [value for value in literal_values if value is not None]
|
||||
matched_values = [_int_at(run, "rsync", "matched_data_bytes") for run in real_runs]
|
||||
matched_values = [_int_at(run, "rsync", "matched_data_bytes") for run in trend_runs]
|
||||
matched_values = [value for value in matched_values if value is not None]
|
||||
max_literal = max(literal_values) if literal_values else 0
|
||||
max_matched = max(matched_values) if matched_values else 0
|
||||
|
||||
return {
|
||||
"runs": [_with_bar_percentages(run, max_literal=max_literal, max_matched=max_matched) for run in real_runs],
|
||||
"runs": [_with_bar_percentages(run, max_literal=max_literal, max_matched=max_matched) for run in trend_runs],
|
||||
"latest_run": real_runs[0] if real_runs else {},
|
||||
"latest_snapshot": latest_snapshot_stats,
|
||||
"avg_literal_data_bytes": _average(literal_values),
|
||||
"avg_daily_literal_data_bytes": _average_daily_literal(real_runs),
|
||||
"avg_daily_literal_data_bytes": _average_daily_literal(trend_runs),
|
||||
"total_literal_data_bytes": sum(literal_values),
|
||||
"total_matched_data_bytes": sum(matched_values),
|
||||
}
|
||||
|
||||
@@ -90,7 +90,14 @@
|
||||
<span class="muted">none</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{% if host.next_run_at %}{{ host.next_run_at }}{% else %}<span class="muted">none</span>{% endif %}</td>
|
||||
<td>
|
||||
{% if host.next_run_at %}
|
||||
{{ host.next_run_at|date:"Y-m-d H:i T" }}
|
||||
<div class="muted">{{ scheduler_timezone }}</div>
|
||||
{% else %}
|
||||
<span class="muted">none</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ host.stats_summary.latest_run.rsync.literal_data_bytes|filesizeformat }}</td>
|
||||
<td>{{ host.run_count }}</td>
|
||||
<td>d{{ host.retention_daily }} w{{ host.retention_weekly }} m{{ host.retention_monthly }} y{{ host.retention_yearly }}</td>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<div><strong>Schedule expression:</strong> {{ schedule.cron_expr }}</div>
|
||||
<div class="muted">Evaluated by the pobsync scheduler service.</div>
|
||||
<div><strong>Enabled:</strong> {{ schedule.enabled|yesno:"yes,no" }}</div>
|
||||
<div><strong>Next run:</strong> {{ next_run_at|default:"" }}</div>
|
||||
<div><strong>Next run:</strong> {% if next_run_at %}{{ next_run_at|date:"Y-m-d H:i T" }} <span class="muted">{{ scheduler_timezone }}</span>{% endif %}</div>
|
||||
<div><strong>Prune:</strong> {{ schedule.prune|yesno:"yes,no" }}</div>
|
||||
<div><strong>Last status:</strong> {{ schedule.last_status|default:"" }}</div>
|
||||
<div><strong>Last started:</strong> {{ schedule.last_started_at|default:"" }}</div>
|
||||
|
||||
@@ -50,6 +50,8 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "web-01")
|
||||
self.assertContains(response, "20260519-021500Z__ABCDEFGH")
|
||||
self.assertContains(response, "success")
|
||||
self.assertContains(response, f"Run {run.id}")
|
||||
self.assertContains(response, "manual")
|
||||
|
||||
def test_dashboard_renders_backup_trend_summary(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
@@ -91,6 +93,7 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "Avg Daily New")
|
||||
self.assertContains(response, "Days Until Full")
|
||||
self.assertContains(response, "Next Run")
|
||||
self.assertContains(response, "UTC")
|
||||
self.assertContains(response, "10")
|
||||
self.assertContains(response, f"Run {run.id}")
|
||||
self.assertContains(response, "manual")
|
||||
@@ -554,6 +557,7 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "Schedule expression")
|
||||
self.assertContains(response, "Evaluated by the pobsync scheduler service.")
|
||||
self.assertContains(response, "Next run:")
|
||||
self.assertContains(response, "UTC")
|
||||
self.assertContains(response, "20260519-021500Z__ABCDEFGH")
|
||||
self.assertContains(response, "Discover snapshots")
|
||||
self.assertContains(response, "Edit schedule")
|
||||
|
||||
@@ -58,6 +58,7 @@ def dashboard(request):
|
||||
"hosts": hosts,
|
||||
"global_config": global_config,
|
||||
"stats_summary": stats_summary,
|
||||
"scheduler_timezone": timezone.get_current_timezone_name(),
|
||||
"latest_runs": BackupRun.objects.select_related("host", "snapshot").order_by("-created_at")[:10],
|
||||
"counts": {
|
||||
"global_configs": GlobalConfig.objects.count(),
|
||||
@@ -271,6 +272,7 @@ def host_detail(request, host: str):
|
||||
"host": host_config,
|
||||
"schedule": schedule,
|
||||
"next_run_at": _next_run_for_schedule(schedule, host_config),
|
||||
"scheduler_timezone": timezone.get_current_timezone_name(),
|
||||
"discovery": inspect_snapshot_discovery(host=host_config),
|
||||
"host_checks": host_checks,
|
||||
"host_check_summary": summarize_self_checks(host_checks),
|
||||
|
||||
Reference in New Issue
Block a user