Release / verify (push) Canceled after 0s
CI / checks (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 0s
Release / test (push) Canceled after 0s
Release / publish (backend) (push) Canceled after 0s
Release / publish (frontend) (push) Canceled after 0s
Release / publish (proxy) (push) Canceled after 0s
Release / publish (updater) (push) Canceled after 0s
Geplante Wartung: Fehler zeigen sich sofort statt erst um 3 Uhr nachts, und der Master lässt sich unbeaufsichtigt aktualisieren. Auf einem Test-Cluster live geprüft (Neustart und ein echtes Update mit Umschaltung), siehe Handbuch „Geplante Wartung“. - **Rollenprüfung beim Einplanen:** Ein Host, der in einem Cluster gerade CARP-MASTER ist, lässt sich nicht mehr einplanen (Neustart/Update/Major-Upgrade). Bisher wurde der Plan angenommen und scheiterte erst zur Uhrzeit. Der Planungsdialog zeigt den Grund an und sperrt den Button. Ist die Rolle nicht feststellbar (Host gerade nicht erreichbar), wird weiter angenommen; die volle Prüfung läuft wie bisher zum Zündzeitpunkt. - **Benachrichtigung „Geplante Wartung nicht ausgeführt“:** Eine fehlgeschlagene geplante Wartung meldet sich per Webhook/E-Mail (einzeln schaltbar in den Einstellungen, standardmäßig an). Offen für drei Tage nach dem geplanten Zeitpunkt, dann ohne „behoben“-Meldung geschlossen. Neue Spalte `notification_settings.scheduled_maintenance_failed_enabled` (Migration `0006`). - **Automatische CARP-Umschaltung (Option pro Plan, standardmäßig aus):** Ein geplanter Neustart, Update oder Major-Upgrade auf dem MASTER schaltet vorher auf den Partner um, prüft die Übernahme, führt die Aktion aus, wartet auf die Rückkehr des Hosts und schaltet zurück. Fehler vor dem Start der Aktion stellen den Normalbetrieb wieder her; danach bleibt der Host bewusst im Wartungsmodus (Partner trägt den Verkehr) und es kommt eine Benachrichtigung. Der Ablauf steht in der Datenbank und überlebt einen Neustart des Backends. Neue Spalten an `scheduled_maintenance` (Migration `0007`). - **Backup vor geplantem Update:** Ein geplantes Update oder Major-Upgrade zieht unmittelbar vor dem Start selbst ein Konfigurationsbackup (nur lesend auf der Firewall). Bisher scheiterte jeder Plan, der mehr als 24 Stunden in der Zukunft lag, an der Backup-Aktualität. Schlägt das Backup fehl, wird nicht aktualisiert.
50 lines
2.7 KiB
Python
50 lines
2.7 KiB
Python
"""One-shot scheduled host-maintenance actions (reboot/update/major_update
|
|
at a future timestamp instead of immediately). A brand-new table, same
|
|
"no manual ALTER needed" situation as host_resource_samples.
|
|
|
|
Deliberately v1-minimal: one fixed `run_at`, no recurrence/cron
|
|
expression — see resource_monitor_service.py's sibling note in
|
|
config.py for the same "keep v1 small" rationale. Firing reuses the
|
|
existing HostOperation/ensure_no_maintenance lock machinery
|
|
(routers/scheduled_maintenance.py, main.py's
|
|
_scheduled_maintenance_poll_loop) rather than duplicating it — this
|
|
table only tracks the *schedule* itself, not the resulting operation.
|
|
"""
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, JSON, String
|
|
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.database import Base
|
|
|
|
|
|
class ScheduledMaintenance(Base):
|
|
__tablename__ = "scheduled_maintenance"
|
|
__table_args__ = (
|
|
Index("ix_scheduled_maintenance_status_run_at", "status", "run_at"),
|
|
)
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
host_id: Mapped[uuid.UUID] = mapped_column(
|
|
PGUUID(as_uuid=True), ForeignKey("opnsense_hosts.id", ondelete="CASCADE"), index=True
|
|
)
|
|
host_name: Mapped[str] = mapped_column(String(120))
|
|
action: Mapped[str] = mapped_column(String(32)) # reboot | update | major_update
|
|
run_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
status: Mapped[str] = mapped_column(String(16), default="pending") # pending|running|succeeded|failed|cancelled
|
|
actor: Mapped[str] = mapped_column(String(120))
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
result: Mapped[dict] = mapped_column(JSON, default=dict)
|
|
# Set to the resulting HostOperation's id once fired, so the UI can
|
|
# link from the schedule to the operation it produced.
|
|
host_operation_id: Mapped[uuid.UUID | None] = mapped_column(PGUUID(as_uuid=True), nullable=True)
|
|
# Opt-in: hand the traffic over to the CARP partner first, run the action, then switch back.
|
|
carp_handover: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="false")
|
|
# Only while status == "running" with a handover: handover_wait | job_wait | leave | leave_wait.
|
|
# State lives here, not in memory, so a backend restart in the middle of a run resumes cleanly.
|
|
phase: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
|
phase_since: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
progress: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
|