2023-07-20 15:24:26 +00:00
|
|
|
"""
|
|
|
|
The tasks module contains the worker tasks that are used to back up and restore
|
|
|
|
"""
|
2024-07-26 19:59:44 +00:00
|
|
|
|
2023-07-20 16:39:10 +00:00
|
|
|
from datetime import datetime, timezone
|
2024-09-06 11:40:16 +00:00
|
|
|
from typing import List
|
2023-04-10 16:35:35 +00:00
|
|
|
|
2023-11-17 15:22:21 +00:00
|
|
|
from selfprivacy_api.graphql.common_types.backup import (
|
|
|
|
RestoreStrategy,
|
|
|
|
BackupReason,
|
|
|
|
)
|
2023-07-07 12:49:52 +00:00
|
|
|
|
2023-04-19 15:09:06 +00:00
|
|
|
from selfprivacy_api.models.backup.snapshot import Snapshot
|
2023-03-29 11:45:52 +00:00
|
|
|
from selfprivacy_api.utils.huey import huey
|
2023-09-01 10:41:27 +00:00
|
|
|
from huey import crontab
|
2023-10-11 17:34:53 +00:00
|
|
|
|
2024-09-06 11:40:16 +00:00
|
|
|
from selfprivacy_api.services import ServiceManager, Service
|
2023-03-29 11:45:52 +00:00
|
|
|
from selfprivacy_api.backup import Backups
|
2024-09-11 09:59:53 +00:00
|
|
|
from selfprivacy_api.backup.jobs import add_autobackup_job
|
2023-11-17 15:22:21 +00:00
|
|
|
from selfprivacy_api.jobs import Jobs, JobStatus, Job
|
2024-07-26 10:12:02 +00:00
|
|
|
from selfprivacy_api.jobs.upgrade_system import rebuild_system
|
|
|
|
from selfprivacy_api.actions.system import add_rebuild_job
|
2023-11-17 15:22:21 +00:00
|
|
|
|
2023-03-29 11:45:52 +00:00
|
|
|
|
2023-09-01 10:41:27 +00:00
|
|
|
SNAPSHOT_CACHE_TTL_HOURS = 6
|
|
|
|
|
2023-04-10 16:35:35 +00:00
|
|
|
|
2023-07-20 15:24:26 +00:00
|
|
|
def validate_datetime(dt: datetime) -> bool:
|
|
|
|
"""
|
2023-09-01 10:41:27 +00:00
|
|
|
Validates that it is time to back up.
|
|
|
|
Also ensures that the timezone-aware time is used.
|
2023-07-20 15:24:26 +00:00
|
|
|
"""
|
2023-07-20 16:39:10 +00:00
|
|
|
if dt.tzinfo is None:
|
|
|
|
return Backups.is_time_to_backup(dt.replace(tzinfo=timezone.utc))
|
2023-04-10 16:35:35 +00:00
|
|
|
return Backups.is_time_to_backup(dt)
|
|
|
|
|
|
|
|
|
2024-09-06 11:40:16 +00:00
|
|
|
def report_job_error(error: Exception, job: Job):
|
|
|
|
Jobs.update(
|
|
|
|
job,
|
|
|
|
status=JobStatus.ERROR,
|
|
|
|
error=type(error).__name__ + ": " + str(error),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-03-29 11:45:52 +00:00
|
|
|
# huey tasks need to return something
|
|
|
|
@huey.task()
|
2023-12-22 11:31:56 +00:00
|
|
|
def start_backup(service_id: str, reason: BackupReason = BackupReason.EXPLICIT) -> bool:
|
2023-07-20 15:24:26 +00:00
|
|
|
"""
|
|
|
|
The worker task that starts the backup process.
|
|
|
|
"""
|
2024-07-24 15:15:31 +00:00
|
|
|
service = ServiceManager.get_service_by_id(service_id)
|
2023-10-11 17:34:53 +00:00
|
|
|
if service is None:
|
|
|
|
raise ValueError(f"No such service: {service_id}")
|
2023-08-21 11:11:56 +00:00
|
|
|
Backups.back_up(service, reason)
|
2023-03-29 11:45:52 +00:00
|
|
|
return True
|
2023-04-10 16:35:35 +00:00
|
|
|
|
|
|
|
|
2023-11-17 15:22:21 +00:00
|
|
|
@huey.task()
|
2023-11-17 15:53:57 +00:00
|
|
|
def prune_autobackup_snapshots(job: Job) -> bool:
|
2023-11-17 15:39:21 +00:00
|
|
|
"""
|
|
|
|
Remove all autobackup snapshots that do not fit into quotas set
|
|
|
|
"""
|
2023-11-17 15:22:21 +00:00
|
|
|
Jobs.update(job, JobStatus.RUNNING)
|
|
|
|
try:
|
2023-11-17 15:39:21 +00:00
|
|
|
Backups.prune_all_autosnaps()
|
2023-11-17 15:22:21 +00:00
|
|
|
except Exception as e:
|
|
|
|
Jobs.update(job, JobStatus.ERROR, error=type(e).__name__ + ":" + str(e))
|
|
|
|
return False
|
|
|
|
|
|
|
|
Jobs.update(job, JobStatus.FINISHED)
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2023-04-19 15:09:06 +00:00
|
|
|
@huey.task()
|
2023-07-07 12:49:52 +00:00
|
|
|
def restore_snapshot(
|
|
|
|
snapshot: Snapshot,
|
|
|
|
strategy: RestoreStrategy = RestoreStrategy.DOWNLOAD_VERIFY_OVERWRITE,
|
|
|
|
) -> bool:
|
2023-07-20 15:24:26 +00:00
|
|
|
"""
|
|
|
|
The worker task that starts the restore process.
|
|
|
|
"""
|
2023-07-07 12:49:52 +00:00
|
|
|
Backups.restore_snapshot(snapshot, strategy)
|
2023-04-19 15:09:06 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
2024-09-06 11:40:16 +00:00
|
|
|
@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)
|
|
|
|
|
|
|
|
|
2024-02-23 18:16:25 +00:00
|
|
|
def do_autobackup() -> None:
|
2023-07-20 15:24:26 +00:00
|
|
|
"""
|
2024-02-23 16:45:59 +00:00
|
|
|
Body of autobackup task, broken out to test it
|
|
|
|
For some reason, we cannot launch periodic huey tasks
|
|
|
|
inside tests
|
2023-07-20 15:24:26 +00:00
|
|
|
"""
|
2024-09-06 11:40:16 +00:00
|
|
|
|
2024-07-26 19:59:32 +00:00
|
|
|
time = datetime.now(timezone.utc)
|
2024-07-29 12:16:33 +00:00
|
|
|
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
|
|
|
|
|
2024-02-23 18:16:25 +00:00
|
|
|
services_to_back_up = Backups.services_to_back_up(time)
|
2024-03-07 20:29:37 +00:00
|
|
|
if not services_to_back_up:
|
|
|
|
return
|
2024-02-23 18:16:25 +00:00
|
|
|
job = add_autobackup_job(services_to_back_up)
|
|
|
|
|
2024-09-06 11:40:16 +00:00
|
|
|
back_up_multiple(job, services_to_back_up, BackupReason.AUTO)
|
2024-02-23 16:45:59 +00:00
|
|
|
|
2024-07-29 12:16:33 +00:00
|
|
|
if backups_were_disabled:
|
|
|
|
Backups.set_autobackup_period_minutes(0)
|
2024-02-23 18:36:11 +00:00
|
|
|
Jobs.update(job, JobStatus.FINISHED)
|
2024-07-29 12:16:33 +00:00
|
|
|
# there is no point of returning the job
|
|
|
|
# this code is called with a delay
|
2024-02-23 18:36:11 +00:00
|
|
|
|
2024-02-23 16:45:59 +00:00
|
|
|
|
2024-07-26 10:12:02 +00:00
|
|
|
def eligible_for_full_restoration(snap: Snapshot):
|
2024-08-07 12:35:34 +00:00
|
|
|
service = ServiceManager.get_service_by_id(snap.service_name)
|
2024-07-26 10:12:02 +00:00
|
|
|
if service is None:
|
|
|
|
return False
|
|
|
|
if service.is_enabled() is False:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2024-07-26 11:23:41 +00:00
|
|
|
def which_snapshots_to_full_restore() -> list[Snapshot]:
|
2024-09-06 08:40:32 +00:00
|
|
|
autoslice = Backups.last_backup_slice()
|
2024-07-26 10:12:02 +00:00
|
|
|
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)
|
|
|
|
]
|
2024-07-26 11:23:41 +00:00
|
|
|
# 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,
|
2024-09-11 09:59:53 +00:00
|
|
|
status_text="Finding the last autobackup session",
|
2024-07-26 11:23:41 +00:00
|
|
|
progress=0,
|
|
|
|
)
|
|
|
|
snapshots_to_restore = which_snapshots_to_full_restore()
|
2024-07-26 10:12:02 +00:00
|
|
|
|
|
|
|
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)
|
2024-09-23 11:35:46 +00:00
|
|
|
return
|
2024-07-26 10:12:02 +00:00
|
|
|
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)
|