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/>.
#
# -----------------------------------------------------------------------
2024-11-28 23:46:48 +01:00
import datetime
2024-11-29 22:54:39 +01:00
import errno
2024-11-28 23:46:48 +01:00
import logging
2013-05-23 10:19:43 +02:00
import os
import re
import select
2024-11-28 23:46:48 +01:00
import shutil
import sqlite3
import subprocess
2013-05-23 10:19:43 +02:00
import sys
2024-11-28 23:46:48 +01:00
import time
from abc import ABC , abstractmethod
from iniparse import ConfigParser
2013-05-23 10:19:43 +02:00
try :
2024-11-29 22:54:39 +01:00
sys . stderr = open ( " /dev/null " ) # Silence silly warnings from paramiko
2013-05-23 10:19:43 +02:00
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
sys . stderr = sys . __stderr__
nagiosStateOk = 0
nagiosStateWarning = 1
nagiosStateCritical = 2
nagiosStateUnknown = 3
backup_drivers = { }
2024-11-29 22:54:39 +01:00
2013-05-23 10:19:43 +02:00
def register_driver ( driverclass ) :
backup_drivers [ driverclass . type ] = driverclass
2024-11-29 22:54:39 +01:00
2013-05-23 10:19:43 +02:00
def datetime2isodate ( adatetime = None ) :
if not adatetime :
adatetime = datetime . datetime . now ( )
2024-11-29 22:54:39 +01:00
assert isinstance ( adatetime , datetime . datetime )
2013-05-23 10:19:43 +02:00
return adatetime . isoformat ( )
2024-11-29 22:54:39 +01:00
2013-05-23 10:19:43 +02:00
def isodate2datetime ( isodatestr ) :
# we remove the microseconds part as it is not working for python2.5 strptime
2024-11-29 22:54:39 +01:00
return datetime . datetime . strptime ( isodatestr . split ( " . " ) [ 0 ] , " % Y- % m- %d T % H: % M: % S " )
2013-05-23 10:19:43 +02:00
def time2display ( adatetime ) :
return adatetime . strftime ( " % Y- % m- %d % H: % M " )
2024-11-29 22:54:39 +01:00
2013-05-23 10:19:43 +02:00
def hours_minutes ( hours ) :
if hours is None :
return None
else :
2024-11-29 22:54:39 +01:00
return " %02i : %02i " % ( int ( hours ) , int ( ( hours - int ( hours ) ) * 60.0 ) )
2013-05-23 10:19:43 +02:00
def fileisodate ( filename ) :
return datetime . datetime . fromtimestamp ( os . stat ( filename ) . st_mtime ) . isoformat ( )
2024-11-29 22:54:39 +01:00
2013-05-23 10:19:43 +02:00
def dateof ( adatetime ) :
2024-11-29 22:54:39 +01:00
return adatetime . replace ( hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
2013-05-23 10:19:43 +02:00
#####################################
# http://code.activestate.com/recipes/498181-add-thousands-separator-commas-to-formatted-number/
# Code from Michael Robellard's comment made 28 Feb 2010
# Modified for leading +, -, space on 1 Mar 2010 by Glenn Linderman
2018-01-30 12:29:16 +01:00
#
2013-05-23 10:19:43 +02:00
# Tail recursion removed and leading garbage handled on March 12 2010, Alessandro Forghieri
2024-11-29 22:54:39 +01:00
def splitThousands ( s , tSep = " , " , dSep = " . " ) :
""" Splits a general float on thousands. GIGO on general input """
if s is None :
2013-05-23 10:19:43 +02:00
return 0
2024-11-29 22:54:39 +01:00
if not isinstance ( s , str ) :
s = str ( s )
cnt = 0
numChars = dSep + " 0123456789 "
ls = len ( s )
while cnt < ls and s [ cnt ] not in numChars :
cnt + = 1
lhs = s [ 0 : cnt ]
s = s [ cnt : ]
if dSep == " " :
2013-05-23 10:19:43 +02:00
cnt = - 1
else :
2024-11-29 22:54:39 +01:00
cnt = s . rfind ( dSep )
2013-05-23 10:19:43 +02:00
if cnt > 0 :
2024-11-29 22:54:39 +01:00
rhs = dSep + s [ cnt + 1 : ]
s = s [ : cnt ]
2013-05-23 10:19:43 +02:00
else :
2024-11-29 22:54:39 +01:00
rhs = " "
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
splt = " "
while s != " " :
splt = s [ - 3 : ] + tSep + splt
s = s [ : - 3 ]
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
return lhs + splt [ : - 1 ] + rhs
2013-05-23 10:19:43 +02:00
def call_external_process ( shell_string ) :
p = subprocess . call ( shell_string , shell = True )
2024-11-29 22:54:39 +01:00
if p != 0 :
raise Exception ( " shell program exited with error code " + str ( p ) , shell_string )
2013-05-23 10:19:43 +02:00
def check_string ( test_string ) :
2024-11-29 22:54:39 +01:00
pattern = r " [^ \ .A-Za-z0-9 \ -_] "
2013-05-23 10:19:43 +02:00
if re . search ( pattern , test_string ) :
2024-11-29 22:54:39 +01:00
# Character other then . a-z 0-9 was found
print ( ( " Invalid : %r " % ( test_string , ) ) )
2013-05-23 10:19:43 +02:00
def convert_bytes ( bytes ) :
if bytes is None :
return None
2018-01-30 12:29:16 +01:00
else :
2013-05-23 10:19:43 +02:00
bytes = float ( bytes )
if bytes > = 1099511627776 :
terabytes = bytes / 1099511627776
2024-11-29 22:54:39 +01:00
size = " %.2f T " % terabytes
2013-05-23 10:19:43 +02:00
elif bytes > = 1073741824 :
gigabytes = bytes / 1073741824
2024-11-29 22:54:39 +01:00
size = " %.2f G " % gigabytes
2013-05-23 10:19:43 +02:00
elif bytes > = 1048576 :
megabytes = bytes / 1048576
2024-11-29 22:54:39 +01:00
size = " %.2f M " % megabytes
2013-05-23 10:19:43 +02:00
elif bytes > = 1024 :
kilobytes = bytes / 1024
2024-11-29 22:54:39 +01:00
size = " %.2f K " % kilobytes
2013-05-23 10:19:43 +02:00
else :
2024-11-29 22:54:39 +01:00
size = " %.2f b " % bytes
2018-01-30 12:29:16 +01:00
return size
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
2013-05-23 10:19:43 +02:00
## {{{ http://code.activestate.com/recipes/81189/ (r2)
def pp ( cursor , data = None , rowlens = 0 , callback = None ) :
"""
pretty print a query result as a table
callback is a function called for each field ( fieldname , value ) to format the output
"""
2024-11-29 22:54:39 +01:00
def defaultcb ( fieldname , value ) :
2013-05-23 10:19:43 +02:00
return value
if not callback :
callback = defaultcb
d = cursor . description
if not d :
return " #### NO RESULTS ### "
names = [ ]
lengths = [ ]
rules = [ ]
if not data :
data = cursor . fetchall ( )
2024-11-29 22:54:39 +01:00
for dd in d : # iterate over description
2013-05-23 10:19:43 +02:00
l = dd [ 1 ]
if not l :
2024-11-29 22:54:39 +01:00
l = 12 # or default arg ...
l = max ( l , len ( dd [ 0 ] ) ) # handle long names
2013-05-23 10:19:43 +02:00
names . append ( dd [ 0 ] )
lengths . append ( l )
for col in range ( len ( lengths ) ) :
if rowlens :
2024-11-29 22:54:39 +01:00
rls = [ len ( str ( callback ( d [ col ] [ 0 ] , row [ col ] ) ) ) for row in data if row [ col ] ]
lengths [ col ] = max ( [ lengths [ col ] ] + rls )
rules . append ( " - " * lengths [ col ] )
2013-05-23 10:19:43 +02:00
format = " " . join ( [ " %% - %s s " % l for l in lengths ] )
result = [ format % tuple ( names ) ]
result . append ( format % tuple ( rules ) )
for row in data :
2024-11-29 22:54:39 +01:00
row_cb = [ ]
2013-05-23 10:19:43 +02:00
for col in range ( len ( d ) ) :
2024-11-29 22:54:39 +01:00
row_cb . append ( callback ( d [ col ] [ 0 ] , row [ col ] ) )
2013-05-23 10:19:43 +02:00
result . append ( format % tuple ( row_cb ) )
return " \n " . join ( result )
2024-11-29 22:54:39 +01:00
2013-05-23 10:19:43 +02:00
## end of http://code.activestate.com/recipes/81189/ }}}
2024-11-29 22:54:39 +01:00
def html_table ( cur , callback = None ) :
2013-05-23 10:19:43 +02:00
"""
2024-11-29 22:54:39 +01:00
cur est un cursor issu d ' une requete
callback est une fonction qui prend ( rowmap , fieldname , value )
et renvoie une representation texte
2013-05-23 10:19:43 +02:00
"""
2024-11-29 22:54:39 +01:00
2013-05-23 10:19:43 +02:00
def safe_unicode ( iso ) :
if iso is None :
return None
elif isinstance ( iso , str ) :
2024-11-29 22:54:39 +01:00
return iso # .decode()
2013-05-23 10:19:43 +02:00
else :
return iso
def itermap ( cur ) :
for row in cur :
2024-11-29 22:54:39 +01:00
yield dict ( ( cur . description [ idx ] [ 0 ] , value ) for idx , value in enumerate ( row ) )
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
head = " <tr> " + " " . join ( [ " <th> " + c [ 0 ] + " </th> " for c in cur . description ] ) + " </tr> "
lines = " "
2013-05-23 10:19:43 +02:00
if callback :
for r in itermap ( cur ) :
2024-11-29 22:54:39 +01:00
lines = (
lines
+ " <tr> "
+ " " . join ( [ " <td> " + str ( callback ( r , c [ 0 ] , safe_unicode ( r [ c [ 0 ] ] ) ) ) + " </td> " for c in cur . description ] )
+ " </tr> "
)
2013-05-23 10:19:43 +02:00
else :
for r in cur :
2024-11-29 22:54:39 +01:00
lines = lines + " <tr> " + " " . join ( [ " <td> " + safe_unicode ( c ) + " </td> " for c in r ] ) + " </tr> "
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
return " <table border=1 cellpadding=2 cellspacing=0> %s %s </table> " % ( head , lines )
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
def monitor_stdout ( aprocess , onoutputdata , context ) :
2013-05-23 10:19:43 +02:00
""" Reads data from stdout and stderr from aprocess and return as a string
2024-11-29 22:54:39 +01:00
on each chunk , call a call back onoutputdata ( dataread )
2013-05-23 10:19:43 +02:00
"""
2024-11-29 22:54:39 +01:00
assert isinstance ( aprocess , subprocess . Popen )
2013-05-23 10:19:43 +02:00
read_set = [ ]
stdout = [ ]
2024-11-29 22:54:39 +01:00
line = " "
2013-05-23 10:19:43 +02:00
if aprocess . stdout :
read_set . append ( aprocess . stdout )
if aprocess . stderr :
read_set . append ( aprocess . stderr )
while read_set :
try :
rlist , wlist , xlist = select . select ( read_set , [ ] , [ ] )
2022-04-25 10:02:43 +02:00
except select . error as e :
2013-05-23 10:19:43 +02:00
if e . args [ 0 ] == errno . EINTR :
continue
raise
# Reads one line from stdout
if aprocess . stdout in rlist :
data = os . read ( aprocess . stdout . fileno ( ) , 1 )
2024-11-29 22:54:39 +01:00
data = data . decode ( errors = " ignore " )
2013-05-23 10:19:43 +02:00
if data == " " :
aprocess . stdout . close ( )
read_set . remove ( aprocess . stdout )
2024-11-29 22:54:39 +01:00
while data and data not in ( " \n " , " \r " ) :
2013-05-23 10:19:43 +02:00
line + = data
data = os . read ( aprocess . stdout . fileno ( ) , 1 )
2024-11-29 22:54:39 +01:00
data = data . decode ( errors = " ignore " )
if line or data in ( " \n " , " \r " ) :
2013-05-23 10:19:43 +02:00
stdout . append ( line )
if onoutputdata :
2024-11-29 22:54:39 +01:00
onoutputdata ( line , context )
line = " "
2013-05-23 10:19:43 +02:00
# Reads one line from stderr
if aprocess . stderr in rlist :
data = os . read ( aprocess . stderr . fileno ( ) , 1 )
2024-11-29 22:54:39 +01:00
data = data . decode ( errors = " ignore " )
2013-05-23 10:19:43 +02:00
if data == " " :
aprocess . stderr . close ( )
read_set . remove ( aprocess . stderr )
2024-11-29 22:54:39 +01:00
while data and data not in ( " \n " , " \r " ) :
2013-05-23 10:19:43 +02:00
line + = data
data = os . read ( aprocess . stderr . fileno ( ) , 1 )
2024-11-29 22:54:39 +01:00
data = data . decode ( errors = " ignore " )
if line or data in ( " \n " , " \r " ) :
2013-05-23 10:19:43 +02:00
stdout . append ( line )
if onoutputdata :
2024-11-29 22:54:39 +01:00
onoutputdata ( line , context )
line = " "
2013-05-23 10:19:43 +02:00
aprocess . wait ( )
if line :
stdout . append ( line )
if onoutputdata :
2024-11-29 22:54:39 +01:00
onoutputdata ( line , context )
2013-05-23 10:19:43 +02:00
return " \n " . join ( stdout )
2014-11-20 15:59:29 +01:00
def str2bool ( val ) :
2024-11-29 22:54:39 +01:00
if not isinstance ( type ( val ) , bool ) :
2014-11-20 15:59:29 +01:00
return val . lower ( ) in ( " yes " , " true " , " t " , " 1 " )
2013-05-23 10:19:43 +02:00
class BackupStat :
2024-11-29 22:54:39 +01:00
dbpath = " "
2013-05-23 10:19:43 +02:00
db = None
2024-11-29 22:54:39 +01:00
logger = logging . getLogger ( " tisbackup " )
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
def __init__ ( self , dbpath ) :
2013-05-23 10:19:43 +02:00
self . dbpath = dbpath
if not os . path . isfile ( self . dbpath ) :
2024-11-29 22:54:39 +01:00
self . db = sqlite3 . connect ( self . dbpath )
2013-05-23 10:19:43 +02:00
self . initdb ( )
else :
2024-11-29 22:54:39 +01:00
self . db = sqlite3 . connect ( self . dbpath , check_same_thread = False )
if " ' TYPE ' " not in str ( self . db . execute ( " select * from stats " ) . description ) :
2013-05-23 10:19:43 +02:00
self . updatedb ( )
2018-01-30 12:29:16 +01:00
2013-05-23 10:19:43 +02:00
def updatedb ( self ) :
2024-11-29 22:54:39 +01:00
self . logger . debug ( " Update stat database " )
2013-05-23 10:19:43 +02:00
self . db . execute ( " alter table stats add column TYPE TEXT; " )
self . db . execute ( " update stats set TYPE= ' BACKUP ' ; " )
self . db . commit ( )
2018-01-30 12:29:16 +01:00
2013-05-23 10:19:43 +02:00
def initdb ( self ) :
2024-11-29 22:54:39 +01:00
assert isinstance ( self . db , sqlite3 . Connection )
self . logger . debug ( " Initialize stat database " )
2013-05-23 10:19:43 +02:00
self . db . execute ( """
create table stats (
backup_name TEXT ,
server_name TEXT ,
description TEXT ,
backup_start TEXT ,
backup_end TEXT ,
backup_duration NUMERIC ,
total_files_count INT ,
written_files_count INT ,
total_bytes INT ,
written_bytes INT ,
status TEXT ,
log TEXT ,
backup_location TEXT ,
TYPE TEXT ) """ )
self . db . execute ( """
create index idx_stats_backup_name on stats ( backup_name ) ; """ )
self . db . execute ( """
create index idx_stats_backup_location on stats ( backup_location ) ; """ )
2014-07-25 15:06:51 +02:00
self . db . execute ( """
CREATE INDEX idx_stats_backup_name_start on stats ( backup_name , backup_start ) ; """ )
2018-01-30 12:29:16 +01:00
self . db . commit ( )
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
def start ( self , backup_name , server_name , TYPE , description = " " , backup_location = None ) :
""" Add in stat DB a record for the newly running backup """
return self . add (
backup_name = backup_name ,
server_name = server_name ,
description = description ,
backup_start = datetime2isodate ( ) ,
status = " Running " ,
TYPE = TYPE ,
)
def finish (
self ,
rowid ,
total_files_count = None ,
written_files_count = None ,
total_bytes = None ,
written_bytes = None ,
log = None ,
status = " OK " ,
backup_end = None ,
backup_duration = None ,
backup_location = None ,
) :
""" Update record in stat DB for the finished backup """
2013-05-23 10:19:43 +02:00
if not backup_end :
backup_end = datetime2isodate ( )
2024-11-29 22:54:39 +01:00
if backup_duration is None :
2013-05-23 10:19:43 +02:00
try :
# get duration using start of backup datetime
2024-11-29 22:54:39 +01:00
backup_duration = (
isodate2datetime ( backup_end )
- isodate2datetime ( self . query ( " select backup_start from stats where rowid=? " , ( rowid , ) ) [ 0 ] [ " backup_start " ] )
) . seconds / 3600.0
2013-05-23 10:19:43 +02:00
except :
backup_duration = None
# update stat record
2024-11-29 22:54:39 +01:00
self . db . execute (
""" \
2018-01-30 12:29:16 +01:00
update stats set
2013-05-23 10:19:43 +02:00
total_files_count = ? , written_files_count = ? , total_bytes = ? , written_bytes = ? , log = ? , status = ? , backup_end = ? , backup_duration = ? , backup_location = ?
where
rowid = ?
2024-11-29 22:54:39 +01:00
""" ,
(
total_files_count ,
written_files_count ,
total_bytes ,
written_bytes ,
log ,
status ,
backup_end ,
backup_duration ,
backup_location ,
rowid ,
) ,
)
2013-05-23 10:19:43 +02:00
self . db . commit ( )
2024-11-29 22:54:39 +01:00
def add (
self ,
backup_name = " " ,
server_name = " " ,
description = " " ,
backup_start = None ,
backup_end = None ,
backup_duration = None ,
total_files_count = None ,
written_files_count = None ,
total_bytes = None ,
written_bytes = None ,
status = " draft " ,
log = " " ,
TYPE = " " ,
backup_location = None ,
) :
2013-05-23 10:19:43 +02:00
if not backup_start :
2024-11-29 22:54:39 +01:00
backup_start = datetime2isodate ( )
2013-05-23 10:19:43 +02:00
if not backup_end :
2024-11-29 22:54:39 +01:00
backup_end = datetime2isodate ( )
2018-01-30 12:29:16 +01:00
2024-11-29 22:54:39 +01:00
cur = self . db . execute (
""" \
2013-05-23 10:19:43 +02:00
insert into stats (
backup_name ,
server_name ,
description ,
backup_start ,
backup_end ,
backup_duration ,
total_files_count ,
written_files_count ,
total_bytes ,
written_bytes ,
status ,
log ,
backup_location ,
TYPE ) values ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )
2024-11-29 22:54:39 +01:00
""" ,
(
backup_name ,
server_name ,
description ,
backup_start ,
backup_end ,
backup_duration ,
total_files_count ,
written_files_count ,
total_bytes ,
written_bytes ,
status ,
log ,
backup_location ,
TYPE ,
) ,
)
2013-05-23 10:19:43 +02:00
self . db . commit ( )
return cur . lastrowid
2024-11-29 22:54:39 +01:00
def query ( self , query , args = ( ) , one = False ) :
2013-05-23 10:19:43 +02:00
"""
execute la requete query sur la db et renvoie un tableau de dictionnaires
"""
cur = self . db . execute ( query , args )
2024-11-29 22:54:39 +01:00
rv = [ dict ( ( cur . description [ idx ] [ 0 ] , value ) for idx , value in enumerate ( row ) ) for row in cur . fetchall ( ) ]
2013-05-23 10:19:43 +02:00
return ( rv [ 0 ] if rv else None ) if one else rv
2024-11-29 22:54:39 +01:00
def last_backups ( self , backup_name , count = 30 ) :
2013-05-23 10:19:43 +02:00
if backup_name :
2024-11-29 22:54:39 +01:00
cur = self . db . execute ( " select * from stats where backup_name=? order by backup_end desc limit ? " , ( backup_name , count ) )
2013-05-23 10:19:43 +02:00
else :
2024-11-29 22:54:39 +01:00
cur = self . db . execute ( " select * from stats order by backup_end desc limit ? " , ( count , ) )
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
def fcb ( fieldname , value ) :
if fieldname in ( " backup_start " , " backup_end " ) :
2013-05-23 10:19:43 +02:00
return time2display ( isodate2datetime ( value ) )
2024-11-29 22:54:39 +01:00
elif " bytes " in fieldname :
2013-05-23 10:19:43 +02:00
return convert_bytes ( value )
2024-11-29 22:54:39 +01:00
elif " count " in fieldname :
return splitThousands ( value , " " , " . " )
elif " backup_duration " in fieldname :
2013-05-23 10:19:43 +02:00
return hours_minutes ( value )
else :
return value
2024-11-29 22:54:39 +01:00
# for r in self.query('select * from stats where backup_name=? order by backup_end desc limit ?',(backup_name,count)):
print ( ( pp ( cur , None , 1 , fcb ) ) )
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
def fcb ( self , fields , fieldname , value ) :
if fieldname in ( " backup_start " , " backup_end " ) :
2013-05-23 10:19:43 +02:00
return time2display ( isodate2datetime ( value ) )
2024-11-29 22:54:39 +01:00
elif " bytes " in fieldname :
2013-05-23 10:19:43 +02:00
return convert_bytes ( value )
2024-11-29 22:54:39 +01:00
elif " count " in fieldname :
return splitThousands ( value , " " , " . " )
elif " backup_duration " in fieldname :
2013-05-23 10:19:43 +02:00
return hours_minutes ( value )
else :
return value
2024-11-29 22:54:39 +01:00
def as_html ( self , cur ) :
2013-05-23 10:19:43 +02:00
if cur :
2024-11-29 22:54:39 +01:00
return html_table ( cur , self . fcb )
2013-05-23 10:19:43 +02:00
else :
2024-11-29 22:54:39 +01:00
return html_table ( self . db . execute ( " select * from stats order by backup_start asc " ) , self . fcb )
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
def ssh_exec ( command , ssh = None , server_name = " " , remote_user = " " , private_key = " " , ssh_port = 22 ) :
2018-01-30 12:29:16 +01:00
""" execute command on server_name using the provided ssh connection
2024-11-29 22:54:39 +01:00
or creates a new connection if ssh is not provided .
returns ( exit_code , output )
2013-05-23 10:19:43 +02:00
2024-11-29 22:54:39 +01:00
output is the concatenation of stdout and stderr
2013-05-23 10:19:43 +02:00
"""
if not ssh :
2024-11-29 22:54:39 +01:00
assert server_name and remote_user and private_key
2013-05-23 10:19:43 +02:00
try :
mykey = paramiko . RSAKey . from_private_key_file ( private_key )
except paramiko . SSHException :
2024-11-29 22:54:39 +01:00
# mykey = paramiko.DSSKey.from_private_key_file(private_key)
mykey = paramiko . Ed25519Key . from_private_key_file ( private_key )
2013-05-23 10:19:43 +02:00
ssh = paramiko . SSHClient ( )
ssh . set_missing_host_key_policy ( paramiko . AutoAddPolicy ( ) )
2024-11-29 22:54:39 +01:00
ssh . connect ( server_name , username = remote_user , pkey = mykey , port = 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 ( )
chan . exec_command ( command )
stdout . flush ( )
2022-04-25 10:02:43 +02:00
output_base = stdout . read ( )
2024-11-29 22:54:39 +01:00
output = output_base . decode ( errors = " ignore " ) . replace ( " ' " , " " )
2013-05-23 10:19:43 +02:00
exit_code = chan . recv_exit_status ( )
2024-11-29 22:54:39 +01:00
return ( exit_code , output )
2013-05-23 10:19:43 +02: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 ) :
if not re . match ( " ^[A-Za-z0-9_ \ - \ .]*$ " , backup_name ) :
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 )
2013-05-23 10:19:43 +02:00
try :
mykey = paramiko . RSAKey . from_private_key_file ( self . private_key )
except paramiko . SSHException :
mykey = paramiko . DSSKey . from_private_key_file ( self . private_key )
ssh = paramiko . SSHClient ( )
ssh . set_missing_host_key_policy ( paramiko . AutoAddPolicy ( ) )
2024-11-29 22:54:39 +01:00
ssh . connect ( self . server_name , username = self . remote_user , pkey = mykey )
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 )
2013-05-23 10:19:43 +02:00
try :
mykey = paramiko . RSAKey . from_private_key_file ( self . private_key )
except paramiko . SSHException :
mykey = paramiko . DSSKey . from_private_key_file ( self . private_key )
ssh = paramiko . SSHClient ( )
ssh . set_missing_host_key_policy ( paramiko . AutoAddPolicy ( ) )
2024-11-29 22:54:39 +01:00
ssh . connect ( self . server_name , username = self . remote_user , pkey = mykey )
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
2024-11-29 22:54:39 +01:00
if __name__ == " __main__ " :
logger = logging . getLogger ( " tisbackup " )
2013-05-23 10:19:43 +02:00
logger . setLevel ( logging . DEBUG )
2024-11-29 22:54:39 +01:00
formatter = logging . Formatter ( " %(asctime)s %(levelname)s %(message)s " )
2013-05-23 10:19:43 +02:00
handler = logging . StreamHandler ( )
handler . setFormatter ( formatter )
logger . addHandler ( handler )
2024-11-29 22:54:39 +01:00
dbstat = BackupStat ( " /backup/data/log/tisbackup.sqlite " )