Fist commit
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
#============================================================================
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of version 2.1 of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#============================================================================
|
||||
# Copyright (C) 2006-2007 XenSource Inc.
|
||||
#============================================================================
|
||||
#
|
||||
# Parts of this file are based upon xmlrpclib.py, the XML-RPC client
|
||||
# interface included in the Python distribution.
|
||||
#
|
||||
# Copyright (c) 1999-2002 by Secret Labs AB
|
||||
# Copyright (c) 1999-2002 by Fredrik Lundh
|
||||
#
|
||||
# By obtaining, using, and/or copying this software and/or its
|
||||
# associated documentation, you agree that you have read, understood,
|
||||
# and will comply with the following terms and conditions:
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its associated documentation for any purpose and without fee is
|
||||
# hereby granted, provided that the above copyright notice appears in
|
||||
# all copies, and that both that copyright notice and this permission
|
||||
# notice appear in supporting documentation, and that the name of
|
||||
# Secret Labs AB or the author not be used in advertising or publicity
|
||||
# pertaining to distribution of the software without specific, written
|
||||
# prior permission.
|
||||
#
|
||||
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
|
||||
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
|
||||
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
|
||||
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
||||
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||
# OF THIS SOFTWARE.
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
import gettext
|
||||
import xmlrpclib
|
||||
import httplib
|
||||
import socket
|
||||
|
||||
translation = gettext.translation('xen-xm', fallback = True)
|
||||
|
||||
API_VERSION_1_1 = '1.1'
|
||||
API_VERSION_1_2 = '1.2'
|
||||
|
||||
#
|
||||
# Methods that have different parameters between API versions 1.1 and 1.2, and
|
||||
# the number of parameters in 1.1.
|
||||
#
|
||||
COMPATIBILITY_METHODS_1_1 = [
|
||||
('SR.create' , 8),
|
||||
('SR.introduce' , 6),
|
||||
('SR.make' , 7),
|
||||
('VDI.snapshot' , 1),
|
||||
('VDI.clone' , 1),
|
||||
]
|
||||
|
||||
class Failure(Exception):
|
||||
def __init__(self, details):
|
||||
self.details = details
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
return str(self.details)
|
||||
except Exception, exn:
|
||||
import sys
|
||||
print >>sys.stderr, exn
|
||||
return "Xen-API failure: %s" % str(self.details)
|
||||
|
||||
def _details_map(self):
|
||||
return dict([(str(i), self.details[i])
|
||||
for i in range(len(self.details))])
|
||||
|
||||
|
||||
_RECONNECT_AND_RETRY = (lambda _ : ())
|
||||
|
||||
class UDSHTTPConnection(httplib.HTTPConnection):
|
||||
"""HTTPConnection subclass to allow HTTP over Unix domain sockets. """
|
||||
def connect(self):
|
||||
path = self.host.replace("_", "/")
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.sock.connect(path)
|
||||
|
||||
class UDSHTTP(httplib.HTTP):
|
||||
_connection_class = UDSHTTPConnection
|
||||
|
||||
class UDSTransport(xmlrpclib.Transport):
|
||||
def make_connection(self, host):
|
||||
return UDSHTTP(host)
|
||||
|
||||
class Session(xmlrpclib.ServerProxy):
|
||||
"""A server proxy and session manager for communicating with xapi using
|
||||
the Xen-API.
|
||||
|
||||
Example:
|
||||
|
||||
session = Session('http://localhost/')
|
||||
session.login_with_password('me', 'mypassword')
|
||||
session.xenapi.VM.start(vm_uuid)
|
||||
session.xenapi.session.logout()
|
||||
"""
|
||||
|
||||
def __init__(self, uri, transport=None, encoding=None, verbose=0,
|
||||
allow_none=1):
|
||||
xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding,
|
||||
verbose, allow_none)
|
||||
self._session = None
|
||||
self.last_login_method = None
|
||||
self.last_login_params = None
|
||||
self.API_version = API_VERSION_1_1
|
||||
|
||||
|
||||
def xenapi_request(self, methodname, params):
|
||||
if methodname.startswith('login'):
|
||||
self._login(methodname, params)
|
||||
return None
|
||||
elif methodname == 'logout':
|
||||
self._logout()
|
||||
return None
|
||||
else:
|
||||
retry_count = 0
|
||||
while retry_count < 3:
|
||||
full_params = (self._session,) + params
|
||||
result = _parse_result(getattr(self, methodname)(*full_params))
|
||||
if result == _RECONNECT_AND_RETRY:
|
||||
retry_count += 1
|
||||
if self.last_login_method:
|
||||
self._login(self.last_login_method,
|
||||
self.last_login_params)
|
||||
else:
|
||||
raise xmlrpclib.Fault(401, 'You must log in')
|
||||
else:
|
||||
return result
|
||||
raise xmlrpclib.Fault(
|
||||
500, 'Tried 3 times to get a valid session, but failed')
|
||||
|
||||
|
||||
def _login(self, method, params):
|
||||
result = _parse_result(getattr(self, 'session.%s' % method)(*params))
|
||||
if result == _RECONNECT_AND_RETRY:
|
||||
raise xmlrpclib.Fault(
|
||||
500, 'Received SESSION_INVALID when logging in')
|
||||
self._session = result
|
||||
self.last_login_method = method
|
||||
self.last_login_params = params
|
||||
if method.startswith("slave_local"):
|
||||
self.API_version = API_VERSION_1_2
|
||||
else:
|
||||
self.API_version = self._get_api_version()
|
||||
|
||||
def logout(self):
|
||||
try:
|
||||
if self.last_login_method.startswith("slave_local"):
|
||||
return _parse_result(self.session.local_logout(self._session))
|
||||
else:
|
||||
return _parse_result(self.session.logout(self._session))
|
||||
finally:
|
||||
self._session = None
|
||||
self.last_login_method = None
|
||||
self.last_login_params = None
|
||||
self.API_version = API_VERSION_1_1
|
||||
|
||||
def _get_api_version(self):
|
||||
pool = self.xenapi.pool.get_all()[0]
|
||||
host = self.xenapi.pool.get_master(pool)
|
||||
if (self.xenapi.host.get_API_version_major(host) == "1" and
|
||||
self.xenapi.host.get_API_version_minor(host) == "2"):
|
||||
return API_VERSION_1_2
|
||||
else:
|
||||
return API_VERSION_1_1
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name == 'handle':
|
||||
return self._session
|
||||
elif name == 'xenapi':
|
||||
return _Dispatcher(self.API_version, self.xenapi_request, None)
|
||||
elif name.startswith('login') or name.startswith('slave_local'):
|
||||
return lambda *params: self._login(name, params)
|
||||
else:
|
||||
return xmlrpclib.ServerProxy.__getattr__(self, name)
|
||||
|
||||
def xapi_local():
|
||||
return Session("http://_var_xapi_xapi/", transport=UDSTransport())
|
||||
|
||||
def _parse_result(result):
|
||||
if type(result) != dict or 'Status' not in result:
|
||||
raise xmlrpclib.Fault(500, 'Missing Status in response from server' + result)
|
||||
if result['Status'] == 'Success':
|
||||
if 'Value' in result:
|
||||
return result['Value']
|
||||
else:
|
||||
raise xmlrpclib.Fault(500,
|
||||
'Missing Value in response from server')
|
||||
else:
|
||||
if 'ErrorDescription' in result:
|
||||
if result['ErrorDescription'][0] == 'SESSION_INVALID':
|
||||
return _RECONNECT_AND_RETRY
|
||||
else:
|
||||
raise Failure(result['ErrorDescription'])
|
||||
else:
|
||||
raise xmlrpclib.Fault(
|
||||
500, 'Missing ErrorDescription in response from server')
|
||||
|
||||
|
||||
# Based upon _Method from xmlrpclib.
|
||||
class _Dispatcher:
|
||||
def __init__(self, API_version, send, name):
|
||||
self.__API_version = API_version
|
||||
self.__send = send
|
||||
self.__name = name
|
||||
|
||||
def __repr__(self):
|
||||
if self.__name:
|
||||
return '<XenAPI._Dispatcher for %s>' % self.__name
|
||||
else:
|
||||
return '<XenAPI._Dispatcher>'
|
||||
|
||||
def __getattr__(self, name):
|
||||
if self.__name is None:
|
||||
return _Dispatcher(self.__API_version, self.__send, name)
|
||||
else:
|
||||
return _Dispatcher(self.__API_version, self.__send, "%s.%s" % (self.__name, name))
|
||||
|
||||
def __call__(self, *args):
|
||||
if self.__API_version == API_VERSION_1_1:
|
||||
for m in COMPATIBILITY_METHODS_1_1:
|
||||
if self.__name == m[0]:
|
||||
return self.__send(self.__name, args[0:m[1]])
|
||||
|
||||
return self.__send(self.__name, args)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# -----------------------------------------------------------------------
|
||||
# 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/>.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/python
|
||||
# -*- 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/>.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
import sys
|
||||
try:
|
||||
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko
|
||||
import paramiko
|
||||
except ImportError,e:
|
||||
print "Error : can not load paramiko library %s" % e
|
||||
raise
|
||||
|
||||
sys.stderr = sys.__stderr__
|
||||
|
||||
import datetime
|
||||
import base64
|
||||
import os
|
||||
from common import *
|
||||
|
||||
class backup_mysql(backup_generic):
|
||||
"""Backup a mysql database as gzipped sql file through ssh"""
|
||||
type = 'mysql+ssh'
|
||||
required_params = backup_generic.required_params + ['db_name','db_user','db_passwd','private_key']
|
||||
db_name=''
|
||||
db_user=''
|
||||
db_passwd=''
|
||||
|
||||
def do_backup(self,stats):
|
||||
|
||||
self.logger.debug('[%s] Connecting to %s with user root and key %s',self.backup_name,self.server_name,self.private_key)
|
||||
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())
|
||||
ssh.connect(self.server_name,username='root',pkey = mykey, port=self.ssh_port)
|
||||
|
||||
t = datetime.datetime.now()
|
||||
backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S')
|
||||
|
||||
# dump db
|
||||
stats['status']='Dumping'
|
||||
cmd = 'mysqldump -u' + self.db_user +' -p' + self.db_passwd + ' ' + self.db_name + ' > /tmp/' + self.db_name + '-' + backup_start_date + '.sql'
|
||||
self.logger.debug('[%s] Dump DB : %s',self.backup_name,cmd)
|
||||
if not self.dry_run:
|
||||
(error_code,output) = ssh_exec(cmd,ssh=ssh)
|
||||
self.logger.debug("[%s] Output of %s :\n%s",self.backup_name,cmd,output)
|
||||
if error_code:
|
||||
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code,cmd))
|
||||
|
||||
# zip the file
|
||||
stats['status']='Zipping'
|
||||
cmd = 'gzip /tmp/' + self.db_name + '-' + backup_start_date + '.sql'
|
||||
self.logger.debug('[%s] Compress backup : %s',self.backup_name,cmd)
|
||||
if not self.dry_run:
|
||||
(error_code,output) = ssh_exec(cmd,ssh=ssh)
|
||||
self.logger.debug("[%s] Output of %s :\n%s",self.backup_name,cmd,output)
|
||||
if error_code:
|
||||
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code,cmd))
|
||||
|
||||
# get the file
|
||||
stats['status']='SFTP'
|
||||
filepath = '/tmp/' + self.db_name + '-' + backup_start_date + '.sql.gz'
|
||||
localpath = os.path.join(self.backup_dir , self.db_name + '-' + backup_start_date + '.sql.gz')
|
||||
self.logger.debug('[%s] Get gz backup with sftp on %s from %s to %s',self.backup_name,self.server_name,filepath,localpath)
|
||||
if not self.dry_run:
|
||||
transport = ssh.get_transport()
|
||||
sftp = paramiko.SFTPClient.from_transport(transport)
|
||||
sftp.get(filepath, localpath)
|
||||
sftp.close()
|
||||
|
||||
if not self.dry_run:
|
||||
stats['total_files_count']=1
|
||||
stats['written_files_count']=1
|
||||
stats['total_bytes']=os.stat(localpath).st_size
|
||||
stats['written_bytes']=os.stat(localpath).st_size
|
||||
stats['log']='gzip dump of DB %s:%s (%d bytes) to %s' % (self.server_name,self.db_name, stats['written_bytes'], localpath)
|
||||
stats['backup_location'] = localpath
|
||||
|
||||
stats['status']='RMTemp'
|
||||
cmd = 'rm -f /tmp/' + self.db_name + '-' + backup_start_date + '.sql.gz'
|
||||
self.logger.debug('[%s] Remove temp gzip : %s',self.backup_name,cmd)
|
||||
if not self.dry_run:
|
||||
(error_code,output) = ssh_exec(cmd,ssh=ssh)
|
||||
self.logger.debug("[%s] Output of %s :\n%s",self.backup_name,cmd,output)
|
||||
if error_code:
|
||||
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code,cmd))
|
||||
stats['status']='OK'
|
||||
|
||||
def register_existingbackups(self):
|
||||
"""scan backup dir 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,))]
|
||||
|
||||
filelist = os.listdir(self.backup_dir)
|
||||
filelist.sort()
|
||||
p = re.compile('^%s-(?P<date>\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}).sql.gz$' % self.db_name)
|
||||
for item in filelist:
|
||||
sr = p.match(item)
|
||||
if sr:
|
||||
file_name = os.path.join(self.backup_dir,item)
|
||||
start = datetime.datetime.strptime(sr.groups()[0],'%Y%m%d-%Hh%Mm%S').isoformat()
|
||||
if not file_name in registered:
|
||||
self.logger.info('Registering %s from %s',file_name,fileisodate(file_name))
|
||||
size_bytes = int(os.popen('du -sb "%s"' % file_name).read().split('\t')[0])
|
||||
self.logger.debug(' Size in bytes : %i',size_bytes)
|
||||
if not self.dry_run:
|
||||
self.dbstat.add(self.backup_name,self.server_name,'',\
|
||||
backup_start=start,backup_end=fileisodate(file_name),status='OK',total_bytes=size_bytes,backup_location=file_name)
|
||||
else:
|
||||
self.logger.info('Skipping %s from %s, already registered',file_name,fileisodate(file_name))
|
||||
|
||||
register_driver(backup_mysql)
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/python
|
||||
# -*- 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/>.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import datetime
|
||||
from common import *
|
||||
|
||||
|
||||
class backup_null(backup_generic):
|
||||
"""Null backup to register servers which don't need any backups
|
||||
but we still want to know they are taken in account"""
|
||||
type = 'null'
|
||||
|
||||
required_params = ['type','server_name','backup_name']
|
||||
optional_params = []
|
||||
|
||||
def do_backup(self,stats):
|
||||
pass
|
||||
def process_backup(self):
|
||||
pass
|
||||
def cleanup_backup(self):
|
||||
pass
|
||||
def export_latestbackup(self,destdir):
|
||||
return {}
|
||||
def checknagios(self,maxage_hours=30):
|
||||
return (nagiosStateOk,"No backups needs to be performed")
|
||||
|
||||
register_driver(backup_null)
|
||||
|
||||
if __name__=='__main__':
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/python
|
||||
# -*- 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/>.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
import sys
|
||||
try:
|
||||
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko
|
||||
import paramiko
|
||||
except ImportError,e:
|
||||
print "Error : can not load paramiko library %s" % e
|
||||
raise
|
||||
|
||||
sys.stderr = sys.__stderr__
|
||||
|
||||
import datetime
|
||||
import base64
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
from common import *
|
||||
|
||||
class backup_pgsql(backup_generic):
|
||||
"""Backup a postgresql database as gzipped sql file through ssh"""
|
||||
type = 'pgsql+ssh'
|
||||
required_params = backup_generic.required_params + ['db_name','private_key']
|
||||
db_name=''
|
||||
|
||||
def do_backup(self,stats):
|
||||
try:
|
||||
mykey = paramiko.RSAKey.from_private_key_file(self.private_key)
|
||||
except paramiko.SSHException:
|
||||
mykey = paramiko.DSSKey.from_private_key_file(self.private_key)
|
||||
|
||||
self.logger.debug('[%s] Trying to connect to "%s" with username root and key "%s"',self.backup_name,self.server_name,self.private_key)
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(self.server_name,username='root',pkey = mykey,port=self.ssh_port)
|
||||
|
||||
t = datetime.datetime.now()
|
||||
backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S')
|
||||
|
||||
# dump db
|
||||
cmd = 'sudo -u postgres pg_dump ' + self.db_name + ' > /tmp/' + self.db_name + '-' + backup_start_date + '.sql'
|
||||
self.logger.debug('[%s] %s ',self.backup_name,cmd)
|
||||
if not self.dry_run:
|
||||
(error_code,output) = ssh_exec(cmd,ssh=ssh)
|
||||
self.logger.debug("[%s] Output of %s :\n%s",self.backup_name,cmd,output)
|
||||
if error_code:
|
||||
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code,cmd))
|
||||
|
||||
# zip the file
|
||||
cmd = 'gzip /tmp/' + self.db_name + '-' + backup_start_date + '.sql'
|
||||
self.logger.debug('[%s] %s ',self.backup_name,cmd)
|
||||
if not self.dry_run:
|
||||
(error_code,output) = ssh_exec(cmd,ssh=ssh)
|
||||
self.logger.debug("[%s] Output of %s :\n%s",self.backup_name,cmd,output)
|
||||
if error_code:
|
||||
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code,cmd))
|
||||
|
||||
# get the file
|
||||
filepath = '/tmp/' + self.db_name + '-' + backup_start_date + '.sql.gz'
|
||||
localpath = self.backup_dir + '/' + self.db_name + '-' + backup_start_date + '.sql.gz'
|
||||
self.logger.debug('[%s] get the file using sftp from "%s" to "%s" ',self.backup_name,filepath,localpath)
|
||||
if not self.dry_run:
|
||||
transport = ssh.get_transport()
|
||||
sftp = paramiko.SFTPClient.from_transport(transport)
|
||||
sftp.get(filepath, localpath)
|
||||
sftp.close()
|
||||
|
||||
if not self.dry_run:
|
||||
stats['total_files_count']=1
|
||||
stats['written_files_count']=1
|
||||
stats['total_bytes']=os.stat(localpath).st_size
|
||||
stats['written_bytes']=os.stat(localpath).st_size
|
||||
stats['log']='gzip dump of DB %s:%s (%d bytes) to %s' % (self.server_name,self.db_name, stats['written_bytes'], localpath)
|
||||
|
||||
stats['backup_location'] = localpath
|
||||
|
||||
cmd = 'rm -f /tmp/' + self.db_name + '-' + backup_start_date + '.sql.gz'
|
||||
self.logger.debug('[%s] %s ',self.backup_name,cmd)
|
||||
if not self.dry_run:
|
||||
(error_code,output) = ssh_exec(cmd,ssh=ssh)
|
||||
self.logger.debug("[%s] Output of %s :\n%s",self.backup_name,cmd,output)
|
||||
if error_code:
|
||||
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code,cmd))
|
||||
|
||||
stats['status']='OK'
|
||||
|
||||
def register_existingbackups(self):
|
||||
"""scan backup dir 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,))]
|
||||
|
||||
filelist = os.listdir(self.backup_dir)
|
||||
filelist.sort()
|
||||
p = re.compile('^%s-(?P<date>\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}).sql.gz$' % self.db_name)
|
||||
for item in filelist:
|
||||
sr = p.match(item)
|
||||
if sr:
|
||||
file_name = os.path.join(self.backup_dir,item)
|
||||
start = datetime.datetime.strptime(sr.groups()[0],'%Y%m%d-%Hh%Mm%S').isoformat()
|
||||
if not file_name in registered:
|
||||
self.logger.info('Registering %s from %s',file_name,fileisodate(file_name))
|
||||
size_bytes = int(os.popen('du -sb "%s"' % file_name).read().split('\t')[0])
|
||||
self.logger.debug(' Size in bytes : %i',size_bytes)
|
||||
if not self.dry_run:
|
||||
self.dbstat.add(self.backup_name,self.server_name,'',\
|
||||
backup_start=start,backup_end=fileisodate(file_name),status='OK',total_bytes=size_bytes,backup_location=file_name)
|
||||
else:
|
||||
self.logger.info('Skipping %s from %s, already registered',file_name,fileisodate(file_name))
|
||||
|
||||
register_driver(backup_pgsql)
|
||||
@@ -0,0 +1,127 @@
|
||||
# -----------------------------------------------------------------------
|
||||
# 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/>.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
import os
|
||||
import datetime
|
||||
from common import *
|
||||
import time
|
||||
|
||||
class backup_rdiff:
|
||||
backup_dir=''
|
||||
backup_start_date=None
|
||||
backup_name=''
|
||||
server_name=''
|
||||
exclude_list=''
|
||||
ssh_port='22'
|
||||
remote_user='root'
|
||||
remote_dir=''
|
||||
dest_dir=''
|
||||
verbose = False
|
||||
dry_run=False
|
||||
|
||||
|
||||
|
||||
def __init__(self, backup_name, backup_base_dir):
|
||||
self.backup_dir = backup_base_dir + '/' + backup_name
|
||||
|
||||
if os.path.isdir(self.backup_dir )==False:
|
||||
os.makedirs(self.backup_dir)
|
||||
|
||||
self.backup_name = backup_name
|
||||
t = datetime.datetime.now()
|
||||
self.backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S')
|
||||
|
||||
def get_latest_backup(self):
|
||||
filelist = os.listdir(self.backup_dir)
|
||||
if len(filelist) == 0:
|
||||
return ''
|
||||
|
||||
filelist.sort()
|
||||
|
||||
return filelist[-1]
|
||||
|
||||
def cleanup_backup(self):
|
||||
filelist = os.listdir(self.backup_dir)
|
||||
if len(filelist) == 0:
|
||||
return ''
|
||||
|
||||
filelist.sort()
|
||||
for backup_date in filelist:
|
||||
today = time.time()
|
||||
print backup_date
|
||||
datestring = backup_date[0:8]
|
||||
c = time.strptime(datestring,"%Y%m%d")
|
||||
# TODO: improve
|
||||
if today - c < 60 * 60 * 24* 30:
|
||||
print time.strftime("%Y%m%d",c) + " is to be deleted"
|
||||
|
||||
|
||||
def copy_latest_to_new(self):
|
||||
# TODO check that latest exist
|
||||
# TODO check that new does not exist
|
||||
|
||||
|
||||
last_backup = self.get_latest_backup()
|
||||
if last_backup=='':
|
||||
print "*********************************"
|
||||
print "*first backup for " + self.backup_name
|
||||
else:
|
||||
latest_backup_path = self.backup_dir + '/' + last_backup
|
||||
new_backup_path = self.backup_dir + '/' + self.backup_start_date
|
||||
print "#cp -al starting"
|
||||
cmd = 'cp -al ' + latest_backup_path + ' ' + new_backup_path
|
||||
print cmd
|
||||
if self.dry_run==False:
|
||||
call_external_process(cmd)
|
||||
print "#cp -al finished"
|
||||
|
||||
|
||||
def rsync_to_new(self):
|
||||
|
||||
self.dest_dir = self.backup_dir + '/' + self.backup_start_date + '/'
|
||||
src_server = self.remote_user + '@' + self.server_name + ':"' + self.remote_dir.strip() + '/"'
|
||||
|
||||
print "#starting rsync"
|
||||
verbose_arg=""
|
||||
if self.verbose==True:
|
||||
verbose_arg = "-P "
|
||||
|
||||
cmd = "rdiff-backup " + verbose_arg + ' --compress-level=9 --numeric-ids -az --partial -e "ssh -o StrictHostKeyChecking=no -c Blowfish -p ' + self.ssh_port + ' -i ' + self.private_key + '" --stats --delete-after ' + self.exclude_list + ' ' + src_server + ' ' + self.dest_dir
|
||||
print cmd
|
||||
|
||||
## deal with exit code 24 (file vanished)
|
||||
if self.dry_run==False:
|
||||
p = subprocess.call(cmd, shell=True)
|
||||
if (p ==24):
|
||||
print "Note: some files vanished before transfer"
|
||||
if (p != 0 and p != 24 ):
|
||||
raise Exception('shell program exited with error code ' + str(p), cmd)
|
||||
|
||||
|
||||
print "#finished rsync"
|
||||
|
||||
def process_backup(self):
|
||||
print ""
|
||||
print "#========Starting backup item ========="
|
||||
self.copy_latest_to_new()
|
||||
|
||||
self.rsync_to_new()
|
||||
print "#========Backup item finished=========="
|
||||
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/python
|
||||
# -*- 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/>.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import datetime
|
||||
from common import *
|
||||
import time
|
||||
import logging
|
||||
import re
|
||||
import os.path
|
||||
import datetime
|
||||
|
||||
|
||||
class backup_rsync(backup_generic):
|
||||
"""Backup a directory on remote server with rsync and rsync protocol (requires running remote rsync daemon)"""
|
||||
type = 'rsync'
|
||||
required_params = backup_generic.required_params + ['remote_user','remote_dir','rsync_module','password_file']
|
||||
optional_params = backup_generic.optional_params + ['compressionlevel','compression','bwlimit','exclude_list','protect_args','overload_args']
|
||||
|
||||
remote_user='root'
|
||||
remote_dir=''
|
||||
|
||||
exclude_list=''
|
||||
rsync_module=''
|
||||
password_file = ''
|
||||
compression = ''
|
||||
bwlimit = 0
|
||||
protect_args = '1'
|
||||
overload_args = None
|
||||
compressionlevel = 0
|
||||
|
||||
|
||||
|
||||
def read_config(self,iniconf):
|
||||
assert(isinstance(iniconf,ConfigParser))
|
||||
backup_generic.read_config(self,iniconf)
|
||||
if not self.bwlimit and iniconf.has_option('global','bw_limit'):
|
||||
self.bwlimit = iniconf.getint('global','bw_limit')
|
||||
if not self.compressionlevel and iniconf.has_option('global','compression_level'):
|
||||
self.compressionlevel = iniconf.getint('global','compression_level')
|
||||
|
||||
def do_backup(self,stats):
|
||||
if not self.set_lock():
|
||||
self.logger.error("[%s] a lock file is set, a backup maybe already running!!",self.backup_name)
|
||||
return False
|
||||
|
||||
try:
|
||||
try:
|
||||
backup_source = 'undefined'
|
||||
dest_dir = os.path.join(self.backup_dir,self.backup_start_date+'.rsync/')
|
||||
if not os.path.isdir(dest_dir):
|
||||
if not self.dry_run:
|
||||
os.makedirs(dest_dir)
|
||||
else:
|
||||
print 'mkdir "%s"' % dest_dir
|
||||
else:
|
||||
raise Exception('backup destination directory already exists : %s' % dest_dir)
|
||||
|
||||
options = ['-rt','--stats','--delete-excluded','--numeric-ids','--delete-after']
|
||||
if self.logger.level:
|
||||
options.append('-P')
|
||||
|
||||
if self.dry_run:
|
||||
options.append('-d')
|
||||
|
||||
if self.overload_args <> None:
|
||||
options.append(self.overload_args)
|
||||
elif not "cygdrive" in self.remote_dir:
|
||||
# we don't preserve owner, group, links, hardlinks, perms for windows/cygwin as it is not reliable nor useful
|
||||
options.append('-lpgoD')
|
||||
|
||||
# the protect-args option is not available in all rsync version
|
||||
if not self.protect_args.lower() in ('false','no','0'):
|
||||
options.append('--protect-args')
|
||||
|
||||
if self.compression.lower() in ('true','yes','1'):
|
||||
options.append('-z')
|
||||
|
||||
if self.compressionlevel:
|
||||
options.append('--compress-level=%s' % self.compressionlevel)
|
||||
|
||||
if self.bwlimit:
|
||||
options.append('--bwlimit %s' % self.bwlimit)
|
||||
|
||||
latest = self.get_latest_backup(self.backup_start_date)
|
||||
if latest:
|
||||
options.extend(['--link-dest="%s"' % os.path.join('..',b,'') for b in latest])
|
||||
|
||||
def strip_quotes(s):
|
||||
if s[0] == '"':
|
||||
s = s[1:]
|
||||
if s[-1] == '"':
|
||||
s = s[:-1]
|
||||
return s
|
||||
|
||||
# Add excludes
|
||||
if "--exclude" in self.exclude_list:
|
||||
# old settings with exclude_list=--exclude toto --exclude=titi
|
||||
excludes = [strip_quotes(s).strip() for s in self.exclude_list.replace('--exclude=','').replace('--exclude ','').split()]
|
||||
else:
|
||||
try:
|
||||
# newsettings with exclude_list='too','titi', parsed as a str python list content
|
||||
excludes = eval('[%s]' % self.exclude_list)
|
||||
except Exception,e:
|
||||
raise Exception('Error reading exclude list : value %s, eval error %s (don\'t forget quotes and comma...)' % (self.exclude_list,e))
|
||||
options.extend(['--exclude="%s"' % x for x in excludes])
|
||||
|
||||
if (self.rsync_module and not self.password_file):
|
||||
raise Exception('You must specify a password file if you specify a rsync module')
|
||||
|
||||
if (not self.rsync_module and not self.private_key):
|
||||
raise Exception('If you don''t use SSH, you must specify a rsync module')
|
||||
|
||||
#rsync_re = re.compile('(?P<server>[^:]*)::(?P<export>[^/]*)/(?P<path>.*)')
|
||||
#ssh_re = re.compile('((?P<user>.*)@)?(?P<server>[^:]*):(?P<path>/.*)')
|
||||
|
||||
# Add ssh connection params
|
||||
if self.rsync_module:
|
||||
# Case of rsync exports
|
||||
if self.password_file:
|
||||
options.append('--password-file="%s"' % self.password_file)
|
||||
backup_source = '%s@%s::%s%s' % (self.remote_user, self.server_name, self.rsync_module, self.remote_dir)
|
||||
else:
|
||||
# case of rsync + ssh
|
||||
ssh_params = ['-o StrictHostKeyChecking=no','-c blowfish']
|
||||
if self.private_key:
|
||||
ssh_params.append('-i %s' % self.private_key)
|
||||
if self.ssh_port <> 22:
|
||||
ssh_params.append('-p %i' % self.ssh_port)
|
||||
options.append('-e "/usr/bin/ssh %s"' % (" ".join(ssh_params)))
|
||||
backup_source = '%s@%s:%s' % (self.remote_user,self.server_name,self.remote_dir)
|
||||
|
||||
# ensure there is a slash at end
|
||||
if backup_source[-1] <> '/':
|
||||
backup_source += '/'
|
||||
|
||||
options_params = " ".join(options)
|
||||
|
||||
cmd = '/usr/bin/rsync %s %s %s 2>&1' % (options_params,backup_source,dest_dir)
|
||||
self.logger.debug("[%s] rsync : %s",self.backup_name,cmd)
|
||||
|
||||
if not self.dry_run:
|
||||
self.line = ''
|
||||
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
|
||||
def ondata(data,context):
|
||||
if context.verbose:
|
||||
print data
|
||||
context.logger.debug(data)
|
||||
|
||||
log = monitor_stdout(process,ondata,self)
|
||||
|
||||
for l in log.splitlines():
|
||||
if l.startswith('Number of files:'):
|
||||
stats['total_files_count'] += int(l.split(':')[1])
|
||||
if l.startswith('Number of files transferred:'):
|
||||
stats['written_files_count'] += int(l.split(':')[1])
|
||||
if l.startswith('Total file size:'):
|
||||
stats['total_bytes'] += int(l.split(':')[1].split()[0])
|
||||
if l.startswith('Total transferred file size:'):
|
||||
stats['written_bytes'] += int(l.split(':')[1].split()[0])
|
||||
|
||||
returncode = process.returncode
|
||||
## deal with exit code 24 (file vanished)
|
||||
if (returncode == 24):
|
||||
self.logger.warning("[" + self.backup_name + "] Note: some files vanished before transfer")
|
||||
elif (returncode == 23):
|
||||
self.logger.warning("[" + self.backup_name + "] unable so set uid on some files")
|
||||
elif (returncode != 0):
|
||||
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:
|
||||
print cmd
|
||||
|
||||
#we suppress the .rsync suffix if everything went well
|
||||
finaldest = os.path.join(self.backup_dir,self.backup_start_date)
|
||||
self.logger.debug("[%s] renaming target directory from %s to %s" ,self.backup_name,dest_dir,finaldest)
|
||||
if not self.dry_run:
|
||||
os.rename(dest_dir, finaldest)
|
||||
self.logger.debug("[%s] touching datetime of target directory %s" ,self.backup_name,finaldest)
|
||||
print os.popen('touch "%s"' % finaldest).read()
|
||||
else:
|
||||
print "mv" ,dest_dir,finaldest
|
||||
stats['backup_location'] = finaldest
|
||||
stats['status']='OK'
|
||||
stats['log']='ssh+rsync backup from %s OK, %d bytes written for %d changed files' % (backup_source,stats['written_bytes'],stats['written_files_count'])
|
||||
|
||||
except BaseException , e:
|
||||
stats['status']='ERROR'
|
||||
stats['log']=str(e)
|
||||
raise
|
||||
|
||||
|
||||
finally:
|
||||
self.remove_lock()
|
||||
|
||||
def get_latest_backup(self,current):
|
||||
result = []
|
||||
filelist = os.listdir(self.backup_dir)
|
||||
filelist.sort()
|
||||
filelist.reverse()
|
||||
full = ''
|
||||
r_full = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$')
|
||||
r_partial = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}.rsync$')
|
||||
# we take all latest partials younger than the latest full and the latest full
|
||||
for item in filelist:
|
||||
if r_partial.match(item) and item<current:
|
||||
result.append(item)
|
||||
elif r_full.match(item) and item<current:
|
||||
result.append(item)
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def register_existingbackups(self):
|
||||
"""scan backup dir 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,))]
|
||||
|
||||
filelist = os.listdir(self.backup_dir)
|
||||
filelist.sort()
|
||||
p = re.compile('^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$')
|
||||
for item in filelist:
|
||||
if p.match(item):
|
||||
dir_name = os.path.join(self.backup_dir,item)
|
||||
if not dir_name in registered:
|
||||
start = datetime.datetime.strptime(item,'%Y%m%d-%Hh%Mm%S').isoformat()
|
||||
if fileisodate(dir_name)>start:
|
||||
stop = fileisodate(dir_name)
|
||||
else:
|
||||
stop = start
|
||||
self.logger.info('Registering %s started on %s',dir_name,start)
|
||||
self.logger.debug(' Disk usage %s','du -sb "%s"' % dir_name)
|
||||
if not self.dry_run:
|
||||
size_bytes = int(os.popen('du -sb "%s"' % dir_name).read().split('\t')[0])
|
||||
else:
|
||||
size_bytes = 0
|
||||
self.logger.debug(' Size in bytes : %i',size_bytes)
|
||||
if not self.dry_run:
|
||||
self.dbstat.add(self.backup_name,self.server_name,'',\
|
||||
backup_start=start,backup_end = stop,status='OK',total_bytes=size_bytes,backup_location=dir_name)
|
||||
else:
|
||||
self.logger.info('Skipping %s, already registered',dir_name)
|
||||
|
||||
|
||||
def is_pid_still_running(self,lockfile):
|
||||
f = open(lockfile)
|
||||
lines = f.readlines()
|
||||
f.close()
|
||||
if len(lines)==0 :
|
||||
self.logger.info("[" + self.backup_name + "] empty lock file, removing...")
|
||||
return False
|
||||
|
||||
for line in lines:
|
||||
if line.startswith('pid='):
|
||||
pid = line.split('=')[1].strip()
|
||||
if os.path.exists("/proc/" + pid):
|
||||
self.logger.info("[" + self.backup_name + "] process still there")
|
||||
return True
|
||||
else:
|
||||
self.logger.info("[" + self.backup_name + "] process not there anymore remove lock")
|
||||
return False
|
||||
else:
|
||||
self.logger.info("[" + self.backup_name + "] incorrrect lock file : no pid line")
|
||||
return False
|
||||
|
||||
|
||||
def set_lock(self):
|
||||
self.logger.debug("[" + self.backup_name + "] setting lock")
|
||||
|
||||
#TODO: improve for race condition
|
||||
#TODO: also check if process is really there
|
||||
if os.path.isfile(self.backup_dir + '/lock'):
|
||||
self.logger.debug("[" + self.backup_name + "] File " + self.backup_dir + '/lock already exist')
|
||||
if self.is_pid_still_running(self.backup_dir + '/lock')==False:
|
||||
self.logger.info("[" + self.backup_name + "] removing lock file " + self.backup_dir + '/lock')
|
||||
os.unlink(self.backup_dir + '/lock')
|
||||
else:
|
||||
return False
|
||||
|
||||
lockfile = open(self.backup_dir + '/lock',"w")
|
||||
# Write all the lines at once:
|
||||
lockfile.write('pid='+str(os.getpid()))
|
||||
lockfile.write('\nbackup_time=' + self.backup_start_date)
|
||||
lockfile.close()
|
||||
return True
|
||||
|
||||
def remove_lock(self):
|
||||
self.logger.debug("[%s] removing lock",self.backup_name )
|
||||
os.unlink(self.backup_dir + '/lock')
|
||||
|
||||
class backup_rsync_ssh(backup_rsync):
|
||||
"""Backup a directory on remote server with rsync and ssh protocol (requires rsync software on remote host)"""
|
||||
type = 'rsync+ssh'
|
||||
required_params = backup_generic.required_params + ['remote_user','remote_dir','private_key']
|
||||
optional_params = backup_generic.optional_params + ['compression','bwlimit','ssh_port','exclude_list','protect_args','overload_args']
|
||||
|
||||
|
||||
register_driver(backup_rsync)
|
||||
register_driver(backup_rsync_ssh)
|
||||
|
||||
if __name__=='__main__':
|
||||
logger = logging.getLogger('tisbackup')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
cp = ConfigParser()
|
||||
cp.read('/opt/tisbackup/configtest.ini')
|
||||
dbstat = BackupStat('/backup/data/log/tisbackup.sqlite')
|
||||
b = backup_rsync('htouvet','/backup/data/htouvet',dbstat)
|
||||
b.read_config(cp)
|
||||
b.process_backup()
|
||||
print b.checknagios()
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/python
|
||||
# -*- 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/>.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import datetime
|
||||
from common import *
|
||||
import XenAPI
|
||||
import time
|
||||
import logging
|
||||
import re
|
||||
import os.path
|
||||
import datetime
|
||||
import select
|
||||
import urllib2, urllib
|
||||
import base64
|
||||
import socket
|
||||
import pexpect
|
||||
from stat import *
|
||||
|
||||
|
||||
class backup_switch(backup_generic):
|
||||
"""Backup a startup-config on a switch"""
|
||||
type = 'switch'
|
||||
|
||||
required_params = backup_generic.required_params + ['switch_ip','switch_user' , 'switch_type']
|
||||
optional_params = backup_generic.optional_params + ['switch_password']
|
||||
|
||||
def switch_hp(self, filename):
|
||||
|
||||
s = socket.socket()
|
||||
try:
|
||||
s.connect((self.switch_ip, 23))
|
||||
s.close()
|
||||
except:
|
||||
raise
|
||||
|
||||
child=pexpect.spawn('telnet '+self.switch_ip)
|
||||
time.sleep(1)
|
||||
if self.switch_user != "":
|
||||
child.sendline(self.switch_user)
|
||||
child.sendline(self.switch_password+'\r')
|
||||
else:
|
||||
child.sendline(self.switch_password+'\r')
|
||||
try:
|
||||
child.expect("#")
|
||||
except:
|
||||
raise Exception("Bad Credentials")
|
||||
child.sendline( "terminal length 1000\r")
|
||||
child.expect("#")
|
||||
child.sendline( "show config\r")
|
||||
child.maxread = 100000000
|
||||
child.expect("Startup.+$")
|
||||
lines = child.after
|
||||
if "-- MORE --" in lines:
|
||||
raise Exception("Terminal lenght is not sufficient")
|
||||
child.expect("#")
|
||||
lines += child.before
|
||||
child.sendline("logout\r")
|
||||
child.send('y\r')
|
||||
for line in lines.split("\n")[1:-1]:
|
||||
open(filename,"a").write(line.strip()+"\n")
|
||||
|
||||
def switch_linksys_SRW2024(self, filename):
|
||||
s = socket.socket()
|
||||
try:
|
||||
s.connect((self.switch_ip, 23))
|
||||
s.close()
|
||||
except:
|
||||
raise
|
||||
|
||||
child=pexpect.spawn('telnet '+self.switch_ip)
|
||||
time.sleep(1)
|
||||
if hasattr(self,'switch_password'):
|
||||
child.sendline(self.switch_user+'\t')
|
||||
child.sendline(self.switch_password+'\r')
|
||||
else:
|
||||
child.sendline(self.switch_user+'\r')
|
||||
try:
|
||||
child.expect('Menu')
|
||||
except:
|
||||
raise Exception("Bad Credentials")
|
||||
child.sendline('\032')
|
||||
child.expect('>')
|
||||
child.sendline('lcli')
|
||||
child.expect("Name:")
|
||||
if hasattr(self,'switch_password'):
|
||||
child.send(self.switch_user+'\r'+self.switch_password+'\r')
|
||||
else:
|
||||
child.sendline(self.switch_user)
|
||||
child.expect(".*#")
|
||||
child.sendline( "terminal datadump")
|
||||
child.expect("#")
|
||||
child.sendline( "show startup-config")
|
||||
child.expect("#")
|
||||
lines = child.before
|
||||
if "Unrecognized command" in lines:
|
||||
raise Exception("Bad Credentials")
|
||||
child.sendline("exit")
|
||||
child.expect( ">")
|
||||
child.sendline("logout")
|
||||
for line in lines.split("\n")[1:-1]:
|
||||
open(filename,"a").write(line.strip()+"\n")
|
||||
|
||||
|
||||
def switch_dlink_DGS1210(self, filename):
|
||||
login_data = urllib.urlencode({'Login' : self.switch_user, 'Password' : self.switch_password, 'currlang' : 0, 'BrowsingPage' : 'index_dlink.htm', 'changlang' : 0})
|
||||
req = urllib2.Request('http://%s/' % self.switch_ip, login_data)
|
||||
resp = urllib2.urlopen(req)
|
||||
if "Wrong password" in resp.read():
|
||||
raise Exception("Wrong password")
|
||||
resp = urllib2.urlopen("http://%s/config.bin?Gambit=gdkdcdgdidbdkdadkdbgegngjgogkdbgegngjgog&dumy=1348649950256" % self.switch_ip)
|
||||
f = open(filename, 'w')
|
||||
f.write(resp.read())
|
||||
|
||||
|
||||
def do_backup(self,stats):
|
||||
try:
|
||||
dest_filename = os.path.join(self.backup_dir,"%s-%s" % (self.backup_name,self.backup_start_date))
|
||||
|
||||
options = []
|
||||
options_params = " ".join(options)
|
||||
if "LINKSYS-SRW2024" == self.switch_type:
|
||||
dest_filename += '.txt'
|
||||
self.switch_linksys_SRW2024(dest_filename)
|
||||
elif self.switch_type in [ "HP-PROCURVE-4104GL", "HP-PROCURVE-2524" ]:
|
||||
dest_filename += '.txt'
|
||||
self.switch_hp(dest_filename)
|
||||
elif "DLINK-DGS1210" == self.switch_type:
|
||||
dest_filename += '.bin'
|
||||
self.switch_dlink_DGS1210(dest_filename)
|
||||
else:
|
||||
raise Exception("Unknown Switch type")
|
||||
|
||||
stats['total_files_count']=1
|
||||
stats['written_files_count']=1
|
||||
stats['total_bytes']= os.stat(dest_filename).st_size
|
||||
stats['written_bytes'] = stats['total_bytes']
|
||||
stats['backup_location'] = dest_filename
|
||||
stats['status']='OK'
|
||||
stats['log']='Switch backup from %s OK, %d bytes written' % (self.server_name,stats['written_bytes'])
|
||||
|
||||
|
||||
except BaseException , e:
|
||||
stats['status']='ERROR'
|
||||
stats['log']=str(e)
|
||||
raise
|
||||
|
||||
|
||||
|
||||
register_driver(backup_switch)
|
||||
|
||||
if __name__=='__main__':
|
||||
logger = logging.getLogger('tisbackup')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
cp = ConfigParser()
|
||||
cp.read('/opt/tisbackup/configtest.ini')
|
||||
b = backup_xva()
|
||||
b.read_config(cp)
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/python
|
||||
# -*- 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/>.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
import datetime
|
||||
import base64
|
||||
import os
|
||||
from common import *
|
||||
|
||||
class backup_xcp_metadata(backup_generic):
|
||||
"""Backup metatdata of a xcp pool using xe pool-dump-database"""
|
||||
type = 'xcp-dump-metadata'
|
||||
required_params = ['type','server_name','xcp_user','xcp_passwd','backup_name']
|
||||
xcp_user=''
|
||||
xcp_passwd=''
|
||||
|
||||
def do_backup(self,stats):
|
||||
|
||||
self.logger.debug('[%s] Connecting to %s with user root and key %s',self.backup_name,self.server_name,self.private_key)
|
||||
|
||||
if os.path.isfile('/opt/xensource/bin/xe') == False:
|
||||
raise Exception('Aborting, /opt/xensource/bin/xe binary not present"')
|
||||
|
||||
|
||||
t = datetime.datetime.now()
|
||||
backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S')
|
||||
|
||||
# dump pool medatadata
|
||||
localpath = os.path.join(self.backup_dir , 'xcp_metadata-' + backup_start_date + '.dump.gz')
|
||||
temppath = '/tmp/xcp_metadata-' + backup_start_date + '.dump'
|
||||
|
||||
stats['status']='Dumping'
|
||||
|
||||
if not self.dry_run:
|
||||
cmd = "/opt/xensource/bin/xe -s %s -u %s -pw %s pool-dump-database file-name=%s" %(self.server_name,self.xcp_user,self.xcp_passwd,temppath)
|
||||
self.logger.debug('[%s] Dump XCP Metadata : %s',self.backup_name,cmd)
|
||||
call_external_process(cmd)
|
||||
|
||||
|
||||
# zip the file
|
||||
stats['status']='Zipping'
|
||||
cmd = 'gzip %s ' %temppath
|
||||
self.logger.debug('[%s] Compress backup : %s',self.backup_name,cmd)
|
||||
if not self.dry_run:
|
||||
call_external_process(cmd)
|
||||
|
||||
# get the file
|
||||
stats['status']='move to backup directory'
|
||||
self.logger.debug('[%s] Moving temp backup file %s to backup new path %s',self.backup_name,self.server_name,localpath)
|
||||
if not self.dry_run:
|
||||
shutil.move (temppath + '.gz' ,localpath)
|
||||
|
||||
if not self.dry_run:
|
||||
stats['total_files_count']=1
|
||||
stats['written_files_count']=1
|
||||
stats['total_bytes']=os.stat(localpath).st_size
|
||||
stats['written_bytes']=os.stat(localpath).st_size
|
||||
stats['log']='gzip dump of DB %s:%s (%d bytes) to %s' % (self.server_name,'xcp metadata dump', stats['written_bytes'], localpath)
|
||||
stats['backup_location'] = localpath
|
||||
stats['status']='OK'
|
||||
|
||||
|
||||
|
||||
def register_existingbackups(self):
|
||||
"""scan metatdata backup files 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,))]
|
||||
|
||||
filelist = os.listdir(self.backup_dir)
|
||||
filelist.sort()
|
||||
p = re.compile('^%s-(?P<date>\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}).dump.gz$' % self.server_name)
|
||||
for item in filelist:
|
||||
sr = p.match(item)
|
||||
if sr:
|
||||
file_name = os.path.join(self.backup_dir,item)
|
||||
start = datetime.datetime.strptime(sr.groups()[0],'%Y%m%d-%Hh%Mm%S').isoformat()
|
||||
if not file_name in registered:
|
||||
self.logger.info('Registering %s from %s',file_name,fileisodate(file_name))
|
||||
size_bytes = int(os.popen('du -sb "%s"' % file_name).read().split('\t')[0])
|
||||
self.logger.debug(' Size in bytes : %i',size_bytes)
|
||||
if not self.dry_run:
|
||||
self.dbstat.add(self.backup_name,self.server_name,'',\
|
||||
backup_start=start,backup_end=fileisodate(file_name),status='OK',total_bytes=size_bytes,backup_location=file_name)
|
||||
else:
|
||||
self.logger.info('Skipping %s from %s, already registered',file_name,fileisodate(file_name))
|
||||
|
||||
register_driver(backup_xcp_metadata)
|
||||
Executable
+165
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/python
|
||||
# -*- 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/>.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import datetime
|
||||
from common import *
|
||||
import XenAPI
|
||||
import time
|
||||
import logging
|
||||
import re
|
||||
import os.path
|
||||
import os
|
||||
import datetime
|
||||
import select
|
||||
import urllib2
|
||||
import base64
|
||||
import socket
|
||||
from stat import *
|
||||
|
||||
|
||||
class backup_xva(backup_generic):
|
||||
"""Backup a VM running on a XCP server as a XVA file (requires xe tools and XenAPI)"""
|
||||
type = 'xen-xva'
|
||||
|
||||
required_params = backup_generic.required_params + ['xcphost','password_file','server_name']
|
||||
optional_params = backup_generic.optional_params + ['excluded_vbds','remote_user','private_key']
|
||||
|
||||
def export_xva(self, vdi_name, filename, dry_run):
|
||||
|
||||
user_xen, password_xen, null = open(self.password_file).read().split('\n')
|
||||
session = XenAPI.Session('https://'+self.xcphost)
|
||||
try:
|
||||
session.login_with_password(user_xen,password_xen)
|
||||
except XenAPI.Failure, error:
|
||||
msg,ip = error.details
|
||||
|
||||
if msg == 'HOST_IS_SLAVE':
|
||||
xcphost = ip
|
||||
session = XenAPI.Session('https://'+xcphost)
|
||||
session.login_with_password(user_xen,password_xen)
|
||||
|
||||
vm = session.xenapi.VM.get_by_name_label(vdi_name)[0]
|
||||
status_vm = session.xenapi.VM.get_power_state(vm)
|
||||
|
||||
self.logger.debug("[%s] Status of VM: %s",self.backup_name,status_vm)
|
||||
if status_vm == "Running":
|
||||
self.logger.debug("[%s] Shudown in progress",self.backup_name)
|
||||
if dry_run:
|
||||
print "session.xenapi.VM.clean_shutdown(vm)"
|
||||
|
||||
else:
|
||||
session.xenapi.VM.clean_shutdown(vm)
|
||||
|
||||
try:
|
||||
try:
|
||||
self.logger.debug("[%s] Copy in progress",self.backup_name)
|
||||
|
||||
socket.setdefaulttimeout(120)
|
||||
auth = base64.encodestring("%s:%s" % (user_xen, password_xen)).strip()
|
||||
url = "https://"+self.xcphost+"/export?uuid="+session.xenapi.VM.get_uuid(vm)
|
||||
request = urllib2.Request(url)
|
||||
request.add_header("Authorization", "Basic %s" % auth)
|
||||
result = urllib2.urlopen(request)
|
||||
|
||||
if dry_run:
|
||||
print "request = urllib2.Request(%s)" % url
|
||||
print 'outputfile = open(%s, "wb")' % filename
|
||||
else:
|
||||
outputfile = open(filename, "wb")
|
||||
for line in result:
|
||||
outputfile.write(line)
|
||||
outputfile.close()
|
||||
|
||||
except:
|
||||
if os.path.exists(filename):
|
||||
os.unlink(filename)
|
||||
raise
|
||||
|
||||
finally:
|
||||
if status_vm == "Running":
|
||||
self.logger.debug("[%s] Starting in progress",self.backup_name)
|
||||
if dry_run:
|
||||
print "session.xenapi.Async.VM.start(vm,False,True)"
|
||||
else:
|
||||
session.xenapi.Async.VM.start(vm,False,True)
|
||||
|
||||
session.logout()
|
||||
|
||||
if os.path.exists(filename):
|
||||
import tarfile
|
||||
tar = tarfile.open(filename)
|
||||
if not tar.getnames():
|
||||
unlink(filename)
|
||||
return("Tar error")
|
||||
tar.close()
|
||||
|
||||
return(0)
|
||||
|
||||
|
||||
|
||||
|
||||
def do_backup(self,stats):
|
||||
try:
|
||||
dest_filename = os.path.join(self.backup_dir,"%s-%s.%s" % (self.backup_name,self.backup_start_date,'xva'))
|
||||
|
||||
options = []
|
||||
options_params = " ".join(options)
|
||||
cmd = self.export_xva( self.server_name, dest_filename, self.dry_run)
|
||||
if os.path.exists(dest_filename):
|
||||
stats['written_bytes'] = os.stat(dest_filename)[ST_SIZE]
|
||||
stats['total_files_count'] = 1
|
||||
stats['written_files_count'] = 1
|
||||
stats['total_bytes'] = stats['written_bytes']
|
||||
else:
|
||||
stats['written_bytes'] = 0
|
||||
|
||||
stats['backup_location'] = dest_filename
|
||||
if cmd == 0:
|
||||
stats['log']='XVA backup from %s OK, %d bytes written' % (self.server_name,stats['written_bytes'])
|
||||
stats['status']='OK'
|
||||
else:
|
||||
stats['status']='ERROR'
|
||||
stats['log']=cmd
|
||||
|
||||
|
||||
|
||||
except BaseException , e:
|
||||
stats['status']='ERROR'
|
||||
stats['log']=str(e)
|
||||
raise
|
||||
|
||||
|
||||
|
||||
register_driver(backup_xva)
|
||||
|
||||
if __name__=='__main__':
|
||||
logger = logging.getLogger('tisbackup')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
cp = ConfigParser()
|
||||
cp.read('/opt/tisbackup/configtest.ini')
|
||||
b = backup_xva()
|
||||
b.read_config(cp)
|
||||
|
||||
@@ -0,0 +1,909 @@
|
||||
#!/usr/bin/python
|
||||
# -*- 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/>.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import re
|
||||
import logging
|
||||
import datetime
|
||||
import time
|
||||
from iniparse import ConfigParser
|
||||
import sqlite3
|
||||
import shutil
|
||||
import select
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko
|
||||
import paramiko
|
||||
except ImportError,e:
|
||||
print "Error : can not load paramiko library %s" % e
|
||||
raise
|
||||
|
||||
sys.stderr = sys.__stderr__
|
||||
|
||||
nagiosStateOk = 0
|
||||
nagiosStateWarning = 1
|
||||
nagiosStateCritical = 2
|
||||
nagiosStateUnknown = 3
|
||||
|
||||
backup_drivers = {}
|
||||
def register_driver(driverclass):
|
||||
backup_drivers[driverclass.type] = driverclass
|
||||
|
||||
def datetime2isodate(adatetime=None):
|
||||
if not adatetime:
|
||||
adatetime = datetime.datetime.now()
|
||||
assert(isinstance(adatetime,datetime.datetime))
|
||||
return adatetime.isoformat()
|
||||
|
||||
def isodate2datetime(isodatestr):
|
||||
# we remove the microseconds part as it is not working for python2.5 strptime
|
||||
return datetime.datetime.strptime(isodatestr.split('.')[0] , "%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
def time2display(adatetime):
|
||||
return adatetime.strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
def hours_minutes(hours):
|
||||
if hours is None:
|
||||
return None
|
||||
else:
|
||||
return "%02i:%02i" % ( int(hours) , int((hours - int(hours)) * 60.0))
|
||||
|
||||
def fileisodate(filename):
|
||||
return datetime.datetime.fromtimestamp(os.stat(filename).st_mtime).isoformat()
|
||||
|
||||
def dateof(adatetime):
|
||||
return adatetime.replace(hour=0,minute=0,second=0,microsecond=0)
|
||||
|
||||
#####################################
|
||||
# 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
|
||||
#
|
||||
# Tail recursion removed and leading garbage handled on March 12 2010, Alessandro Forghieri
|
||||
def splitThousands( s, tSep=',', dSep='.'):
|
||||
'''Splits a general float on thousands. GIGO on general input'''
|
||||
if s == None:
|
||||
return 0
|
||||
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 == '':
|
||||
cnt = -1
|
||||
else:
|
||||
cnt = s.rfind( dSep )
|
||||
if cnt > 0:
|
||||
rhs = dSep + s[ cnt+1: ]
|
||||
s = s[ :cnt ]
|
||||
else:
|
||||
rhs = ''
|
||||
|
||||
splt=''
|
||||
while s != '':
|
||||
splt= s[ -3: ] + tSep + splt
|
||||
s = s[ :-3 ]
|
||||
|
||||
return lhs + splt[ :-1 ] + rhs
|
||||
|
||||
|
||||
def call_external_process(shell_string):
|
||||
p = subprocess.call(shell_string, shell=True)
|
||||
if (p != 0 ):
|
||||
raise Exception('shell program exited with error code ' + str(p), shell_string)
|
||||
|
||||
def check_string(test_string):
|
||||
pattern = r'[^\.A-Za-z0-9\-_]'
|
||||
if re.search(pattern, test_string):
|
||||
#Character other then . a-z 0-9 was found
|
||||
print 'Invalid : %r' % (test_string,)
|
||||
|
||||
def convert_bytes(bytes):
|
||||
if bytes is None:
|
||||
return None
|
||||
else:
|
||||
bytes = float(bytes)
|
||||
if bytes >= 1099511627776:
|
||||
terabytes = bytes / 1099511627776
|
||||
size = '%.2fT' % terabytes
|
||||
elif bytes >= 1073741824:
|
||||
gigabytes = bytes / 1073741824
|
||||
size = '%.2fG' % gigabytes
|
||||
elif bytes >= 1048576:
|
||||
megabytes = bytes / 1048576
|
||||
size = '%.2fM' % megabytes
|
||||
elif bytes >= 1024:
|
||||
kilobytes = bytes / 1024
|
||||
size = '%.2fK' % kilobytes
|
||||
else:
|
||||
size = '%.2fb' % bytes
|
||||
return size
|
||||
|
||||
## {{{ 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
|
||||
"""
|
||||
def defaultcb(fieldname,value):
|
||||
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()
|
||||
for dd in d: # iterate over description
|
||||
l = dd[1]
|
||||
if not l:
|
||||
l = 12 # or default arg ...
|
||||
l = max(l, len(dd[0])) # handle long names
|
||||
names.append(dd[0])
|
||||
lengths.append(l)
|
||||
for col in range(len(lengths)):
|
||||
if rowlens:
|
||||
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])
|
||||
format = " ".join(["%%-%ss" % l for l in lengths])
|
||||
result = [format % tuple(names)]
|
||||
result.append(format % tuple(rules))
|
||||
for row in data:
|
||||
row_cb=[]
|
||||
for col in range(len(d)):
|
||||
row_cb.append(callback(d[col][0],row[col]))
|
||||
result.append(format % tuple(row_cb))
|
||||
return "\n".join(result)
|
||||
## end of http://code.activestate.com/recipes/81189/ }}}
|
||||
|
||||
|
||||
def html_table(cur,callback=None):
|
||||
"""
|
||||
cur est un cursor issu d'une requete
|
||||
callback est une fonction qui prend (rowmap,fieldname,value)
|
||||
et renvoie une representation texte
|
||||
"""
|
||||
def safe_unicode(iso):
|
||||
if iso is None:
|
||||
return None
|
||||
elif isinstance(iso, str):
|
||||
return iso.decode('iso8859')
|
||||
else:
|
||||
return iso
|
||||
|
||||
def itermap(cur):
|
||||
for row in cur:
|
||||
yield dict((cur.description[idx][0], value)
|
||||
for idx, value in enumerate(row))
|
||||
|
||||
head=u"<tr>"+"".join(["<th>"+c[0]+"</th>" for c in cur.description])+"</tr>"
|
||||
lines=""
|
||||
if callback:
|
||||
for r in itermap(cur):
|
||||
lines=lines+"<tr>"+"".join(["<td>"+str(callback(r,c[0],safe_unicode(r[c[0]])))+"</td>" for c in cur.description])+"</tr>"
|
||||
else:
|
||||
for r in cur:
|
||||
lines=lines+"<tr>"+"".join(["<td>"+safe_unicode(c)+"</td>" for c in r])+"</tr>"
|
||||
|
||||
return "<table border=1 cellpadding=2 cellspacing=0>%s%s</table>" % (head,lines)
|
||||
|
||||
|
||||
|
||||
def monitor_stdout(aprocess, onoutputdata,context):
|
||||
"""Reads data from stdout and stderr from aprocess and return as a string
|
||||
on each chunk, call a call back onoutputdata(dataread)
|
||||
"""
|
||||
assert(isinstance(aprocess,subprocess.Popen))
|
||||
read_set = []
|
||||
stdout = []
|
||||
line = ''
|
||||
|
||||
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, [], [])
|
||||
except select.error, e:
|
||||
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)
|
||||
if data == "":
|
||||
aprocess.stdout.close()
|
||||
read_set.remove(aprocess.stdout)
|
||||
while data and not data in ('\n','\r'):
|
||||
line += data
|
||||
data = os.read(aprocess.stdout.fileno(), 1)
|
||||
if line or data in ('\n','\r'):
|
||||
stdout.append(line)
|
||||
if onoutputdata:
|
||||
onoutputdata(line,context)
|
||||
line=''
|
||||
|
||||
# Reads one line from stderr
|
||||
if aprocess.stderr in rlist:
|
||||
data = os.read(aprocess.stderr.fileno(), 1)
|
||||
if data == "":
|
||||
aprocess.stderr.close()
|
||||
read_set.remove(aprocess.stderr)
|
||||
while data and not data in ('\n','\r'):
|
||||
line += data
|
||||
data = os.read(aprocess.stderr.fileno(), 1)
|
||||
if line or data in ('\n','\r'):
|
||||
stdout.append(line)
|
||||
if onoutputdata:
|
||||
onoutputdata(line,context)
|
||||
line=''
|
||||
|
||||
aprocess.wait()
|
||||
if line:
|
||||
stdout.append(line)
|
||||
if onoutputdata:
|
||||
onoutputdata(line,context)
|
||||
return "\n".join(stdout)
|
||||
|
||||
|
||||
class BackupStat:
|
||||
dbpath = ''
|
||||
db = None
|
||||
logger = logging.getLogger('tisbackup')
|
||||
|
||||
def __init__(self,dbpath):
|
||||
self.dbpath = dbpath
|
||||
if not os.path.isfile(self.dbpath):
|
||||
self.db=sqlite3.connect(self.dbpath)
|
||||
self.initdb()
|
||||
else:
|
||||
self.db=sqlite3.connect(self.dbpath)
|
||||
if not "'TYPE'" in str(self.db.execute("select * from stats").description):
|
||||
self.updatedb()
|
||||
|
||||
|
||||
def updatedb(self):
|
||||
self.logger.debug('Update stat database')
|
||||
self.db.execute("alter table stats add column TYPE TEXT;")
|
||||
self.db.execute("update stats set TYPE='BACKUP';")
|
||||
self.db.commit()
|
||||
|
||||
def initdb(self):
|
||||
assert(isinstance(self.db,sqlite3.Connection))
|
||||
self.logger.debug('Initialize stat database')
|
||||
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);""")
|
||||
self.db.commit()
|
||||
|
||||
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"""
|
||||
if not backup_end:
|
||||
backup_end = datetime2isodate()
|
||||
if backup_duration == None:
|
||||
try:
|
||||
# get duration using start of backup datetime
|
||||
backup_duration = (isodate2datetime(backup_end) - isodate2datetime(self.query('select backup_start from stats where rowid=?',(rowid,))[0]['backup_start'])).seconds / 3600.0
|
||||
except:
|
||||
backup_duration = None
|
||||
|
||||
# update stat record
|
||||
self.db.execute("""\
|
||||
update stats set
|
||||
total_files_count=?,written_files_count=?,total_bytes=?,written_bytes=?,log=?,status=?,backup_end=?,backup_duration=?,backup_location=?
|
||||
where
|
||||
rowid = ?
|
||||
""",(total_files_count,written_files_count,total_bytes,written_bytes,log,status,backup_end,backup_duration,backup_location,rowid))
|
||||
self.db.commit()
|
||||
|
||||
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):
|
||||
if not backup_start:
|
||||
backup_start=datetime2isodate()
|
||||
if not backup_end:
|
||||
backup_end=datetime2isodate()
|
||||
|
||||
cur = self.db.execute("""\
|
||||
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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
""",(
|
||||
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)
|
||||
)
|
||||
|
||||
self.db.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
def query(self,query, args=(), one=False):
|
||||
"""
|
||||
execute la requete query sur la db et renvoie un tableau de dictionnaires
|
||||
"""
|
||||
cur = self.db.execute(query, args)
|
||||
rv = [dict((cur.description[idx][0], value)
|
||||
for idx, value in enumerate(row)) for row in cur.fetchall()]
|
||||
return (rv[0] if rv else None) if one else rv
|
||||
|
||||
def last_backups(self,backup_name,count=30):
|
||||
if backup_name:
|
||||
cur = self.db.execute('select * from stats where backup_name=? order by backup_end desc limit ?',(backup_name,count))
|
||||
else:
|
||||
cur = self.db.execute('select * from stats order by backup_end desc limit ?',(count,))
|
||||
|
||||
def fcb(fieldname,value):
|
||||
if fieldname in ('backup_start','backup_end'):
|
||||
return time2display(isodate2datetime(value))
|
||||
elif 'bytes' in fieldname:
|
||||
return convert_bytes(value)
|
||||
elif 'count' in fieldname:
|
||||
return splitThousands(value,' ','.')
|
||||
elif 'backup_duration' in fieldname:
|
||||
return hours_minutes(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
#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)
|
||||
|
||||
|
||||
def fcb(self,fields,fieldname,value):
|
||||
if fieldname in ('backup_start','backup_end'):
|
||||
return time2display(isodate2datetime(value))
|
||||
elif 'bytes' in fieldname:
|
||||
return convert_bytes(value)
|
||||
elif 'count' in fieldname:
|
||||
return splitThousands(value,' ','.')
|
||||
elif 'backup_duration' in fieldname:
|
||||
return hours_minutes(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def as_html(self,cur):
|
||||
if cur:
|
||||
return html_table(cur,self.fcb)
|
||||
else:
|
||||
return html_table(self.db.execute('select * from stats order by backup_start asc'),self.fcb)
|
||||
|
||||
|
||||
def ssh_exec(command,ssh=None,server_name='',remote_user='',private_key='',ssh_port=22):
|
||||
"""execute command on server_name using the provided ssh connection
|
||||
or creates a new connection if ssh is not provided.
|
||||
returns (exit_code,output)
|
||||
|
||||
output is the concatenation of stdout and stderr
|
||||
"""
|
||||
if not ssh:
|
||||
assert(server_name and remote_user and private_key)
|
||||
try:
|
||||
mykey = paramiko.RSAKey.from_private_key_file(private_key)
|
||||
except paramiko.SSHException:
|
||||
mykey = paramiko.DSSKey.from_private_key_file(private_key)
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(server_name,username=remote_user,pkey = private_key,port=ssh_port)
|
||||
|
||||
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()
|
||||
output = stdout.read()
|
||||
exit_code = chan.recv_exit_status()
|
||||
return (exit_code,output)
|
||||
|
||||
|
||||
class backup_generic:
|
||||
"""Generic ancestor class for backups, not registered"""
|
||||
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 = ''
|
||||
dbstat = None
|
||||
dry_run = False
|
||||
preexec = ''
|
||||
postexec = ''
|
||||
maximum_backup_age = None
|
||||
backup_retention_time = None
|
||||
verbose = False
|
||||
private_key=''
|
||||
ssh_port=22
|
||||
|
||||
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)
|
||||
self.backup_name = backup_name
|
||||
self.backup_dir = backup_dir
|
||||
|
||||
self.dbstat = dbstat
|
||||
assert(isinstance(self.dbstat,BackupStat) or self.dbstat==None)
|
||||
|
||||
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
|
||||
""" % {'type':cls.type,
|
||||
'desc':cls.__doc__,
|
||||
'required':",".join(cls.required_params),
|
||||
'optional':",".join(cls.optional_params)}
|
||||
|
||||
def check_required_params(self):
|
||||
for name in self.required_params:
|
||||
if not hasattr(self,name) or not getattr(self,name):
|
||||
raise Exception('[%s] Config Attribute %s is required' % (self.backup_name,name))
|
||||
if (self.preexec or self.postexec) and (not self.private_key or not self.remote_user):
|
||||
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 not name 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)
|
||||
|
||||
# if retention (in days) is not defined at section level, get default global one.
|
||||
if not self.backup_retention_time:
|
||||
self.backup_retention_time = iniconf.getint('global','backup_retention_time')
|
||||
|
||||
# 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:
|
||||
self.maximum_backup_age = iniconf.getint('global','maximum_backup_age')
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def do_preexec(self,stats):
|
||||
self.logger.info("[%s] executing preexec %s ",self.backup_name,self.preexec)
|
||||
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())
|
||||
ssh.connect(self.server_name,username=self.remote_user,pkey = mykey)
|
||||
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()
|
||||
self.logger.info('[%s] preexec exit code : "%i", output : %s',self.backup_name , exit_code, output )
|
||||
return exit_code
|
||||
else:
|
||||
return 0
|
||||
|
||||
def do_postexec(self,stats):
|
||||
self.logger.info("[%s] executing postexec %s ",self.backup_name,self.postexec)
|
||||
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())
|
||||
ssh.connect(self.server_name,username=self.remote_user,pkey = mykey)
|
||||
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()
|
||||
self.logger.info('[%s] postexec exit code : "%i", output : %s',self.backup_name , exit_code, output )
|
||||
return exit_code
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def do_backup(self,stats):
|
||||
"""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.
|
||||
launch
|
||||
- do_preexec
|
||||
- do_backup
|
||||
- do_postexec
|
||||
|
||||
returns a dict for stats
|
||||
"""
|
||||
self.logger.info('[%s] ######### Starting backup',self.backup_name)
|
||||
|
||||
starttime = time.time()
|
||||
self.backup_start_date = datetime.datetime.now().strftime('%Y%m%d-%Hh%Mm%S')
|
||||
|
||||
if not self.dry_run and self.dbstat:
|
||||
stat_rowid = self.dbstat.start(backup_name=self.backup_name,server_name=self.server_name,TYPE="BACKUP")
|
||||
else:
|
||||
stat_rowid = None
|
||||
|
||||
try:
|
||||
stats = {}
|
||||
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
|
||||
|
||||
if self.preexec.strip():
|
||||
exit_code = self.do_preexec(stats)
|
||||
if exit_code != 0 :
|
||||
raise Exception('Preexec "%s" failed with exit code "%i"' % (self.preexec,exit_code))
|
||||
|
||||
self.do_backup(stats)
|
||||
|
||||
if self.postexec.strip():
|
||||
exit_code = self.do_postexec(stats)
|
||||
if exit_code != 0 :
|
||||
raise Exception('Postexec "%s" failed with exit code "%i"' % (self.postexec,exit_code))
|
||||
|
||||
endtime = time.time()
|
||||
duration = (endtime-starttime)/3600.0
|
||||
if not self.dry_run and self.dbstat:
|
||||
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'])
|
||||
return stats
|
||||
|
||||
except BaseException, e:
|
||||
stats['status']='ERROR'
|
||||
stats['log']=str(e)
|
||||
endtime = time.time()
|
||||
duration = (endtime-starttime)/3600.0
|
||||
if not self.dry_run and self.dbstat:
|
||||
self.dbstat.finish(stat_rowid,
|
||||
backup_end=datetime2isodate(datetime.datetime.now()),
|
||||
backup_duration = duration,
|
||||
total_files_count=stats['total_files_count'],
|
||||
written_files_count=stats['written_files_count'],
|
||||
total_bytes=stats['total_bytes'],
|
||||
written_bytes=stats['written_bytes'],
|
||||
status=stats['status'],
|
||||
log=stats['log'],
|
||||
backup_location=stats['backup_location'])
|
||||
|
||||
self.logger.error('[%s] ######### Backup finished with ERROR: %s',self.backup_name,stats['log'])
|
||||
raise
|
||||
|
||||
|
||||
def checknagios(self,maxage_hours=30):
|
||||
"""
|
||||
Returns a tuple (nagiosstatus,message) for the current backup_name
|
||||
Read status from dbstat database
|
||||
"""
|
||||
if not self.dbstat:
|
||||
self.logger.warn('[%s] checknagios : no database provided',self.backup_name)
|
||||
return ('No database provided',nagiosStateUnknown)
|
||||
else:
|
||||
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,))
|
||||
if not q:
|
||||
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)
|
||||
else:
|
||||
mindate = datetime2isodate((datetime.datetime.now() - datetime.timedelta(hours=maxage_hours)))
|
||||
self.logger.debug('[%s] checknagios : looking for most recent OK not older than %s',self.backup_name,mindate)
|
||||
for b in q:
|
||||
if b['backup_end'] >= mindate and b['status'] == 'OK':
|
||||
# check if backup actually exists on registered backup location and is newer than backup start date
|
||||
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']))
|
||||
else:
|
||||
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']))
|
||||
else:
|
||||
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)
|
||||
for b in q:
|
||||
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']))
|
||||
|
||||
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 )
|
||||
|
||||
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 :
|
||||
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))
|
||||
removed = []
|
||||
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))
|
||||
if records:
|
||||
for oldbackup_location in [rec['backup_location'] for rec in records if rec['backup_location']]:
|
||||
try:
|
||||
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)
|
||||
if not self.dry_run:
|
||||
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)
|
||||
if not self.dry_run:
|
||||
os.remove(oldbackup_location)
|
||||
self.logger.debug('Cleanup_backup : Removing records from DB : [%s]-"%s"',self.backup_name,oldbackup_location)
|
||||
if not self.dry_run:
|
||||
self.dbstat.db.execute('update stats set TYPE="CLEAN" where backup_name=? and backup_location=?',(self.backup_name,oldbackup_location))
|
||||
self.dbstat.db.commit()
|
||||
except BaseException,e:
|
||||
self.logger.error('cleanup_backup : Unable to remove directory/file "%s". Error %s', oldbackup_location,e)
|
||||
removed.append((self.backup_name,oldbackup_location))
|
||||
else:
|
||||
self.logger.debug('[%s] cleanup : no result for query',self.backup_name)
|
||||
else:
|
||||
self.logger.info('Nothing to do because we want to keep at least one OK backup after cleaning')
|
||||
|
||||
self.logger.info('[%s] Cleanup finished : removed : %s' , self.backup_name,','.join([('[%s]-"%s"') % r for r in removed]) or 'Nothing')
|
||||
return removed
|
||||
|
||||
def register_existingbackups(self):
|
||||
"""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')
|
||||
|
||||
def export_latestbackup(self,destdir):
|
||||
"""Copy (rsync) latest OK backup to external storage located at locally mounted "destdir"
|
||||
"""
|
||||
stats = {}
|
||||
stats['total_files_count']=0
|
||||
stats['written_files_count']=0
|
||||
stats['total_bytes']=0
|
||||
stats['written_bytes']=0
|
||||
stats['log']=''
|
||||
stats['status']='Running'
|
||||
if not self.dbstat:
|
||||
self.logger.critical('[%s] export_latestbackup : no database provided',self.backup_name)
|
||||
raise Exception('No database')
|
||||
else:
|
||||
latest_sql = """\
|
||||
select status, backup_start, backup_end, log, backup_location, total_bytes
|
||||
from stats
|
||||
where backup_name=? and status='OK' and TYPE='BACKUP'
|
||||
order by backup_start desc limit 30"""
|
||||
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,))
|
||||
if not q:
|
||||
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)
|
||||
else:
|
||||
latest = q[0]
|
||||
backup_source = latest['backup_location']
|
||||
backup_dest = os.path.join(os.path.abspath(destdir),self.backup_name)
|
||||
if not os.path.exists(backup_source):
|
||||
raise Exception('Backup source %s doesn\'t exists' % backup_source)
|
||||
|
||||
# ensure there is a slash at end
|
||||
if os.path.isdir(backup_source) and backup_source[-1] <> '/':
|
||||
backup_source += '/'
|
||||
if backup_dest[-1] <> '/':
|
||||
backup_dest += '/'
|
||||
|
||||
if not os.path.isdir(backup_dest):
|
||||
os.makedirs(backup_dest)
|
||||
|
||||
options = ['-aP','--stats','--delete-excluded','--numeric-ids','--delete-after']
|
||||
if self.logger.level:
|
||||
options.append('-P')
|
||||
|
||||
if self.dry_run:
|
||||
options.append('-d')
|
||||
|
||||
options_params = " ".join(options)
|
||||
|
||||
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)
|
||||
|
||||
if not self.dry_run:
|
||||
self.line = ''
|
||||
starttime = time.time()
|
||||
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)
|
||||
def ondata(data,context):
|
||||
if context.verbose:
|
||||
print data
|
||||
context.logger.debug(data)
|
||||
|
||||
log = monitor_stdout(process,ondata,self)
|
||||
|
||||
for l in log.splitlines():
|
||||
if l.startswith('Number of files:'):
|
||||
stats['total_files_count'] += int(l.split(':')[1])
|
||||
if l.startswith('Number of files transferred:'):
|
||||
stats['written_files_count'] += int(l.split(':')[1])
|
||||
if l.startswith('Total file size:'):
|
||||
stats['total_bytes'] += int(l.split(':')[1].split()[0])
|
||||
if l.startswith('Total transferred file size:'):
|
||||
stats['written_bytes'] += int(l.split(':')[1].split()[0])
|
||||
returncode = process.returncode
|
||||
## deal with exit code 24 (file vanished)
|
||||
if (returncode == 24):
|
||||
self.logger.warning("[" + self.backup_name + "] Note: some files vanished before transfer")
|
||||
elif (returncode == 23):
|
||||
self.logger.warning("[" + self.backup_name + "] unable so set uid on some files")
|
||||
elif (returncode != 0):
|
||||
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:
|
||||
print cmd
|
||||
|
||||
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']))
|
||||
|
||||
endtime = time.time()
|
||||
duration = (endtime-starttime)/3600.0
|
||||
|
||||
if not self.dry_run and self.dbstat:
|
||||
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)
|
||||
return stats
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger = logging.getLogger('tisbackup')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
dbstat = BackupStat('/backup/data/log/tisbackup.sqlite')
|
||||
Executable
+224
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/python
|
||||
# -*- 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/>.
|
||||
#
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import datetime
|
||||
from common import *
|
||||
import XenAPI
|
||||
import time
|
||||
import logging
|
||||
import re
|
||||
import os.path
|
||||
import os
|
||||
import datetime
|
||||
import select
|
||||
import urllib2
|
||||
import base64
|
||||
import socket
|
||||
from stat import *
|
||||
|
||||
|
||||
class copy_vm_xcp(backup_generic):
|
||||
"""Backup a VM running on a XCP server on a second SR (requires xe tools and XenAPI)"""
|
||||
type = 'copy-vm-xcp'
|
||||
|
||||
required_params = backup_generic.required_params + ['server_name','storage_name','password_file','vm_name','network_name']
|
||||
optional_params = backup_generic.optional_params + ['start_vm','max_copies']
|
||||
|
||||
start_vm = "no"
|
||||
max_copies = 1
|
||||
|
||||
def read_config(self,iniconf):
|
||||
assert(isinstance(iniconf,ConfigParser))
|
||||
backup_generic.read_config(self,iniconf)
|
||||
if self.start_vm in 'no' and iniconf.has_option('global','start_vm'):
|
||||
self.start_vm = iniconf.get('global','start_vm')
|
||||
if self.max_copies == 1 and iniconf.has_option('global','max_copies'):
|
||||
self.max_copies = iniconf.getint('global','max_copies')
|
||||
|
||||
|
||||
def copy_vm_to_sr(self, vm_name, storage_name, dry_run):
|
||||
|
||||
user_xen, password_xen, null = open(self.password_file).read().split('\n')
|
||||
session = XenAPI.Session('https://'+self.server_name)
|
||||
try:
|
||||
session.login_with_password(user_xen,password_xen)
|
||||
except XenAPI.Failure, error:
|
||||
msg,ip = error.details
|
||||
|
||||
if msg == 'HOST_IS_SLAVE':
|
||||
server_name = ip
|
||||
session = XenAPI.Session('https://'+server_name)
|
||||
session.login_with_password(user_xen,password_xen)
|
||||
|
||||
|
||||
self.logger.debug("[%s] VM (%s) to backup in storage: %s",self.backup_name,vm_name,storage_name)
|
||||
now = datetime.datetime.now()
|
||||
|
||||
#get storage opaqueRef
|
||||
try:
|
||||
storage = session.xenapi.SR.get_by_name_label(storage_name)[0]
|
||||
except IndexError,error:
|
||||
return("error get storage opaqueref %s"%(error))
|
||||
|
||||
#get vm to copy opaqueRef
|
||||
try:
|
||||
vm = session.xenapi.VM.get_by_name_label(vm_name)[0]
|
||||
except IndexError,error:
|
||||
return("error get VM opaqueref %s"%(error))
|
||||
|
||||
#do the snapshot
|
||||
self.logger.debug("[%s] Snapshot in progress",self.backup_name)
|
||||
try:
|
||||
snapshot = session.xenapi.VM.snapshot(vm,"tisbackup-%s"%(vm_name))
|
||||
except XenAPI.Failure, error:
|
||||
return("error when snapshot %s"%(error))
|
||||
|
||||
#get snapshot opaqueRef
|
||||
snapshot = session.xenapi.VM.get_by_name_label("tisbackup-%s"%(vm_name))[0]
|
||||
session.xenapi.VM.set_name_description(snapshot,"snapshot created by tisbackup on : %s"%(now.strftime("%Y-%m-%d %H:%M")))
|
||||
|
||||
|
||||
|
||||
vm_backup_name = "zzz-%s-"%(vm_name)
|
||||
|
||||
|
||||
#Check if old backup exit
|
||||
list_backups = []
|
||||
for vm_ref in session.xenapi.VM.get_all():
|
||||
name_lablel = session.xenapi.VM.get_name_label(vm_ref)
|
||||
if vm_backup_name in name_lablel:
|
||||
list_backups.append(name_lablel)
|
||||
|
||||
list_backups.sort()
|
||||
|
||||
if len(list_backups) >= 1:
|
||||
|
||||
# Shutting last backup if started
|
||||
last_backup_vm = session.xenapi.VM.get_by_name_label(list_backups[-1])[0]
|
||||
if not "Halted" in session.xenapi.VM.get_power_state(last_backup_vm):
|
||||
self.logger.debug("[%s] Shutting down last backup vm : %s", self.backup_name, list_backups[-1] )
|
||||
session.xenapi.VM.hard_shutdown(last_backup_vm)
|
||||
|
||||
# Delete oldest backup if exist
|
||||
if len(list_backups) >= int(self.max_copies):
|
||||
for i in range(len(list_backups)-int(self.max_copies)+1):
|
||||
oldest_backup_vm = session.xenapi.VM.get_by_name_label(list_backups[i])[0]
|
||||
if not "Halted" in session.xenapi.VM.get_power_state(oldest_backup_vm):
|
||||
self.logger.debug("[%s] Shutting down old vm : %s", self.backup_name, list_backups[i] )
|
||||
session.xenapi.VM.hard_shutdown(oldest_backup_vm)
|
||||
|
||||
try:
|
||||
self.logger.debug("[%s] Deleting old vm : %s", self.backup_name, list_backups[i])
|
||||
for vbd in session.xenapi.VM.get_VBDs(oldest_backup_vm):
|
||||
vdi = session.xenapi.VBD.get_VDI(vbd)
|
||||
if not 'NULL' in vdi:
|
||||
session.xenapi.VDI.destroy(vdi)
|
||||
|
||||
session.xenapi.VM.destroy(oldest_backup_vm)
|
||||
except XenAPI.Failure, error:
|
||||
return("error when destroy old backup vm %s"%(error))
|
||||
|
||||
|
||||
self.logger.debug("[%s] Copy %s in progress on %s",self.backup_name,vm_name,storage_name)
|
||||
try:
|
||||
backup_vm = session.xenapi.VM.copy(snapshot,vm_backup_name+now.strftime("%Y-%m-%d %H:%M"),storage)
|
||||
except XenAPI.Failure, error:
|
||||
return("error when copy %s"%(error))
|
||||
|
||||
|
||||
# define VM as a template
|
||||
session.xenapi.VM.set_is_a_template(backup_vm,False)
|
||||
|
||||
#change the network of the new VM
|
||||
try:
|
||||
vifDestroy = session.xenapi.VM.get_VIFs(backup_vm)
|
||||
except IndexError,error:
|
||||
return("error get VIF opaqueref %s"%(error))
|
||||
|
||||
for i in vifDestroy:
|
||||
vifRecord = session.xenapi.VIF.get_record(i)
|
||||
session.xenapi.VIF.destroy(i)
|
||||
networkRef = session.xenapi.network.get_by_name_label(self.network_name)[0]
|
||||
data = {'MAC': vifRecord['MAC'],
|
||||
'MAC_autogenerated': False,
|
||||
'MTU': vifRecord['MTU'],
|
||||
'VM': backup_vm,
|
||||
'current_operations': vifRecord['current_operations'],
|
||||
'currently_attached': vifRecord['currently_attached'],
|
||||
'device': vifRecord['device'],
|
||||
'ipv4_allowed': vifRecord['ipv4_allowed'],
|
||||
'ipv6_allowed': vifRecord['ipv6_allowed'],
|
||||
'locking_mode': vifRecord['locking_mode'],
|
||||
'network': networkRef,
|
||||
'other_config': vifRecord['other_config'],
|
||||
'qos_algorithm_params': vifRecord['qos_algorithm_params'],
|
||||
'qos_algorithm_type': vifRecord['qos_algorithm_type'],
|
||||
'qos_supported_algorithms': vifRecord['qos_supported_algorithms'],
|
||||
'runtime_properties': vifRecord['runtime_properties'],
|
||||
'status_code': vifRecord['status_code'],
|
||||
'status_detail': vifRecord['status_detail']
|
||||
}
|
||||
try:
|
||||
session.xenapi.VIF.create(data)
|
||||
except Exception, error:
|
||||
return(error)
|
||||
|
||||
|
||||
if self.start_vm in ['true', '1', 't', 'y', 'yes', 'oui']:
|
||||
session.xenapi.VM.start(backup_vm,False,True)
|
||||
|
||||
session.xenapi.VM.set_name_description(backup_vm,"snapshot created by tisbackup on : %s"%(now.strftime("%Y-%m-%d %H:%M")))
|
||||
#delete the snapshot
|
||||
try:
|
||||
session.xenapi.VM.destroy(snapshot)
|
||||
except XenAPI.Failure, error:
|
||||
return("error when destroy snapshot %s"%(error))
|
||||
|
||||
return(0)
|
||||
|
||||
|
||||
def do_backup(self,stats):
|
||||
try:
|
||||
timestamp = int(time.time())
|
||||
cmd = self.copy_vm_to_sr(self.vm_name, self.storage_name, self.dry_run)
|
||||
if cmd == 0:
|
||||
timeExec = int(time.time()) - timestamp
|
||||
stats['log']='copy of %s to an other storage OK' % (self.backup_name)
|
||||
stats['status']='OK'
|
||||
stats['total_files_count'] = 1
|
||||
stats['backup_location'] = self.storage_name
|
||||
else:
|
||||
stats['status']='ERROR'
|
||||
stats['log']=cmd
|
||||
|
||||
except BaseException,e:
|
||||
stats['status']='ERROR'
|
||||
stats['log']=str(e)
|
||||
raise
|
||||
|
||||
|
||||
|
||||
register_driver(copy_vm_xcp)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user