Files
TISbackup/libtisbackup/base_driver.py
T

527 lines
25 KiB
Python
Raw Normal View History

2022-04-25 10:02:43 +02:00
#!/usr/bin/python3
2013-05-23 10:19:43 +02:00
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------
# This file is part of TISBackup
#
# TISBackup is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# TISBackup is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with TISBackup. If not, see <http://www.gnu.org/licenses/>.
#
# -----------------------------------------------------------------------
"""Base backup driver class and driver registry."""
2024-11-28 23:46:48 +01:00
import datetime
import logging
2013-05-23 10:19:43 +02:00
import os
import re
2024-11-28 23:46:48 +01:00
import shutil
import subprocess
import time
from abc import ABC, abstractmethod
from iniparse import ConfigParser
2013-05-23 10:19:43 +02:00
from .database import BackupStat
from .process import monitor_stdout
from .ssh import load_ssh_private_key
from .utils import dateof, datetime2isodate, isodate2datetime
2013-05-23 10:19:43 +02:00
try:
import paramiko
2022-04-25 10:02:43 +02:00
except ImportError as e:
print(("Error : can not load paramiko library %s" % e))
2013-05-23 10:19:43 +02:00
raise
# Nagios state constants
2013-05-23 10:19:43 +02:00
nagiosStateOk = 0
nagiosStateWarning = 1
nagiosStateCritical = 2
nagiosStateUnknown = 3
# Global driver registry
2013-05-23 10:19:43 +02:00
backup_drivers = {}
2024-11-29 22:54:39 +01:00
2013-05-23 10:19:43 +02:00
def register_driver(driverclass):
"""Register a backup driver class in the global registry."""
2013-05-23 10:19:43 +02:00
backup_drivers[driverclass.type] = driverclass
2024-11-29 22:54:39 +01:00
2022-04-25 10:02:43 +02:00
class backup_generic(ABC):
2013-05-23 10:19:43 +02:00
"""Generic ancestor class for backups, not registered"""
2024-11-29 22:54:39 +01:00
type = "generic"
required_params = ["type", "backup_name", "backup_dir", "server_name", "backup_retention_time", "maximum_backup_age"]
optional_params = ["preexec", "postexec", "description", "private_key", "remote_user", "ssh_port"]
logger = logging.getLogger("tisbackup")
backup_name = ""
backup_dir = ""
server_name = ""
remote_user = "root"
description = ""
2013-05-23 10:19:43 +02:00
dbstat = None
dry_run = False
2024-11-29 22:54:39 +01:00
preexec = ""
postexec = ""
2013-05-23 10:19:43 +02:00
maximum_backup_age = None
backup_retention_time = None
verbose = False
2024-11-29 22:54:39 +01:00
private_key = ""
ssh_port = 22
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
def __init__(self, backup_name, backup_dir, dbstat=None, dry_run=False):
2025-03-07 22:51:20 +01:00
if not re.match(r"^[A-Za-z0-9_\-\.]*$", backup_name):
2024-11-29 22:54:39 +01:00
raise Exception("The backup name %s should contain only alphanumerical characters" % backup_name)
2013-05-23 10:19:43 +02:00
self.backup_name = backup_name
self.backup_dir = backup_dir
self.dbstat = dbstat
2024-11-29 22:54:39 +01:00
assert isinstance(self.dbstat, BackupStat) or self.dbstat is None
2013-05-23 10:19:43 +02:00
if not os.path.isdir(self.backup_dir):
os.makedirs(self.backup_dir)
self.dry_run = dry_run
@classmethod
def get_help(cls):
return """\
%(type)s : %(desc)s
Required params : %(required)s
Optional params : %(optional)s
2024-11-29 22:54:39 +01:00
""" % {"type": cls.type, "desc": cls.__doc__, "required": ",".join(cls.required_params), "optional": ",".join(cls.optional_params)}
2013-05-23 10:19:43 +02:00
def check_required_params(self):
for name in self.required_params:
2024-11-29 22:54:39 +01:00
if not hasattr(self, name) or not getattr(self, name):
raise Exception("[%s] Config Attribute %s is required" % (self.backup_name, name))
2013-05-23 10:19:43 +02:00
if (self.preexec or self.postexec) and (not self.private_key or not self.remote_user):
2024-11-29 22:54:39 +01:00
raise Exception("[%s] remote_user and private_key file required if preexec or postexec is used" % self.backup_name)
def read_config(self, iniconf):
assert isinstance(iniconf, ConfigParser)
allowed_params = self.required_params + self.optional_params
for name, value in iniconf.items(self.backup_name):
if name not in allowed_params:
self.logger.critical('[%s] Invalid param name "%s"', self.backup_name, name)
raise Exception('[%s] Invalid param name "%s"', self.backup_name, name)
self.logger.debug("[%s] reading param %s = %s ", self.backup_name, name, value)
setattr(self, name, value)
2013-05-23 10:19:43 +02:00
# if retention (in days) is not defined at section level, get default global one.
if not self.backup_retention_time:
2024-11-29 22:54:39 +01:00
self.backup_retention_time = iniconf.getint("global", "backup_retention_time")
2013-05-23 10:19:43 +02:00
# for nagios, if maximum last backup age (in hours) is not defined at section level, get default global one.
if not self.maximum_backup_age:
2024-11-29 22:54:39 +01:00
self.maximum_backup_age = iniconf.getint("global", "maximum_backup_age")
2013-05-23 10:19:43 +02:00
self.ssh_port = int(self.ssh_port)
self.backup_retention_time = int(self.backup_retention_time)
self.maximum_backup_age = int(self.maximum_backup_age)
self.check_required_params()
2024-11-29 22:54:39 +01:00
def do_preexec(self, stats):
self.logger.info("[%s] executing preexec %s ", self.backup_name, self.preexec)
mykey = load_ssh_private_key(self.private_key)
2013-05-23 10:19:43 +02:00
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(self.server_name, username=self.remote_user, pkey=mykey, port=self.ssh_port)
2013-05-23 10:19:43 +02:00
tran = ssh.get_transport()
chan = tran.open_session()
# chan.set_combine_stderr(True)
chan.get_pty()
stdout = chan.makefile()
if not self.dry_run:
chan.exec_command(self.preexec)
output = stdout.read()
exit_code = chan.recv_exit_status()
2024-11-29 22:54:39 +01:00
self.logger.info('[%s] preexec exit code : "%i", output : %s', self.backup_name, exit_code, output)
2013-05-23 10:19:43 +02:00
return exit_code
else:
return 0
2024-11-29 22:54:39 +01:00
def do_postexec(self, stats):
self.logger.info("[%s] executing postexec %s ", self.backup_name, self.postexec)
mykey = load_ssh_private_key(self.private_key)
2013-05-23 10:19:43 +02:00
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(self.server_name, username=self.remote_user, pkey=mykey, port=self.ssh_port)
2013-05-23 10:19:43 +02:00
tran = ssh.get_transport()
chan = tran.open_session()
# chan.set_combine_stderr(True)
chan.get_pty()
stdout = chan.makefile()
if not self.dry_run:
chan.exec_command(self.postexec)
output = stdout.read()
exit_code = chan.recv_exit_status()
2024-11-29 22:54:39 +01:00
self.logger.info('[%s] postexec exit code : "%i", output : %s', self.backup_name, exit_code, output)
2013-05-23 10:19:43 +02:00
return exit_code
else:
return 0
2024-11-29 22:54:39 +01:00
def do_backup(self, stats):
2013-05-23 10:19:43 +02:00
"""stats dict with keys : total_files_count,written_files_count,total_bytes,written_bytes"""
pass
def check_params_connections(self):
"""Perform a dry run trying to connect without actually doing backup"""
self.check_required_params()
def process_backup(self):
"""Process the backup.
2024-11-29 22:54:39 +01:00
launch
- do_preexec
- do_backup
- do_postexec
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
returns a dict for stats
2013-05-23 10:19:43 +02:00
"""
2024-11-29 22:54:39 +01:00
self.logger.info("[%s] ######### Starting backup", self.backup_name)
2013-05-23 10:19:43 +02:00
starttime = time.time()
2024-11-29 22:54:39 +01:00
self.backup_start_date = datetime.datetime.now().strftime("%Y%m%d-%Hh%Mm%S")
2013-05-23 10:19:43 +02:00
if not self.dry_run and self.dbstat:
2024-11-29 22:54:39 +01:00
stat_rowid = self.dbstat.start(backup_name=self.backup_name, server_name=self.server_name, TYPE="BACKUP")
2013-05-23 10:19:43 +02:00
else:
stat_rowid = None
try:
stats = {}
2024-11-29 22:54:39 +01:00
stats["total_files_count"] = 0
stats["written_files_count"] = 0
stats["total_bytes"] = 0
stats["written_bytes"] = 0
stats["log"] = ""
stats["status"] = "Running"
stats["backup_location"] = None
2013-05-23 10:19:43 +02:00
if self.preexec.strip():
exit_code = self.do_preexec(stats)
2024-11-29 22:54:39 +01:00
if exit_code != 0:
raise Exception('Preexec "%s" failed with exit code "%i"' % (self.preexec, exit_code))
2013-05-23 10:19:43 +02:00
self.do_backup(stats)
if self.postexec.strip():
exit_code = self.do_postexec(stats)
2024-11-29 22:54:39 +01:00
if exit_code != 0:
raise Exception('Postexec "%s" failed with exit code "%i"' % (self.postexec, exit_code))
2013-05-23 10:19:43 +02:00
endtime = time.time()
2024-11-29 22:54:39 +01:00
duration = (endtime - starttime) / 3600.0
2013-05-23 10:19:43 +02:00
if not self.dry_run and self.dbstat:
2024-11-29 22:54:39 +01:00
self.dbstat.finish(
stat_rowid,
backup_end=datetime2isodate(datetime.datetime.now()),
backup_duration=duration,
total_files_count=stats["total_files_count"],
written_files_count=stats["written_files_count"],
total_bytes=stats["total_bytes"],
written_bytes=stats["written_bytes"],
status=stats["status"],
log=stats["log"],
backup_location=stats["backup_location"],
)
self.logger.info("[%s] ######### Backup finished : %s", self.backup_name, stats["log"])
2013-05-23 10:19:43 +02:00
return stats
2022-04-25 10:02:43 +02:00
except BaseException as e:
2024-11-29 22:54:39 +01:00
stats["status"] = "ERROR"
stats["log"] = str(e)
2013-05-23 10:19:43 +02:00
endtime = time.time()
2024-11-29 22:54:39 +01:00
duration = (endtime - starttime) / 3600.0
2013-05-23 10:19:43 +02:00
if not self.dry_run and self.dbstat:
2024-11-29 22:54:39 +01:00
self.dbstat.finish(
stat_rowid,
backup_end=datetime2isodate(datetime.datetime.now()),
backup_duration=duration,
total_files_count=stats["total_files_count"],
written_files_count=stats["written_files_count"],
total_bytes=stats["total_bytes"],
written_bytes=stats["written_bytes"],
status=stats["status"],
log=stats["log"],
backup_location=stats["backup_location"],
)
self.logger.error("[%s] ######### Backup finished with ERROR: %s", self.backup_name, stats["log"])
raise
2013-05-23 10:19:43 +02:00
def checknagios(self):
2013-05-23 10:19:43 +02:00
"""
Returns a tuple (nagiosstatus,message) for the current backup_name
Read status from dbstat database
"""
if not self.dbstat:
2024-11-29 22:54:39 +01:00
self.logger.warn("[%s] checknagios : no database provided", self.backup_name)
return ("No database provided", nagiosStateUnknown)
2013-05-23 10:19:43 +02:00
else:
2024-11-29 22:54:39 +01:00
self.logger.debug(
'[%s] checknagios : sql query "%s" %s',
self.backup_name,
"select status, backup_end, log from stats where TYPE='BACKUP' AND backup_name=? order by backup_end desc limit 30",
self.backup_name,
)
q = self.dbstat.query(
"select status, backup_start, backup_end, log, backup_location, total_bytes from stats where TYPE='BACKUP' AND backup_name=? order by backup_start desc limit 30",
(self.backup_name,),
)
2013-05-23 10:19:43 +02:00
if not q:
2024-11-29 22:54:39 +01:00
self.logger.debug("[%s] checknagios : no result from query", self.backup_name)
return (nagiosStateCritical, "CRITICAL : No backup found for %s in database" % self.backup_name)
2013-05-23 10:19:43 +02:00
else:
mindate = datetime2isodate((datetime.datetime.now() - datetime.timedelta(hours=self.maximum_backup_age)))
2024-11-29 22:54:39 +01:00
self.logger.debug("[%s] checknagios : looking for most recent OK not older than %s", self.backup_name, mindate)
2013-05-23 10:19:43 +02:00
for b in q:
2024-11-29 22:54:39 +01:00
if b["backup_end"] >= mindate and b["status"] == "OK":
2013-05-23 10:19:43 +02:00
# check if backup actually exists on registered backup location and is newer than backup start date
2024-11-29 22:54:39 +01:00
if b["total_bytes"] == 0:
return (nagiosStateWarning, "WARNING : No data to backup was found for %s" % (self.backup_name,))
if not b["backup_location"]:
return (
nagiosStateWarning,
"WARNING : No Backup location found for %s finished on (%s) %s"
% (self.backup_name, isodate2datetime(b["backup_end"]), b["log"]),
)
if os.path.isfile(b["backup_location"]):
backup_actual_date = datetime.datetime.fromtimestamp(os.stat(b["backup_location"]).st_ctime)
if backup_actual_date + datetime.timedelta(hours=1) > isodate2datetime(b["backup_start"]):
return (
nagiosStateOk,
"OK Backup %s (%s), %s" % (self.backup_name, isodate2datetime(b["backup_end"]), b["log"]),
)
2013-05-23 10:19:43 +02:00
else:
2024-11-29 22:54:39 +01:00
return (
nagiosStateCritical,
"CRITICAL Backup %s (%s), %s seems older than start of backup"
% (self.backup_name, isodate2datetime(b["backup_end"]), b["log"]),
)
elif os.path.isdir(b["backup_location"]):
return (
nagiosStateOk,
"OK Backup %s (%s), %s" % (self.backup_name, isodate2datetime(b["backup_end"]), b["log"]),
)
elif self.type == "copy-vm-xcp":
return (
nagiosStateOk,
"OK Backup %s (%s), %s" % (self.backup_name, isodate2datetime(b["backup_end"]), b["log"]),
)
2013-05-23 10:19:43 +02:00
else:
2024-11-29 22:54:39 +01:00
return (
nagiosStateCritical,
"CRITICAL Backup %s (%s), %s has disapeared from backup location %s"
% (self.backup_name, isodate2datetime(b["backup_end"]), b["log"], b["backup_location"]),
)
self.logger.debug(
"[%s] checknagios : looking for most recent Warning or Running not older than %s", self.backup_name, mindate
)
2013-05-23 10:19:43 +02:00
for b in q:
2024-11-29 22:54:39 +01:00
if b["backup_end"] >= mindate and b["status"] in ("Warning", "Running"):
return (nagiosStateWarning, "WARNING : Backup %s still running or warning. %s" % (self.backup_name, b["log"]))
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
self.logger.debug("[%s] checknagios : No Ok or warning recent backup found", self.backup_name)
return (nagiosStateCritical, "CRITICAL : No recent backup for %s" % self.backup_name)
2013-05-23 10:19:43 +02:00
def cleanup_backup(self):
"""Removes obsolete backups (older than backup_retention_time)"""
mindate = datetime2isodate((dateof(datetime.datetime.now()) - datetime.timedelta(days=self.backup_retention_time)))
# check if there is at least 1 "OK" backup left after cleanup :
2024-11-29 22:54:39 +01:00
ok_backups = self.dbstat.query(
'select backup_location from stats where TYPE="BACKUP" and backup_name=? and backup_start>=? and status="OK" order by backup_start desc',
(self.backup_name, mindate),
)
2013-05-23 10:19:43 +02:00
removed = []
2024-11-29 22:54:39 +01:00
if ok_backups and os.path.exists(ok_backups[0]["backup_location"]):
records = self.dbstat.query(
'select status, backup_start, backup_end, log, backup_location from stats where backup_name=? and backup_start<? and backup_location is not null and TYPE="BACKUP" order by backup_start',
(self.backup_name, mindate),
)
2013-05-23 10:19:43 +02:00
if records:
2024-11-29 22:54:39 +01:00
for oldbackup_location in [rec["backup_location"] for rec in records if rec["backup_location"]]:
2013-05-23 10:19:43 +02:00
try:
2024-11-29 22:54:39 +01:00
if os.path.isdir(oldbackup_location) and self.backup_dir in oldbackup_location:
self.logger.info('[%s] removing directory "%s"', self.backup_name, oldbackup_location)
2013-05-23 10:19:43 +02:00
if not self.dry_run:
2024-11-29 22:54:39 +01:00
if self.type == "rsync+btrfs+ssh" or self.type == "rsync+btrfs":
cmd = "/bin/btrfs subvolume delete %s" % oldbackup_location
process = subprocess.Popen(
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True
)
log = monitor_stdout(process, "", self)
returncode = process.returncode
2024-11-29 22:54:39 +01:00
if returncode != 0:
self.logger.error("[" + self.backup_name + "] shell program exited with error code: %s" % log)
raise Exception(
"[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd
)
else:
2024-11-29 22:54:39 +01:00
self.logger.info(
"[" + self.backup_name + "] deleting snapshot volume: %s" % oldbackup_location.encode("ascii")
)
else:
2024-11-29 22:54:39 +01:00
shutil.rmtree(oldbackup_location.encode("ascii"))
if os.path.isfile(oldbackup_location) and self.backup_dir in oldbackup_location:
self.logger.debug('[%s] removing file "%s"', self.backup_name, oldbackup_location)
2013-05-23 10:19:43 +02:00
if not self.dry_run:
os.remove(oldbackup_location)
2024-11-29 22:54:39 +01:00
self.logger.debug('Cleanup_backup : Removing records from DB : [%s]-"%s"', self.backup_name, oldbackup_location)
2013-05-23 10:19:43 +02:00
if not self.dry_run:
2024-11-29 22:54:39 +01:00
self.dbstat.db.execute(
'update stats set TYPE="CLEAN" where backup_name=? and backup_location=?',
(self.backup_name, oldbackup_location),
)
2013-05-23 10:19:43 +02:00
self.dbstat.db.commit()
2022-04-25 10:02:43 +02:00
except BaseException as e:
2024-11-29 22:54:39 +01:00
self.logger.error('cleanup_backup : Unable to remove directory/file "%s". Error %s', oldbackup_location, e)
removed.append((self.backup_name, oldbackup_location))
2013-05-23 10:19:43 +02:00
else:
2024-11-29 22:54:39 +01:00
self.logger.debug("[%s] cleanup : no result for query", self.backup_name)
2013-05-23 10:19:43 +02:00
else:
2024-11-29 22:54:39 +01:00
self.logger.info("Nothing to do because we want to keep at least one OK backup after cleaning")
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
self.logger.info(
"[%s] Cleanup finished : removed : %s", self.backup_name, ",".join([('[%s]-"%s"') % r for r in removed]) or "Nothing"
)
2013-05-23 10:19:43 +02:00
return removed
2022-04-25 10:02:43 +02:00
@abstractmethod
2013-05-23 10:19:43 +02:00
def register_existingbackups(self):
2022-04-25 10:02:43 +02:00
pass
# """scan existing backups and insert stats in database"""
# registered = [b['backup_location'] for b in self.dbstat.query('select distinct backup_location from stats where backup_name=?',[self.backup_name])]
# raise Exception('Abstract method')
2024-11-29 22:54:39 +01:00
def export_latestbackup(self, destdir):
"""Copy (rsync) latest OK backup to external storage located at locally mounted "destdir" """
2013-05-23 10:19:43 +02:00
stats = {}
2024-11-29 22:54:39 +01:00
stats["total_files_count"] = 0
stats["written_files_count"] = 0
stats["total_bytes"] = 0
stats["written_bytes"] = 0
stats["log"] = ""
stats["status"] = "Running"
2013-05-23 10:19:43 +02:00
if not self.dbstat:
2024-11-29 22:54:39 +01:00
self.logger.critical("[%s] export_latestbackup : no database provided", self.backup_name)
raise Exception("No database")
2013-05-23 10:19:43 +02:00
else:
latest_sql = """\
select status, backup_start, backup_end, log, backup_location, total_bytes
from stats
2013-05-23 10:19:43 +02:00
where backup_name=? and status='OK' and TYPE='BACKUP'
order by backup_start desc limit 30"""
2024-11-29 22:54:39 +01:00
self.logger.debug('[%s] export_latestbackup : sql query "%s" %s', self.backup_name, latest_sql, self.backup_name)
q = self.dbstat.query(latest_sql, (self.backup_name,))
2013-05-23 10:19:43 +02:00
if not q:
2024-11-29 22:54:39 +01:00
self.logger.debug("[%s] export_latestbackup : no result from query", self.backup_name)
raise Exception("No OK backup found for %s in database" % self.backup_name)
2013-05-23 10:19:43 +02:00
else:
latest = q[0]
2024-11-29 22:54:39 +01:00
backup_source = latest["backup_location"]
backup_dest = os.path.join(os.path.abspath(destdir), self.backup_name)
2013-05-23 10:19:43 +02:00
if not os.path.exists(backup_source):
2024-11-29 22:54:39 +01:00
raise Exception("Backup source %s doesn't exists" % backup_source)
2013-05-23 10:19:43 +02:00
# ensure there is a slash at end
2024-11-29 22:54:39 +01:00
if os.path.isdir(backup_source) and backup_source[-1] != "/":
backup_source += "/"
if backup_dest[-1] != "/":
backup_dest += "/"
2013-05-23 10:19:43 +02:00
if not os.path.isdir(backup_dest):
os.makedirs(backup_dest)
2024-11-29 22:54:39 +01:00
options = ["-aP", "--stats", "--delete-excluded", "--numeric-ids", "--delete-after"]
2013-05-23 10:19:43 +02:00
if self.logger.level:
2024-11-29 22:54:39 +01:00
options.append("-P")
2013-05-23 10:19:43 +02:00
if self.dry_run:
2024-11-29 22:54:39 +01:00
options.append("-d")
2013-05-23 10:19:43 +02:00
options_params = " ".join(options)
2024-11-29 22:54:39 +01:00
cmd = "/usr/bin/rsync %s %s %s 2>&1" % (options_params, backup_source, backup_dest)
self.logger.debug("[%s] rsync : %s", self.backup_name, cmd)
2013-05-23 10:19:43 +02:00
if not self.dry_run:
2024-11-29 22:54:39 +01:00
self.line = ""
2013-05-23 10:19:43 +02:00
starttime = time.time()
2024-11-29 22:54:39 +01:00
stat_rowid = self.dbstat.start(backup_name=self.backup_name, server_name=self.server_name, TYPE="EXPORT")
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
def ondata(data, context):
2013-05-23 10:19:43 +02:00
if context.verbose:
2022-04-25 10:02:43 +02:00
print(data)
2013-05-23 10:19:43 +02:00
context.logger.debug(data)
2024-11-29 22:54:39 +01:00
log = monitor_stdout(process, ondata, self)
2013-05-23 10:19:43 +02:00
for l in log.splitlines():
2024-11-29 22:54:39 +01:00
if l.startswith("Number of files:"):
stats["total_files_count"] += int(re.findall("[0-9]+", l.split(":")[1])[0])
if l.startswith("Number of files transferred:"):
stats["written_files_count"] += int(l.split(":")[1])
if l.startswith("Total file size:"):
stats["total_bytes"] += float(l.replace(",", "").split(":")[1].split()[0])
if l.startswith("Total transferred file size:"):
stats["written_bytes"] += float(l.replace(",", "").split(":")[1].split()[0])
2013-05-23 10:19:43 +02:00
returncode = process.returncode
## deal with exit code 24 (file vanished)
2024-11-29 22:54:39 +01:00
if returncode == 24:
2013-05-23 10:19:43 +02:00
self.logger.warning("[" + self.backup_name + "] Note: some files vanished before transfer")
2024-11-29 22:54:39 +01:00
elif returncode == 23:
2013-05-23 10:19:43 +02:00
self.logger.warning("[" + self.backup_name + "] unable so set uid on some files")
2024-11-29 22:54:39 +01:00
elif returncode != 0:
2013-05-23 10:19:43 +02:00
self.logger.error("[" + self.backup_name + "] shell program exited with error code ")
raise Exception("[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd)
else:
2022-04-25 10:02:43 +02:00
print(cmd)
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
stats["status"] = "OK"
self.logger.info(
"export backup from %s to %s OK, %d bytes written for %d changed files"
% (backup_source, backup_dest, stats["written_bytes"], stats["written_files_count"])
)
2013-05-23 10:19:43 +02:00
endtime = time.time()
2024-11-29 22:54:39 +01:00
duration = (endtime - starttime) / 3600.0
2013-05-23 10:19:43 +02:00
if not self.dry_run and self.dbstat:
2024-11-29 22:54:39 +01:00
self.dbstat.finish(
stat_rowid,
backup_end=datetime2isodate(datetime.datetime.now()),
backup_duration=duration,
total_files_count=stats["total_files_count"],
written_files_count=stats["written_files_count"],
total_bytes=stats["total_bytes"],
written_bytes=stats["written_bytes"],
status=stats["status"],
log=stats["log"],
backup_location=backup_dest,
)
2013-05-23 10:19:43 +02:00
return stats