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 ):
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 )
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" )