2018-11-22 03:26:26 +00:00
|
|
|
import logging
|
2021-10-27 04:37:28 +00:00
|
|
|
from datetime import datetime as dt, date, timedelta
|
2019-01-13 01:51:35 +00:00
|
|
|
|
2021-02-11 06:09:22 +00:00
|
|
|
from PyQt5 import QtCore
|
2021-10-04 11:31:41 +00:00
|
|
|
from vorta.borg.check import BorgCheckJob
|
|
|
|
from vorta.borg.create import BorgCreateJob
|
|
|
|
from vorta.borg.job_scheduler import DEBUG, JobsManager
|
|
|
|
from vorta.borg.list_repo import BorgListRepoJob
|
|
|
|
from vorta.borg.prune import BorgPruneJob
|
2019-01-20 03:50:10 +00:00
|
|
|
from vorta.i18n import translate
|
2019-01-13 01:51:35 +00:00
|
|
|
|
2020-06-01 23:58:09 +00:00
|
|
|
from vorta.models import BackupProfileModel, EventLogModel
|
|
|
|
from vorta.notifications import VortaNotifications
|
2018-11-06 05:13:49 +00:00
|
|
|
|
2019-01-13 01:51:35 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
2018-11-22 03:26:26 +00:00
|
|
|
|
2018-10-31 16:09:01 +00:00
|
|
|
|
2021-10-27 04:37:28 +00:00
|
|
|
class VortaScheduler(QtCore.QObject):
|
|
|
|
def __init__(self):
|
2018-10-31 11:14:12 +00:00
|
|
|
super().__init__()
|
2021-10-27 04:37:28 +00:00
|
|
|
self.jobs_manager = JobsManager() # push scheduled jobs to JobManager for execution
|
|
|
|
self.timers = dict() # keep mapping of profiles to timers
|
2018-10-29 05:12:45 +00:00
|
|
|
|
2021-10-27 04:37:28 +00:00
|
|
|
# Set additional timer to make sure background tasks stay scheduled.
|
|
|
|
# E.g. after hibernation
|
2021-02-11 06:09:22 +00:00
|
|
|
self.qt_timer = QtCore.QTimer()
|
2021-10-27 04:37:28 +00:00
|
|
|
self.qt_timer.timeout.connect(self.reload_all_timers)
|
|
|
|
self.qt_timer.setInterval(15 * 60 * 1000)
|
2021-02-11 06:09:22 +00:00
|
|
|
self.qt_timer.start()
|
|
|
|
|
2021-10-04 11:31:41 +00:00
|
|
|
def cancel_all_jobs(self):
|
2021-10-27 04:37:28 +00:00
|
|
|
logger.debug('Cancelling all jobs on job queue')
|
2021-10-04 11:31:41 +00:00
|
|
|
self.jobs_manager.cancel_all_jobs()
|
|
|
|
|
2019-01-20 03:50:10 +00:00
|
|
|
def tr(self, *args, **kwargs):
|
|
|
|
scope = self.__class__.__name__
|
|
|
|
return translate(scope, *args, **kwargs)
|
|
|
|
|
2021-10-27 04:37:28 +00:00
|
|
|
def set_timer_for_profile(self, profile_id):
|
|
|
|
"""
|
|
|
|
Set a timer for next scheduled backup run of this profile.
|
|
|
|
|
|
|
|
Does nothing if set to manual backups or no repo is assigned.
|
|
|
|
|
|
|
|
Else will look for previous scheduled backups and catch up if
|
|
|
|
schedule_make_up_missed is enabled.
|
|
|
|
|
|
|
|
Or, if catch-up is not enabled, will add interval to last run to find
|
|
|
|
next suitable backup time.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Stop and remove any existing timer for this profile
|
|
|
|
if profile_id in self.timers:
|
|
|
|
self.timers[profile_id]['qtt'].stop()
|
|
|
|
del self.timers[profile_id]
|
|
|
|
|
|
|
|
profile = BackupProfileModel.get_or_none(id=profile_id)
|
|
|
|
if profile is None \
|
|
|
|
or profile.repo is None \
|
|
|
|
or profile.schedule_mode == 'off':
|
|
|
|
return
|
|
|
|
|
|
|
|
logger.info('Setting timer for profile %s', profile_id)
|
|
|
|
|
|
|
|
last_run_log = EventLogModel.select().where(
|
|
|
|
EventLogModel.subcommand == 'create',
|
|
|
|
EventLogModel.category == 'scheduled',
|
|
|
|
EventLogModel.profile == profile.id,
|
|
|
|
).order_by(EventLogModel.end_time.desc()).first()
|
|
|
|
|
|
|
|
# Desired interval between scheduled backups. Uses datetime.timedelta() units.
|
|
|
|
if profile.schedule_mode == 'interval':
|
|
|
|
interval = {profile.schedule_interval_unit: profile.schedule_interval_count}
|
|
|
|
elif profile.schedule_mode == 'fixed':
|
|
|
|
interval = {'days': 1}
|
|
|
|
|
|
|
|
# If last run was too long ago and catch-up is enabled, run now
|
|
|
|
if profile.schedule_make_up_missed \
|
|
|
|
and last_run_log is not None \
|
|
|
|
and last_run_log.end_time + timedelta(**interval) < dt.now():
|
|
|
|
logger.debug('Catching up by running job for %s', profile.name)
|
|
|
|
self.create_backup(profile.id)
|
|
|
|
return
|
|
|
|
|
|
|
|
# If the job never ran, use midnight as random starting point
|
|
|
|
if last_run_log is None:
|
|
|
|
last_run = dt.now().replace(hour=0, minute=0)
|
|
|
|
else:
|
|
|
|
last_run = last_run_log.end_time
|
|
|
|
|
|
|
|
# Squash seconds to get nice starting time
|
|
|
|
last_run = last_run.replace(second=0, microsecond=0)
|
|
|
|
|
|
|
|
# Fixed time is a special case of days=1 interval
|
|
|
|
if profile.schedule_mode == 'fixed':
|
|
|
|
last_run = last_run.replace(hour=profile.schedule_fixed_hour, minute=profile.schedule_fixed_minute)
|
|
|
|
|
|
|
|
# Add interval to last run time to arrive at next run.
|
|
|
|
next_run = last_run
|
|
|
|
now = dt.now()
|
|
|
|
while next_run < now:
|
|
|
|
next_run += timedelta(**interval)
|
|
|
|
|
|
|
|
logger.debug('Scheduling next run for %s', next_run)
|
|
|
|
timer_ms = (next_run - dt.now()).total_seconds() * 1000
|
|
|
|
timer = QtCore.QTimer()
|
|
|
|
timer.setSingleShot(True)
|
|
|
|
timer.setInterval(int(timer_ms))
|
|
|
|
timer.timeout.connect(lambda: self.create_backup(profile_id))
|
|
|
|
timer.start()
|
|
|
|
self.timers[profile_id] = {'qtt': timer, 'dt': next_run}
|
|
|
|
|
|
|
|
def reload_all_timers(self):
|
|
|
|
logger.debug('Refreshing all scheduler timers')
|
2018-11-17 08:51:53 +00:00
|
|
|
for profile in BackupProfileModel.select():
|
2021-10-27 04:37:28 +00:00
|
|
|
self.set_timer_for_profile(profile.id)
|
2018-10-31 16:09:01 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def next_job(self):
|
2021-10-27 04:37:28 +00:00
|
|
|
next_job = dt.now()
|
|
|
|
next_profile = None
|
|
|
|
for profile_id, timer in self.timers.items():
|
|
|
|
if timer['dt'] > next_job and timer['qtt'].isActive():
|
|
|
|
next_job = timer['dt']
|
|
|
|
next_profile = profile_id
|
|
|
|
|
|
|
|
if next_profile is not None:
|
|
|
|
profile = BackupProfileModel.get_or_none(id=next_profile)
|
|
|
|
return f"{next_job.strftime('%H:%M')} ({profile.name})"
|
2018-11-17 08:51:53 +00:00
|
|
|
else:
|
2019-01-20 03:50:10 +00:00
|
|
|
return self.tr('None scheduled')
|
2018-11-17 08:51:53 +00:00
|
|
|
|
|
|
|
def next_job_for_profile(self, profile_id):
|
2021-10-27 04:37:28 +00:00
|
|
|
job = self.timers.get(profile_id)
|
2018-11-01 01:10:11 +00:00
|
|
|
if job is None:
|
2019-01-20 03:50:10 +00:00
|
|
|
return self.tr('None scheduled')
|
2018-11-01 01:10:11 +00:00
|
|
|
else:
|
2021-10-27 04:37:28 +00:00
|
|
|
return job['dt'].strftime('%Y-%m-%d %H:%M')
|
2018-10-31 16:09:01 +00:00
|
|
|
|
2021-10-27 04:37:28 +00:00
|
|
|
def create_backup(self, profile_id):
|
|
|
|
logger.debug('Start scheduled backup for profile %s', profile_id)
|
2019-01-13 01:51:35 +00:00
|
|
|
notifier = VortaNotifications.pick()
|
2021-10-27 04:37:28 +00:00
|
|
|
profile = BackupProfileModel.get_or_none(id=profile_id)
|
|
|
|
|
|
|
|
if profile is None:
|
|
|
|
logger.info('Profile not found. Maybe deleted?')
|
|
|
|
return
|
2019-01-13 01:51:35 +00:00
|
|
|
|
2018-11-22 03:26:26 +00:00
|
|
|
logger.info('Starting background backup for %s', profile.name)
|
2019-01-20 03:50:10 +00:00
|
|
|
notifier.deliver(self.tr('Vorta Backup'),
|
|
|
|
self.tr('Starting background backup for %s.') % profile.name,
|
|
|
|
level='info')
|
2021-10-04 11:31:41 +00:00
|
|
|
msg = BorgCreateJob.prepare(profile)
|
2018-10-31 16:09:01 +00:00
|
|
|
if msg['ok']:
|
2018-11-22 03:26:26 +00:00
|
|
|
logger.info('Preparation for backup successful.')
|
2021-10-27 04:37:28 +00:00
|
|
|
msg['category'] = 'scheduled'
|
|
|
|
job = BorgCreateJob(msg['cmd'], msg, profile.repo.id)
|
2021-10-04 11:31:41 +00:00
|
|
|
job.result.connect(self.notify)
|
|
|
|
self.jobs_manager.add_job(job)
|
|
|
|
|
2018-11-06 05:13:49 +00:00
|
|
|
else:
|
2018-11-22 03:26:26 +00:00
|
|
|
logger.error('Conditions for backup not met. Aborting.')
|
|
|
|
logger.error(msg['message'])
|
2019-01-20 03:50:10 +00:00
|
|
|
notifier.deliver(self.tr('Vorta Backup'), translate('messages', msg['message']), level='error')
|
2021-10-04 11:31:41 +00:00
|
|
|
if DEBUG:
|
|
|
|
print("End backup for profile ", profile_id)
|
|
|
|
|
|
|
|
def notify(self, result):
|
|
|
|
notifier = VortaNotifications.pick()
|
|
|
|
profile_name = result['params']['profile_name']
|
|
|
|
profile_id = result['params']['profile']
|
|
|
|
|
|
|
|
if result['returncode'] in [0, 1]:
|
|
|
|
notifier.deliver(self.tr('Vorta Backup'),
|
|
|
|
self.tr('Backup successful for %s.') % profile_name,
|
|
|
|
level='info')
|
|
|
|
logger.info('Backup creation successful.')
|
|
|
|
self.post_backup_tasks(profile_id)
|
|
|
|
else:
|
|
|
|
notifier.deliver(self.tr('Vorta Backup'), self.tr('Error during backup creation.'), level='error')
|
|
|
|
logger.error('Error during backup creation.')
|
2018-11-04 15:37:46 +00:00
|
|
|
|
2021-10-27 04:37:28 +00:00
|
|
|
self.set_timer_for_profile(profile_id)
|
|
|
|
|
2018-11-17 08:51:53 +00:00
|
|
|
def post_backup_tasks(self, profile_id):
|
2018-11-04 15:37:46 +00:00
|
|
|
"""
|
|
|
|
Pruning and checking after successful backup.
|
|
|
|
"""
|
2018-11-17 08:51:53 +00:00
|
|
|
profile = BackupProfileModel.get(id=profile_id)
|
2018-11-22 03:26:26 +00:00
|
|
|
logger.info('Doing post-backup jobs for %s', profile.name)
|
2018-11-04 15:37:46 +00:00
|
|
|
if profile.prune_on:
|
2021-10-04 11:31:41 +00:00
|
|
|
msg = BorgPruneJob.prepare(profile)
|
2018-11-04 15:37:46 +00:00
|
|
|
if msg['ok']:
|
2021-10-04 11:31:41 +00:00
|
|
|
job = BorgPruneJob(msg['cmd'], msg, profile.repo.id)
|
|
|
|
self.jobs_manager.add_job(job)
|
2018-11-04 15:37:46 +00:00
|
|
|
|
2019-01-20 03:50:10 +00:00
|
|
|
# Refresh archives
|
2021-10-04 11:31:41 +00:00
|
|
|
msg = BorgListRepoJob.prepare(profile)
|
2018-11-04 15:37:46 +00:00
|
|
|
if msg['ok']:
|
2021-10-04 11:31:41 +00:00
|
|
|
job = BorgListRepoJob(msg['cmd'], msg, profile.repo.id)
|
|
|
|
self.jobs_manager.add_job(job)
|
2018-11-04 08:23:17 +00:00
|
|
|
|
2018-11-22 00:43:37 +00:00
|
|
|
validation_cutoff = date.today() - timedelta(days=7 * profile.validation_weeks)
|
2018-11-04 15:37:46 +00:00
|
|
|
recent_validations = EventLogModel.select().where(
|
2018-11-22 20:21:52 +00:00
|
|
|
(
|
|
|
|
EventLogModel.subcommand == 'check'
|
|
|
|
) & (
|
|
|
|
EventLogModel.start_time > validation_cutoff
|
|
|
|
) & (
|
|
|
|
EventLogModel.repo_url == profile.repo.url
|
|
|
|
)
|
2018-11-04 15:37:46 +00:00
|
|
|
).count()
|
|
|
|
if profile.validation_on and recent_validations == 0:
|
2021-10-04 11:31:41 +00:00
|
|
|
msg = BorgCheckJob.prepare(profile)
|
2018-11-04 15:37:46 +00:00
|
|
|
if msg['ok']:
|
2021-10-04 11:31:41 +00:00
|
|
|
job = BorgCheckJob(msg['cmd'], msg, profile.repo.id)
|
|
|
|
self.jobs_manager.add_job(job)
|
2018-11-22 03:26:26 +00:00
|
|
|
|
|
|
|
logger.info('Finished background task for profile %s', profile.name)
|