selfprivacy-rest-api/selfprivacy_api/backup/tasks.py

238 lines
6.6 KiB
Python

"""
The tasks module contains the worker tasks that are used to back up and restore
"""
from datetime import datetime, timezone
from typing import List
from selfprivacy_api.graphql.common_types.backup import (
RestoreStrategy,
BackupReason,
)
from selfprivacy_api.models.backup.snapshot import Snapshot
from selfprivacy_api.utils.huey import huey
from huey import crontab
from selfprivacy_api.services import ServiceManager, Service
from selfprivacy_api.backup import Backups
from selfprivacy_api.backup.jobs import add_autobackup_job, add_total_restore_job
from selfprivacy_api.jobs import Jobs, JobStatus, Job
from selfprivacy_api.jobs.upgrade_system import rebuild_system
from selfprivacy_api.actions.system import add_rebuild_job
SNAPSHOT_CACHE_TTL_HOURS = 6
def validate_datetime(dt: datetime) -> bool:
"""
Validates that it is time to back up.
Also ensures that the timezone-aware time is used.
"""
if dt.tzinfo is None:
return Backups.is_time_to_backup(dt.replace(tzinfo=timezone.utc))
return Backups.is_time_to_backup(dt)
def report_job_error(error: Exception, job: Job):
Jobs.update(
job,
status=JobStatus.ERROR,
error=type(error).__name__ + ": " + str(error),
)
# huey tasks need to return something
@huey.task()
def start_backup(service_id: str, reason: BackupReason = BackupReason.EXPLICIT) -> bool:
"""
The worker task that starts the backup process.
"""
service = ServiceManager.get_service_by_id(service_id)
if service is None:
raise ValueError(f"No such service: {service_id}")
Backups.back_up(service, reason)
return True
@huey.task()
def prune_autobackup_snapshots(job: Job) -> bool:
"""
Remove all autobackup snapshots that do not fit into quotas set
"""
Jobs.update(job, JobStatus.RUNNING)
try:
Backups.prune_all_autosnaps()
except Exception as e:
Jobs.update(job, JobStatus.ERROR, error=type(e).__name__ + ":" + str(e))
return False
Jobs.update(job, JobStatus.FINISHED)
return True
@huey.task()
def restore_snapshot(
snapshot: Snapshot,
strategy: RestoreStrategy = RestoreStrategy.DOWNLOAD_VERIFY_OVERWRITE,
) -> bool:
"""
The worker task that starts the restore process.
"""
Backups.restore_snapshot(snapshot, strategy)
return True
@huey.task()
def full_restore(job: Job) -> bool:
do_full_restore(job)
return True
@huey.periodic_task(validate_datetime=validate_datetime)
def automatic_backup() -> None:
"""
The worker periodic task that starts the automatic backup process.
"""
do_autobackup()
@huey.task()
def total_backup(job: Job) -> bool:
do_total_backup(job)
return True
@huey.periodic_task(crontab(hour="*/" + str(SNAPSHOT_CACHE_TTL_HOURS)))
def reload_snapshot_cache():
Backups.force_snapshot_cache_reload()
def back_up_multiple(
job: Job,
services_to_back_up: List[Service],
reason: BackupReason = BackupReason.EXPLICIT,
):
if services_to_back_up == []:
return
progress_per_service = 100 // len(services_to_back_up)
progress = 0
Jobs.update(job, JobStatus.RUNNING, progress=progress)
for service in services_to_back_up:
try:
Backups.back_up(service, reason)
except Exception as error:
report_job_error(error, job)
raise error
progress = progress + progress_per_service
Jobs.update(job, JobStatus.RUNNING, progress=progress)
def do_total_backup(job: Job) -> None:
"""
Body of total backup task, broken out to test it
"""
back_up_multiple(job, ServiceManager.get_enabled_services())
Jobs.update(job, JobStatus.FINISHED)
def do_autobackup() -> None:
"""
Body of autobackup task, broken out to test it
For some reason, we cannot launch periodic huey tasks
inside tests
"""
time = datetime.now(timezone.utc)
backups_were_disabled = Backups.autobackup_period_minutes() is None
if backups_were_disabled:
# Temporarily enable autobackup
Backups.set_autobackup_period_minutes(24 * 60) # 1 day
services_to_back_up = Backups.services_to_back_up(time)
if not services_to_back_up:
return
job = add_autobackup_job(services_to_back_up)
back_up_multiple(job, services_to_back_up, BackupReason.AUTO)
if backups_were_disabled:
Backups.set_autobackup_period_minutes(0)
Jobs.update(job, JobStatus.FINISHED)
# there is no point of returning the job
# this code is called with a delay
def eligible_for_full_restoration(snap: Snapshot):
service = ServiceManager.get_service_by_id(snap.service_name)
if service is None:
return False
if service.is_enabled() is False:
return False
return True
def which_snapshots_to_full_restore() -> list[Snapshot]:
autoslice = Backups.last_backup_slice()
api_snapshot = None
for snap in autoslice:
if snap.service_name == "api":
api_snapshot = snap
autoslice.remove(snap)
if api_snapshot is None:
raise ValueError(
"Cannot restore, no configuration snapshot found. This particular error should be unreachable"
)
snapshots_to_restore = [
snap for snap in autoslice if eligible_for_full_restoration(snap)
]
# API should be restored in the very end of the list because it requires rebuild right afterwards
snapshots_to_restore.append(api_snapshot)
return snapshots_to_restore
def do_full_restore(job: Job) -> None:
"""
Body full restore task, a part of server migration.
Broken out to test it independently from task infra
"""
Jobs.update(
job,
JobStatus.RUNNING,
status_text=f"Finding the last autobackup session",
progress=0,
)
snapshots_to_restore = which_snapshots_to_full_restore()
progress_per_service = 99 // len(snapshots_to_restore)
progress = 0
Jobs.update(job, JobStatus.RUNNING, progress=progress)
for snap in snapshots_to_restore:
try:
Backups.restore_snapshot(snap)
except Exception as error:
report_job_error(error, job)
progress = progress + progress_per_service
Jobs.update(
job,
JobStatus.RUNNING,
status_text=f"restoring {snap.service_name}",
progress=progress,
)
Jobs.update(job, JobStatus.RUNNING, status_text="rebuilding system", progress=99)
# Adding a separate job to not confuse the user with jumping progress bar
rebuild_job = add_rebuild_job()
rebuild_system(rebuild_job)
Jobs.update(job, JobStatus.FINISHED)