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/>.
#
# -----------------------------------------------------------------------
2025-10-05 23:54:26 +02:00
"""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
2025-10-05 23:54:26 +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
2025-10-05 23:54:26 +02:00
# Nagios state constants
2013-05-23 10:19:43 +02:00
nagiosStateOk = 0
nagiosStateWarning = 1
nagiosStateCritical = 2
nagiosStateUnknown = 3
2025-10-05 23:54:26 +02:00
# 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 ):
2025-10-05 23:54:26 +02:00
"""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 )
2025-10-05 01:39:17 +02:00
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 ())
2025-10-05 01:39:17 +02:00
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 )
2025-10-05 01:39:17 +02:00
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 ())
2025-10-05 01:39:17 +02:00
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" ])
2018-01-30 12:29:16 +01:00
raise
2013-05-23 10:19:43 +02:00
2018-01-30 12:29:16 +01: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 :
2018-01-30 12:29:16 +01:00
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 )
2014-07-25 15:06:51 +02:00
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
)
2014-07-25 15:06:51 +02:00
else :
2024-11-29 22:54:39 +01:00
self . logger . info (
"[" + self . backup_name + "] deleting snapshot volume: %s " % oldbackup_location . encode ( "ascii" )
)
2014-07-25 15:06:51 +02:00
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 :
2018-01-30 12:29:16 +01:00
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')
2018-01-30 12:29:16 +01:00
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 = """ \
2018-01-30 12:29:16 +01:00
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 += "/"
2018-01-30 12:29:16 +01:00
2013-05-23 10:19:43 +02:00
if not os . path . isdir ( backup_dest ):
os . makedirs ( backup_dest )
2018-01-30 12:29:16 +01:00
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" ])
)
2018-01-30 12:29:16 +01:00
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