refactor(drivers): organize backup modules into drivers subfolder
lint / docker (push) Has been cancelled

- Move all backup_*.py files to libtisbackup/drivers/ subdirectory
- Move XenAPI.py and copy_vm_xcp.py to drivers/ (driver-specific)
- Create drivers/__init__.py with automatic driver imports
- Update tisbackup.py imports to use new structure
- Add pyvmomi>=8.0.0 as mandatory dependency
- Sync requirements.txt with pyproject.toml dependencies
- Add pylint>=3.0.0 and pytest-cov>=6.0.0 to dev dependencies
- Configure pylint and coverage tools in pyproject.toml
- Add conventional commits guidelines to CLAUDE.md
- Enhance .gitignore with comprehensive patterns for Python, IDEs, testing, and secrets
- Update CLAUDE.md documentation with new structure and tooling

Breaking Changes:
- Drivers must now be imported from libtisbackup.drivers instead of libtisbackup
- All backup driver files relocated to drivers/ subdirectory

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-05 23:54:26 +02:00
parent 38a0d788d4
commit 1cb731cbdb
33 changed files with 2519 additions and 634 deletions
+264
View File
@@ -0,0 +1,264 @@
# Copyright (c) Citrix Systems, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1) Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2) Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# --------------------------------------------------------------------
# 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 socket
import sys
import six.moves.http_client as httplib
import six.moves.xmlrpc_client as xmlrpclib
translation = gettext.translation("xen-xm", fallback=True)
API_VERSION_1_1 = "1.1"
API_VERSION_1_2 = "1.2"
class Failure(Exception):
def __init__(self, details):
self.details = details
def __str__(self):
try:
return str(self.details)
except Exception as exn:
msg = "Xen-API failure: %s" % exn
sys.stderr.write(msg)
return msg
def _details_map(self):
return dict([(str(i), self.details[i]) for i in range(len(self.details))])
# Just a "constant" that we use to decide whether to retry the RPC
_RECONNECT_AND_RETRY = object()
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.HTTPConnection):
_connection_class = UDSHTTPConnection
class UDSTransport(xmlrpclib.Transport):
def __init__(self, use_datetime=0):
self._use_datetime = use_datetime
self._extra_headers = []
self._connection = (None, None)
def add_extra_header(self, key, value):
self._extra_headers += [(key, value)]
def make_connection(self, host):
# Python 2.4 compatibility
if sys.version_info[0] <= 2 and sys.version_info[1] < 7:
return UDSHTTP(host)
else:
return UDSHTTPConnection(host)
def send_request(self, connection, handler, request_body):
connection.putrequest("POST", handler)
for key, value in self._extra_headers:
connection.putheader(key, value)
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', '1.0', 'xen-api-scripts-xenapi.py')
session.xenapi.VM.start(vm_uuid)
session.xenapi.session.logout()
"""
def __init__(self, uri, transport=None, encoding=None, verbose=0, allow_none=1, ignore_ssl=False):
# Fix for CA-172901 (+ Python 2.4 compatibility)
# Fix for context=ctx ( < Python 2.7.9 compatibility)
if not (sys.version_info[0] <= 2 and sys.version_info[1] <= 7 and sys.version_info[2] <= 9) and ignore_ssl:
import ssl
ctx = ssl._create_unverified_context()
xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding, verbose, allow_none, context=ctx)
else:
xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding, verbose, allow_none)
self.transport = transport
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" or methodname == "session.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 is _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):
try:
result = _parse_result(getattr(self, "session.%s" % method)(*params))
if result is _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
self.API_version = self._get_api_version()
except socket.error as e:
if e.errno == socket.errno.ETIMEDOUT:
raise xmlrpclib.Fault(504, "The connection timed out")
else:
raise e
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)
major = self.xenapi.host.get_API_version_major(host)
minor = self.xenapi.host.get_API_version_minor(host)
return "%s.%s" % (major, minor)
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)
elif name == "logout":
return _Dispatcher(self.API_version, self.xenapi_request, "logout")
else:
return xmlrpclib.ServerProxy.__getattr__(self, name)
def xapi_local():
return Session("http://_var_lib_xcp_xapi/", transport=UDSTransport())
def _parse_result(result):
if not isinstance(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):
return self.__send(self.__name, args)
+60
View File
@@ -0,0 +1,60 @@
# -----------------------------------------------------------------------
# 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/>.
#
# -----------------------------------------------------------------------
"""
TISBackup drivers - Pluggable backup driver implementations.
This package contains all backup driver implementations:
- Database drivers (MySQL, PostgreSQL, Oracle, SQL Server)
- File sync drivers (rsync, rsync+btrfs)
- VM backup drivers (XenServer XVA, VMware VMDK)
- Other drivers (Samba4, network switches, etc.)
"""
# Import all drivers to ensure they register themselves
from .backup_mysql import backup_mysql
from .backup_null import backup_null
from .backup_oracle import backup_oracle
from .backup_pgsql import backup_pgsql
from .backup_rsync import backup_rsync, backup_rsync_ssh
from .backup_rsync_btrfs import backup_rsync_btrfs, backup_rsync__btrfs_ssh
from .backup_samba4 import backup_samba4
from .backup_sqlserver import backup_sqlserver
from .backup_switch import backup_switch
from .backup_vmdk import backup_vmdk
from .backup_xcp_metadata import backup_xcp_metadata
from .backup_xva import backup_xva
from .copy_vm_xcp import copy_vm_xcp
__all__ = [
"backup_mysql",
"backup_null",
"backup_oracle",
"backup_pgsql",
"backup_rsync",
"backup_rsync_ssh",
"backup_rsync_btrfs",
"backup_rsync__btrfs_ssh",
"backup_samba4",
"backup_sqlserver",
"backup_switch",
"backup_vmdk",
"backup_xcp_metadata",
"backup_xva",
"copy_vm_xcp",
]
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/python3
# -*- 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 as e:
print(("Error : can not load paramiko library %s" % e))
raise
sys.stderr = sys.__stderr__
from libtisbackup 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_user", "db_passwd", "private_key"]
optional_params = backup_generic.optional_params + ["db_name"]
db_name = ""
db_user = ""
db_passwd = ""
dest_dir = ""
def do_backup(self, stats):
self.dest_dir = os.path.join(self.backup_dir, self.backup_start_date)
if not os.path.isdir(self.dest_dir):
if not self.dry_run:
os.makedirs(self.dest_dir)
else:
print(('mkdir "%s"' % self.dest_dir))
else:
raise Exception("backup destination directory already exists : %s" % self.dest_dir)
self.logger.debug("[%s] Connecting to %s with user root and key %s", self.backup_name, self.server_name, self.private_key)
mykey = load_ssh_private_key(self.private_key)
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh.connect(self.server_name, username="root", pkey=mykey, port=self.ssh_port)
self.db_passwd = self.db_passwd.replace("$", r"\$")
if not self.db_name:
stats["log"] = "Successfully backuping processed to the following databases :"
stats["status"] = "List"
cmd = 'mysql -N -B -p -e "SHOW DATABASES;" -u ' + self.db_user + " -p" + self.db_passwd + " 2> /dev/null"
self.logger.debug("[%s] List databases: %s", self.backup_name, cmd)
(error_code, output) = ssh_exec(cmd, ssh=self.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))
databases = output.split("\n")
for database in databases:
if database != "":
self.db_name = database.rstrip()
self.do_mysqldump(stats)
else:
stats["log"] = "Successfully backup processed to the following database :"
self.do_mysqldump(stats)
def do_mysqldump(self, stats):
t = datetime.datetime.now()
backup_start_date = t.strftime("%Y%m%d-%Hh%Mm%S")
# dump db
stats["status"] = "Dumping"
cmd = (
"mysqldump --single-transaction -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=self.ssh)
print(output)
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=self.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.dest_dir, self.db_name + ".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 = self.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.get("total_files_count", 0)
stats["written_files_count"] = 1 + stats.get("written_files_count", 0)
stats["total_bytes"] = os.stat(localpath).st_size + stats.get("total_bytes", 0)
stats["written_bytes"] = os.stat(localpath).st_size + stats.get("written_bytes", 0)
stats["log"] = '%s "%s"' % (stats["log"], self.db_name)
stats["backup_location"] = self.dest_dir
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=self.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(r"^\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 dir_name not 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)
register_driver(backup_mysql)
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/python3
# -*- 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 datetime
import os
from libtisbackup 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 register_existingbackups(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
+191
View File
@@ -0,0 +1,191 @@
#!/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 as e:
print(("Error : can not load paramiko library %s" % e))
raise
sys.stderr = sys.__stderr__
import base64
import datetime
import os
import re
from libtisbackup import *
class backup_oracle(backup_generic):
"""Backup a oracle database as zipped file through ssh"""
type = "oracle+ssh"
required_params = backup_generic.required_params + ["db_name", "private_key", "userid"]
optional_params = ["username", "remote_backup_dir", "ignore_error_oracle_code"]
db_name = ""
username = "oracle"
remote_backup_dir = r"/home/oracle/backup"
ignore_error_oracle_code = []
def do_backup(self, stats):
self.logger.debug(
"[%s] Connecting to %s with user %s and key %s", self.backup_name, self.server_name, self.username, self.private_key
)
mykey = load_ssh_private_key(self.private_key)
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh.connect(self.server_name, username=self.username, pkey=mykey, port=self.ssh_port)
t = datetime.datetime.now()
self.backup_start_date = t.strftime("%Y%m%d-%Hh%Mm%S")
dumpfile = self.remote_backup_dir + "/" + self.db_name + "_" + self.backup_start_date + ".dmp"
dumplog = self.remote_backup_dir + "/" + self.db_name + "_" + self.backup_start_date + ".log"
self.dest_dir = os.path.join(self.backup_dir, self.backup_start_date)
if not os.path.isdir(self.dest_dir):
if not self.dry_run:
os.makedirs(self.dest_dir)
else:
print(('mkdir "%s"' % self.dest_dir))
else:
raise Exception("backup destination directory already exists : %s" % self.dest_dir)
# dump db
stats["status"] = "Dumping"
cmd = "exp '%s' file='%s' grants=y log='%s'" % (self.userid, dumpfile, dumplog)
self.logger.debug("[%s] Dump DB : %s", self.backup_name, cmd)
if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh)
self.logger.debug("[%s] Output of %s :\n%s", self.backup_name, cmd, output)
if error_code:
localpath = os.path.join(self.dest_dir, self.db_name + ".log")
self.logger.debug("[%s] Get log file with sftp on %s from %s to %s", self.backup_name, self.server_name, dumplog, localpath)
transport = self.ssh.get_transport()
sftp = paramiko.SFTPClient.from_transport(transport)
sftp.get(dumplog, localpath)
sftp.close()
file = open(localpath)
for line in file:
if (
re.search("EXP-[0-9]+:", line)
and re.match("EXP-[0-9]+:", line).group(0).replace(":", "") not in self.ignore_error_oracle_code
):
stats["status"] = "RMTemp"
self.clean_dumpfiles(dumpfile, dumplog)
raise Exception(
'Aborting, Not null exit code (%s) for "%s"' % (re.match("EXP-[0-9]+:", line).group(0).replace(":", ""), cmd)
)
file.close()
# zip the file
stats["status"] = "Zipping"
cmd = "gzip %s" % dumpfile
self.logger.debug("[%s] Compress backup : %s", self.backup_name, cmd)
if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.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 = dumpfile + ".gz"
localpath = os.path.join(self.dest_dir, self.db_name + ".dmp.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 = self.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"] = self.dest_dir
stats["status"] = "RMTemp"
self.clean_dumpfiles(dumpfile, dumplog)
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(r"^\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 dir_name not 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 clean_dumpfiles(self, dumpfile, dumplog):
cmd = 'rm -f "%s.gz" "%s"' % (dumpfile, dumplog)
self.logger.debug("[%s] Remove temp gzip : %s", self.backup_name, cmd)
if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.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))
cmd = "rm -f " + self.remote_backup_dir + "/" + self.db_name + "_" + self.backup_start_date + ".dmp"
self.logger.debug("[%s] Remove temp dump : %s", self.backup_name, cmd)
if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.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))
register_driver(backup_oracle)
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/python3
# -*- 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 as e:
print(("Error : can not load paramiko library %s" % e))
raise
sys.stderr = sys.__stderr__
from libtisbackup 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 + ["private_key"]
optional_params = backup_generic.optional_params + ["db_name", "tmp_dir", "encoding"]
db_name = ""
tmp_dir = "/tmp"
encoding = "UTF8"
def do_backup(self, stats):
self.dest_dir = os.path.join(self.backup_dir, self.backup_start_date)
if not os.path.isdir(self.dest_dir):
if not self.dry_run:
os.makedirs(self.dest_dir)
else:
print(('mkdir "%s"' % self.dest_dir))
else:
raise Exception("backup destination directory already exists : %s" % self.dest_dir)
mykey = load_ssh_private_key(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
)
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh.connect(self.server_name, username="root", pkey=mykey, port=self.ssh_port)
if self.db_name:
stats["log"] = "Successfully backup processed to the following database :"
self.do_pgsqldump(stats)
else:
stats["log"] = "Successfully backuping processed to the following databases :"
stats["status"] = "List"
cmd = """su - postgres -c 'psql -A -t -c "SELECT datname FROM pg_database WHERE datistemplate = false;"' 2> /dev/null"""
self.logger.debug("[%s] List databases: %s", self.backup_name, cmd)
(error_code, output) = ssh_exec(cmd, ssh=self.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))
databases = output.split("\n")
for database in databases:
if database.strip() not in ("", "template0", "template1"):
self.db_name = database.strip()
self.do_pgsqldump(stats)
stats["status"] = "OK"
def do_pgsqldump(self, stats):
t = datetime.datetime.now()
backup_start_date = t.strftime("%Y%m%d-%Hh%Mm%S")
params = {
"encoding": self.encoding,
"db_name": self.db_name,
"tmp_dir": self.tmp_dir,
"dest_dir": self.dest_dir,
"backup_start_date": backup_start_date,
}
# dump db
filepath = "%(tmp_dir)s/%(db_name)s-%(backup_start_date)s.sql.gz" % params
cmd = "su - postgres -c 'pg_dump -E %(encoding)s -Z9 %(db_name)s'" % params
cmd += " > " + filepath
self.logger.debug("[%s] %s ", self.backup_name, cmd)
if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.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
localpath = "%(dest_dir)s/%(db_name)s-%(backup_start_date)s.sql.gz" % params
self.logger.debug('[%s] get the file using sftp from "%s" to "%s" ', self.backup_name, filepath, localpath)
if not self.dry_run:
transport = self.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.get("total_files_count", 0)
stats["written_files_count"] = 1 + stats.get("written_files_count", 0)
stats["total_bytes"] = os.stat(localpath).st_size + stats.get("total_bytes", 0)
stats["written_bytes"] = os.stat(localpath).st_size + stats.get("written_bytes", 0)
stats["log"] = '%s "%s"' % (stats["log"], self.db_name)
stats["backup_location"] = self.dest_dir
cmd = "rm -f %(tmp_dir)s/%(db_name)s-%(backup_start_date)s.sql.gz" % params
self.logger.debug("[%s] %s ", self.backup_name, cmd)
if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.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))
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(r"^\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 dir_name not 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)
register_driver(backup_pgsql)
+378
View File
@@ -0,0 +1,378 @@
#!/usr/bin/python3
# -*- 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 datetime
import logging
import os
import os.path
import re
import time
from libtisbackup import *
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 is not None:
options.append(self.overload_args)
elif "cygdrive" not 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 self.protect_args.lower() not 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 as 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(r'(?P<server>[^:]*)::(?P<export>[^/]*)/(?P<path>.*)')
# ssh_re = re.compile(r'((?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"]
ssh_params.append("-o BatchMode=yes")
if self.private_key:
ssh_params.append("-i %s" % self.private_key)
if self.cipher_spec:
ssh_params.append("-c %s" % self.cipher_spec)
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)
reg_total_files = re.compile(r"Number of files: (?P<file>\d+)")
reg_transferred_files = re.compile(r"Number of .*files transferred: (?P<file>\d+)")
for l in log.splitlines():
line = l.replace(",", "")
m = reg_total_files.match(line)
if m:
stats["total_files_count"] += int(m.groupdict()["file"])
m = reg_transferred_files.match(line)
if m:
stats["written_files_count"] += int(m.groupdict()["file"])
if line.startswith("Total file size:"):
stats["total_bytes"] += int(line.split(":")[1].split()[0])
if line.startswith("Total transferred file size:"):
stats["written_bytes"] += int(line.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 " + str(returncode))
raise Exception(
"[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd, log[-512:]
)
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 as 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(r"^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$")
r_partial = re.compile(r"^\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(r"^\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 dir_name not 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 not self.is_pid_still_running(self.backup_dir + "/lock"):
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",
"cipher_spec",
]
cipher_spec = ""
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()))
+395
View File
@@ -0,0 +1,395 @@
#!/usr/bin/python3
# -*- 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 datetime
import logging
import os
import os.path
import re
import time
from libtisbackup import *
class backup_rsync_btrfs(backup_generic):
"""Backup a directory on remote server with rsync and btrfs protocol (requires running remote rsync daemon)"""
type = "rsync+btrfs"
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, "last_backup")
if not os.path.isdir(dest_dir):
if not self.dry_run:
cmd = "/bin/btrfs subvolume create %s" % dest_dir
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
log = monitor_stdout(process, "", self)
returncode = process.returncode
if returncode != 0:
self.logger.error("[" + self.backup_name + "] shell program exited with error code: %s" % log)
raise Exception("[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd)
else:
self.logger.info("[" + self.backup_name + "] create btrs volume: %s" % dest_dir)
else:
print(('btrfs subvolume create "%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 is not None:
options.append(self.overload_args)
elif "cygdrive" not 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 self.protect_args.lower() not 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)
# remove link-dest replace by btrfs
# 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 as 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(r'(?P<server>[^:]*)::(?P<export>[^/]*)/(?P<path>.*)')
# ssh_re = re.compile(r'((?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"]
if self.private_key:
ssh_params.append("-i %s" % self.private_key)
if self.cipher_spec:
ssh_params.append("-c %s" % self.cipher_spec)
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)
reg_total_files = re.compile(r"Number of files: (?P<file>\d+)")
reg_transferred_files = re.compile(r"Number of .*files transferred: (?P<file>\d+)")
for l in log.splitlines():
line = l.replace(",", "")
m = reg_total_files.match(line)
if m:
stats["total_files_count"] += int(m.groupdict()["file"])
m = reg_transferred_files.match(line)
if m:
stats["written_files_count"] += int(m.groupdict()["file"])
if line.startswith("Total file size:"):
stats["total_bytes"] += int(line.split(":")[1].split()[0])
if line.startswith("Total transferred file size:"):
stats["written_bytes"] += int(line.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 ", str(returncode))
raise Exception(
"[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd, log[-512:]
)
else:
print(cmd)
# we take a snapshot of last_backup if everything went well
finaldest = os.path.join(self.backup_dir, self.backup_start_date)
self.logger.debug("[%s] snapshoting last_backup directory from %s to %s", self.backup_name, dest_dir, finaldest)
if not os.path.isdir(finaldest):
if not self.dry_run:
cmd = "/bin/btrfs subvolume snapshot %s %s" % (dest_dir, finaldest)
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
log = monitor_stdout(process, "", self)
returncode = process.returncode
if returncode != 0:
self.logger.error("[" + self.backup_name + "] shell program exited with error code " + str(returncode))
raise Exception(
"[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd, log[-512:]
)
else:
self.logger.info("[" + self.backup_name + "] snapshot directory created %s" % finaldest)
else:
print(("btrfs snapshot of %s to %s" % (dest_dir, finaldest)))
else:
raise Exception("snapshot directory already exists : %s" % finaldest)
self.logger.debug("[%s] touching datetime of target directory %s", self.backup_name, finaldest)
print((os.popen('touch "%s"' % finaldest).read()))
stats["backup_location"] = finaldest
stats["status"] = "OK"
stats["log"] = "ssh+rsync+btrfs backup from %s OK, %d bytes written for %d changed files" % (
backup_source,
stats["written_bytes"],
stats["written_files_count"],
)
except BaseException as 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(r"^\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}$")
r_partial = re.compile(r"^\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(r"^\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 dir_name not 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 not self.is_pid_still_running(self.backup_dir + "/lock"):
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__btrfs_ssh(backup_rsync_btrfs):
"""Backup a directory on remote server with rsync,ssh and btrfs protocol (requires rsync software on remote host)"""
type = "rsync+btrfs+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",
"cipher_spec",
]
cipher_spec = ""
register_driver(backup_rsync_btrfs)
register_driver(backup_rsync__btrfs_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()))
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/python3
# -*- 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 as e:
print("Error : can not load paramiko library %s" % e)
raise
sys.stderr = sys.__stderr__
from libtisbackup import *
class backup_samba4(backup_generic):
"""Backup a samba4 databases as gzipped tdbs file through ssh"""
type = "samba4"
required_params = backup_generic.required_params + ["private_key"]
optional_params = backup_generic.optional_params + ["root_dir_samba"]
root_dir_samba = "/var/lib/samba/"
def do_backup(self, stats):
self.dest_dir = os.path.join(self.backup_dir, self.backup_start_date)
if not os.path.isdir(self.dest_dir):
if not self.dry_run:
os.makedirs(self.dest_dir)
else:
print('mkdir "%s"' % self.dest_dir)
else:
raise Exception("backup destination directory already exists : %s" % self.dest_dir)
self.logger.debug("[%s] Connecting to %s with user root and key %s", self.backup_name, self.server_name, self.private_key)
mykey = load_ssh_private_key(self.private_key)
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh.connect(self.server_name, username="root", pkey=mykey, port=self.ssh_port)
stats["log"] = "Successfully backuping processed to the following databases :"
stats["status"] = "List"
dir_ldbs = os.path.join(self.root_dir_samba + "/private/sam.ldb.d/")
cmd = "ls %s/*.ldb 2> /dev/null" % dir_ldbs
self.logger.debug("[%s] List databases: %s", self.backup_name, cmd)
(error_code, output) = ssh_exec(cmd, ssh=self.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))
databases = output.split("\n")
for database in databases:
if database != "":
self.db_name = database.rstrip()
self.do_mysqldump(stats)
def do_mysqldump(self, stats):
# t = datetime.datetime.now()
# backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S')
# dump db
stats["status"] = "Dumping"
cmd = "tdbbackup -s .tisbackup " + self.db_name
self.logger.debug("[%s] Dump DB : %s", self.backup_name, cmd)
if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.ssh)
print(output)
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 -f "%s.tisbackup"' % self.db_name
self.logger.debug("[%s] Compress backup : %s", self.backup_name, cmd)
if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.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 = self.db_name + ".tisbackup.gz"
localpath = os.path.join(self.dest_dir, os.path.basename(self.db_name) + ".tisbackup.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 = self.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.get("total_files_count", 0)
stats["written_files_count"] = 1 + stats.get("written_files_count", 0)
stats["total_bytes"] = os.stat(localpath).st_size + stats.get("total_bytes", 0)
stats["written_bytes"] = os.stat(localpath).st_size + stats.get("written_bytes", 0)
stats["log"] = '%s "%s"' % (stats["log"], self.db_name)
stats["backup_location"] = self.dest_dir
stats["status"] = "RMTemp"
cmd = 'rm -f "%s"' % filepath
self.logger.debug("[%s] Remove temp gzip : %s", self.backup_name, cmd)
if not self.dry_run:
(error_code, output) = ssh_exec(cmd, ssh=self.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(r"^\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 dir_name not 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)
register_driver(backup_samba4)
+171
View File
@@ -0,0 +1,171 @@
#!/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 as e:
print("Error : can not load paramiko library %s" % e)
raise
sys.stderr = sys.__stderr__
import base64
import datetime
import os
from libtisbackup import *
class backup_sqlserver(backup_generic):
"""Backup a SQLSERVER database as gzipped sql file through ssh"""
type = "sqlserver+ssh"
required_params = backup_generic.required_params + ["db_name", "private_key"]
optional_params = ["username", "remote_backup_dir", "sqlserver_before_2005", "db_server_name", "db_user", "db_password"]
db_name = ""
db_user = ""
db_password = ""
userdb = "-E"
username = "Administrateur"
remote_backup_dir = r"c:/WINDOWS/Temp/"
sqlserver_before_2005 = False
db_server_name = "localhost"
def do_backup(self, stats):
mykey = load_ssh_private_key(self.private_key)
self.logger.debug("[%s] Connecting to %s with user 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=self.username, pkey=mykey, port=self.ssh_port)
t = datetime.datetime.now()
backup_start_date = t.strftime("%Y%m%d-%Hh%Mm%S")
backup_file = self.remote_backup_dir + "/" + self.db_name + "-" + backup_start_date + ".bak"
if not self.db_user == "":
self.userdb = "-U %s -P %s" % (self.db_user, self.db_password)
# dump db
stats["status"] = "Dumping"
if self.sqlserver_before_2005:
cmd = """osql -E -Q "BACKUP DATABASE [%s]
TO DISK='%s'
WITH FORMAT" """ % (self.db_name, backup_file)
else:
cmd = """sqlcmd %s -S "%s" -d master -Q "BACKUP DATABASE [%s]
TO DISK = N'%s'
WITH INIT, NOUNLOAD ,
NAME = N'Backup %s', NOSKIP ,STATS = 10, NOFORMAT" """ % (
self.userdb,
self.db_server_name,
self.db_name,
backup_file,
self.db_name,
)
self.logger.debug("[%s] Dump DB : %s", self.backup_name, cmd)
try:
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 "%s"' % backup_file
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 = backup_file + ".gz"
localpath = os.path.join(self.backup_dir, self.db_name + "-" + backup_start_date + ".bak.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
finally:
stats["status"] = "RMTemp"
cmd = 'rm -f "%s" "%s"' % (backup_file + ".gz", backup_file)
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(r"^%s-(?P<date>\d{8,8}-\d{2,2}h\d{2,2}m\d{2,2}).bak.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 file_name not 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_sqlserver)
+267
View File
@@ -0,0 +1,267 @@
#!/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 base64
import datetime
import logging
import os
import os.path
import re
import select
import socket
import time
import urllib.error
import urllib.parse
import urllib.request
from stat import *
import pexpect
import requests
from . import XenAPI
from libtisbackup import *
class backup_switch(backup_generic):
"""Backup a startup-config on a switch"""
type = "switch"
required_params = backup_generic.required_params + ["switch_ip", "switch_type"]
optional_params = backup_generic.optional_params + ["switch_user", "switch_password"]
switch_user = ""
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_cisco(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.expect("Password: ")
child.sendline(self.switch_password + "\r")
try:
child.expect(">")
except:
raise Exception("Bad Credentials")
child.sendline("enable\r")
child.expect("Password: ")
child.sendline(self.switch_password + "\r")
try:
child.expect("#")
except:
raise Exception("Bad Credentials")
child.sendline("terminal length 0\r")
child.expect("#")
child.sendline("show run\r")
child.expect("Building configuration...")
child.expect("#")
running_config = child.before
child.sendline("show vlan\r")
child.expect("VLAN")
child.expect("#")
vlan = "VLAN" + child.before
open(filename, "a").write(running_config + "\n" + vlan)
child.send("exit\r")
child.close()
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 = {"Login": self.switch_user, "Password": self.switch_password, "sellanId": 0, "sellan": 0, "lang_seqid": 1}
resp = requests.post(
"http://%s/form/formLoginApply" % self.switch_ip,
data=login_data,
headers={"Referer": "http://%s/www/login.html" % self.switch_ip},
)
if "Wrong password" in resp.text:
raise Exception("Wrong password")
resp = requests.post(
"http://%s/BinFile/config.bin" % self.switch_ip, headers={"Referer": "http://%s/www/iss/013_download_cfg.html" % self.switch_ip}
)
with open(filename, "w") as f:
f.write(resp.content)
def switch_dlink_DGS1510(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.expect("Password:")
child.sendline(self.switch_password + "\r")
try:
child.expect("#")
except:
raise Exception("Bad Credentials")
child.sendline("terminal length 0\r")
child.expect("#")
child.sendline("show run\r")
child.logfile_read = open(filename, "a")
child.expect("End of configuration file")
child.expect("#--")
child.expect("#")
child.close()
myre = re.compile(r"#--+")
config = myre.split(open(filename).read())[2]
with open(filename, "w") as f:
f.write(config)
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 [
"CISCO",
]:
dest_filename += ".txt"
self.switch_cisco(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)
elif "DLINK-DGS1510" == self.switch_type:
dest_filename += ".cfg"
self.switch_dlink_DGS1510(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 as 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)
+270
View File
@@ -0,0 +1,270 @@
#!/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 atexit
import getpass
from datetime import date, datetime, timedelta
import pyVmomi
import requests
from pyVim.connect import Disconnect, SmartConnect
from pyVmomi import vim, vmodl
# Disable HTTPS verification warnings.
from requests.packages import urllib3
from libtisbackup import *
urllib3.disable_warnings()
import os
import re
import tarfile
import time
import xml.etree.ElementTree as ET
from stat import *
class backup_vmdk(backup_generic):
type = "esx-vmdk"
required_params = backup_generic.required_params + ["esxhost", "password_file", "server_name"]
optional_params = backup_generic.optional_params + ["esx_port", "prefix_clone", "create_ovafile", "halt_vm"]
esx_port = 443
prefix_clone = "clone-"
create_ovafile = "no"
halt_vm = "no"
def make_compatible_cookie(self, client_cookie):
cookie_name = client_cookie.split("=", 1)[0]
cookie_value = client_cookie.split("=", 1)[1].split(";", 1)[0]
cookie_path = client_cookie.split("=", 1)[1].split(";", 1)[1].split(";", 1)[0].lstrip()
cookie_text = " " + cookie_value + "; $" + cookie_path
# Make a cookie
cookie = dict()
cookie[cookie_name] = cookie_text
return cookie
def download_file(self, url, local_filename, cookie, headers):
r = requests.get(url, stream=True, headers=headers, cookies=cookie, verify=False)
with open(local_filename, "wb") as f:
for chunk in r.iter_content(chunk_size=1024 * 1024 * 64):
if chunk:
f.write(chunk)
f.flush()
return local_filename
def export_vmdks(self, vm):
HttpNfcLease = vm.ExportVm()
try:
infos = HttpNfcLease.info
device_urls = infos.deviceUrl
vmdks = []
for device_url in device_urls:
deviceId = device_url.key
deviceUrlStr = device_url.url
diskFileName = vm.name.replace(self.prefix_clone, "") + "-" + device_url.targetId
diskUrlStr = deviceUrlStr.replace("*", self.esxhost)
# diskLocalPath = './' + diskFileName
cookie = self.make_compatible_cookie(si._stub.cookie)
headers = {"Content-Type": "application/octet-stream"}
self.logger.debug("[%s] exporting disk: %s" % (self.server_name, diskFileName))
self.download_file(diskUrlStr, diskFileName, cookie, headers)
vmdks.append({"filename": diskFileName, "id": deviceId})
finally:
HttpNfcLease.Complete()
return vmdks
def create_ovf(self, vm, vmdks):
ovfDescParams = vim.OvfManager.CreateDescriptorParams()
ovf = si.content.ovfManager.CreateDescriptor(vm, ovfDescParams)
root = ET.fromstring(ovf.ovfDescriptor)
new_id = list(root[0][1].attrib.values())[0][1:3]
ovfFiles = []
for vmdk in vmdks:
old_id = vmdk["id"][1:3]
id = vmdk["id"].replace(old_id, new_id)
ovfFiles.append(vim.OvfManager.OvfFile(size=os.path.getsize(vmdk["filename"]), path=vmdk["filename"], deviceId=id))
ovfDescParams = vim.OvfManager.CreateDescriptorParams()
ovfDescParams.ovfFiles = ovfFiles
ovf = si.content.ovfManager.CreateDescriptor(vm, ovfDescParams)
ovf_filename = vm.name + ".ovf"
self.logger.debug("[%s] creating ovf file: %s" % (self.server_name, ovf_filename))
with open(ovf_filename, "w") as f:
f.write(ovf.ovfDescriptor)
return ovf_filename
def create_ova(self, vm, vmdks, ovf_filename):
ova_filename = vm.name + ".ova"
vmdks.insert(0, {"filename": ovf_filename, "id": "false"})
self.logger.debug("[%s] creating ova file: %s" % (self.server_name, ova_filename))
with tarfile.open(ova_filename, "w") as tar:
for vmdk in vmdks:
tar.add(vmdk["filename"])
os.unlink(vmdk["filename"])
return ova_filename
def clone_vm(self, vm):
task = self.wait_task(
vm.CreateSnapshot_Task(
name="backup", description="Automatic backup " + datetime.now().strftime("%Y-%m-%d %H:%M:%s"), memory=False, quiesce=True
)
)
snapshot = task.info.result
prefix_vmclone = self.prefix_clone
clone_name = prefix_vmclone + vm.name
datastore = "[%s]" % vm.datastore[0].name
vmx_file = vim.vm.FileInfo(logDirectory=None, snapshotDirectory=None, suspendDirectory=None, vmPathName=datastore)
config = vim.vm.ConfigSpec(
name=clone_name, memoryMB=vm.summary.config.memorySizeMB, numCPUs=vm.summary.config.numCpu, files=vmx_file
)
hosts = datacenter.hostFolder.childEntity
resource_pool = hosts[0].resourcePool
self.wait_task(vmFolder.CreateVM_Task(config=config, pool=resource_pool))
new_vm = [x for x in vmFolder.childEntity if x.name == clone_name][0]
controller = vim.vm.device.VirtualDeviceSpec()
controller.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
controller.device = vim.vm.device.VirtualLsiLogicController(busNumber=0, sharedBus="noSharing")
controller.device.key = 0
i = 0
vm_devices = []
clone_folder = "%s/" % "/".join(new_vm.summary.config.vmPathName.split("/")[:-1])
for device in vm.config.hardware.device:
if device.__class__.__name__ == "vim.vm.device.VirtualDisk":
cur_vers = int(re.findall(r"\d{3,6}", device.backing.fileName)[0])
if cur_vers == 1:
source = device.backing.fileName.replace("-000001", "")
else:
source = device.backing.fileName.replace("%d." % cur_vers, "%d." % (cur_vers - 1))
dest = clone_folder + source.split("/")[-1]
disk_spec = vim.VirtualDiskManager.VirtualDiskSpec(diskType="sparseMonolithic", adapterType="ide")
self.wait_task(si.content.virtualDiskManager.CopyVirtualDisk_Task(sourceName=source, destName=dest, destSpec=disk_spec))
# self.wait_task(si.content.virtualDiskManager.ShrinkVirtualDisk_Task(dest))
diskfileBacking = vim.vm.device.VirtualDisk.FlatVer2BackingInfo()
diskfileBacking.fileName = dest
diskfileBacking.diskMode = "persistent"
vdisk_spec = vim.vm.device.VirtualDeviceSpec()
vdisk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
vdisk_spec.device = vim.vm.device.VirtualDisk(capacityInKB=10000, controllerKey=controller.device.key)
vdisk_spec.device.key = 0
vdisk_spec.device.backing = diskfileBacking
vdisk_spec.device.unitNumber = i
vm_devices.append(vdisk_spec)
i += 1
vm_devices.append(controller)
config.deviceChange = vm_devices
self.wait_task(new_vm.ReconfigVM_Task(config))
self.wait_task(snapshot.RemoveSnapshot_Task(removeChildren=True))
return new_vm
def wait_task(self, task):
while task.info.state in ["queued", "running"]:
time.sleep(2)
self.logger.debug("[%s] %s", self.server_name, task.info.descriptionId)
return task
def do_backup(self, stats):
try:
dest_dir = os.path.join(self.backup_dir, "%s" % self.backup_start_date)
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)
os.chdir(dest_dir)
user_esx, password_esx, null = open(self.password_file).read().split("\n")
global si
si = SmartConnect(host=self.esxhost, user=user_esx, pwd=password_esx, port=self.esx_port)
if not si:
raise Exception("Could not connect to the specified host using specified " "username and password")
atexit.register(Disconnect, si)
content = si.RetrieveContent()
for child in content.rootFolder.childEntity:
if hasattr(child, "vmFolder"):
global vmFolder, datacenter
datacenter = child
vmFolder = datacenter.vmFolder
vmList = vmFolder.childEntity
for vm in vmList:
if vm.name == self.server_name:
vm_is_off = vm.summary.runtime.powerState == "poweredOff"
if str2bool(self.halt_vm):
vm.ShutdownGuest()
vm_is_off = True
if vm_is_off:
vmdks = self.export_vmdks(vm)
ovf_filename = self.create_ovf(vm, vmdks)
else:
new_vm = self.clone_vm(vm)
vmdks = self.export_vmdks(new_vm)
ovf_filename = self.create_ovf(vm, vmdks)
self.wait_task(new_vm.Destroy_Task())
if str2bool(self.create_ovafile):
ova_filename = self.create_ova(vm, vmdks, ovf_filename) # noqa : F841
if str2bool(self.halt_vm):
vm.PowerOnVM()
if os.path.exists(dest_dir):
for file in os.listdir(dest_dir):
stats["written_bytes"] += os.stat(file)[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_dir
stats["log"] = "XVA backup from %s OK, %d bytes written" % (self.server_name, stats["written_bytes"])
stats["status"] = "OK"
except BaseException as e:
stats["status"] = "ERROR"
stats["log"] = str(e)
raise
register_driver(backup_vmdk)
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/python3
# -*- 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 paramiko
from libtisbackup 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", "private_key", "backup_name"]
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)
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")
stats["status"] = "Dumping"
if not self.dry_run:
cmd = "/opt/xensource/bin/xe pool-dump-database file-name="
self.logger.debug("[%s] Dump XCP Metadata : %s", self.backup_name, cmd)
(error_code, output) = ssh_exec(cmd, server_name=self.server_name, private_key=self.private_key, remote_user="root")
with open(localpath, "w") as f:
f.write(output)
# zip the file
stats["status"] = "Zipping"
cmd = "gzip %s " % localpath
self.logger.debug("[%s] Compress backup : %s", self.backup_name, cmd)
if not self.dry_run:
call_external_process(cmd)
localpath += ".gz"
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(r"^%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 file_name not 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)
+309
View File
@@ -0,0 +1,309 @@
#!/usr/bin/python3
# -*- 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 datetime
import hashlib
import logging
import os
import re
import socket
import ssl
import subprocess
import tarfile
import urllib.error
import urllib.parse
import urllib.request
from stat import *
import requests
from . import XenAPI
from libtisbackup import *
if hasattr(ssl, "_create_unverified_context"):
ssl._create_default_https_context = ssl._create_unverified_context
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 + [
"enable_https",
"halt_vm",
"verify_export",
"reuse_snapshot",
"ignore_proxies",
"use_compression",
]
enable_https = "no"
halt_vm = "no"
verify_export = "no"
reuse_snapshot = "no"
ignore_proxies = "yes"
use_compression = "true"
if str2bool(ignore_proxies):
os.environ["http_proxy"] = ""
os.environ["https_proxy"] = ""
def verify_export_xva(self, filename):
self.logger.debug("[%s] Verify xva export integrity", self.server_name)
tar = tarfile.open(filename)
members = tar.getmembers()
for tarinfo in members:
if re.search("^[0-9]*$", os.path.basename(tarinfo.name)):
sha1sum = hashlib.sha1(tar.extractfile(tarinfo).read()).hexdigest()
sha1sum2 = tar.extractfile(tarinfo.name + ".checksum").read()
if not sha1sum == sha1sum2:
raise Exception("File corrupt")
tar.close()
def export_xva(self, vdi_name, filename, halt_vm, dry_run, enable_https=True, reuse_snapshot="no"):
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 as 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)
if not session.xenapi.VM.get_by_name_label(vdi_name):
return "bad VM name: %s" % vdi_name
vm = session.xenapi.VM.get_by_name_label(vdi_name)[0]
status_vm = session.xenapi.VM.get_power_state(vm)
self.logger.debug("[%s] Check if previous fail backups exist", vdi_name)
backups_fail = [f for f in os.listdir(self.backup_dir) if f.startswith(vdi_name) and f.endswith(".tmp")]
for backup_fail in backups_fail:
self.logger.debug('[%s] Delete backup "%s"', vdi_name, backup_fail)
os.unlink(os.path.join(self.backup_dir, backup_fail))
# add snapshot option
if not str2bool(halt_vm):
self.logger.debug("[%s] Check if previous tisbackups snapshots exist", vdi_name)
old_snapshots = session.xenapi.VM.get_by_name_label("tisbackup-%s" % (vdi_name))
self.logger.debug("[%s] Old snaps count %s", vdi_name, len(old_snapshots))
if len(old_snapshots) == 1 and str2bool(reuse_snapshot):
snapshot = old_snapshots[0]
self.logger.debug('[%s] Reusing snap "%s"', vdi_name, session.xenapi.VM.get_name_description(snapshot))
vm = snapshot # vm = session.xenapi.VM.get_by_name_label("tisbackup-%s"%(vdi_name))[0]
else:
self.logger.debug("[%s] Deleting %s old snaps", vdi_name, len(old_snapshots))
for old_snapshot in old_snapshots:
self.logger.debug("[%s] Destroy snapshot %s", vdi_name, session.xenapi.VM.get_name_description(old_snapshot))
try:
for vbd in session.xenapi.VM.get_VBDs(old_snapshot):
if session.xenapi.VBD.get_type(vbd) == "CD" and not session.xenapi.VBD.get_record(vbd)["empty"]:
session.xenapi.VBD.eject(vbd)
else:
vdi = session.xenapi.VBD.get_VDI(vbd)
if "NULL" not in vdi:
session.xenapi.VDI.destroy(vdi)
session.xenapi.VM.destroy(old_snapshot)
except XenAPI.Failure as error:
return "error when destroy snapshot %s" % (error)
now = datetime.datetime.now()
self.logger.debug("[%s] Snapshot in progress", vdi_name)
try:
snapshot = session.xenapi.VM.snapshot(vm, "tisbackup-%s" % (vdi_name))
self.logger.debug("[%s] got snapshot %s", vdi_name, snapshot)
except XenAPI.Failure as error:
return "error when snapshot %s" % (error)
# get snapshot opaqueRef
vm = session.xenapi.VM.get_by_name_label("tisbackup-%s" % (vdi_name))[0]
session.xenapi.VM.set_name_description(snapshot, "snapshot created by tisbackup on: %s" % (now.strftime("%Y-%m-%d %H:%M")))
else:
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:
filename_temp = filename + ".tmp"
self.logger.debug("[%s] Copy in progress", self.backup_name)
if not str2bool(self.use_compression):
socket.setdefaulttimeout(120)
scheme = "http://"
if str2bool(enable_https):
scheme = "https://"
# url = scheme+user_xen+":"+password_xen+"@"+self.xcphost+"/export?use_compression="+self.use_compression+"&uuid="+session.xenapi.VM.get_uuid(vm)
top_level_url = (
scheme + self.xcphost + "/export?use_compression=" + self.use_compression + "&uuid=" + session.xenapi.VM.get_uuid(vm)
)
r = requests.get(top_level_url, auth=(user_xen, password_xen))
open(filename_temp, "wb").write(r.content)
except Exception as e:
self.logger.error("[%s] error when fetching snap: %s", "tisbackup-%s" % (vdi_name), e)
if os.path.exists(filename_temp):
os.unlink(filename_temp)
raise
finally:
if not str2bool(halt_vm):
self.logger.debug("[%s] Destroy snapshot", "tisbackup-%s" % (vdi_name))
try:
for vbd in session.xenapi.VM.get_VBDs(snapshot):
if session.xenapi.VBD.get_type(vbd) == "CD" and not session.xenapi.VBD.get_record(vbd)["empty"]:
session.xenapi.VBD.eject(vbd)
else:
vdi = session.xenapi.VBD.get_VDI(vbd)
if "NULL" not in vdi:
session.xenapi.VDI.destroy(vdi)
session.xenapi.VM.destroy(snapshot)
except XenAPI.Failure as error:
return "error when destroy snapshot %s" % (error)
elif 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_temp):
# Verify tar file integrity using subprocess instead of os.system
try:
subprocess.run(
["tar", "tf", filename_temp],
capture_output=True,
check=True,
timeout=300
)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
os.unlink(filename_temp)
return "Tar error"
if str2bool(self.verify_export):
self.verify_export_xva(filename_temp)
os.rename(filename_temp, filename)
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(
vdi_name=self.server_name,
filename=dest_filename,
halt_vm=self.halt_vm,
enable_https=self.enable_https,
dry_run=self.dry_run,
reuse_snapshot=self.reuse_snapshot,
)
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:
raise Exception(cmd)
except BaseException as e:
stats["status"] = "ERROR"
stats["log"] = str(e)
raise
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()
for item in filelist:
if item.endswith(".xva"):
dir_name = os.path.join(self.backup_dir, item)
if dir_name not in registered:
start = (
datetime.datetime.strptime(item, self.backup_name + "-%Y%m%d-%Hh%Mm%S.xva") + datetime.timedelta(0, 30 * 60)
).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,
TYPE="BACKUP",
)
else:
self.logger.info("Skipping %s, already registered", dir_name)
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)
+287
View File
@@ -0,0 +1,287 @@
#!/usr/bin/python3
# -*- 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 base64
import datetime
import logging
import os
import os.path
import re
import select
import socket
import ssl
import time
import urllib.error
import urllib.parse
import urllib.request
from stat import *
from . import XenAPI
from libtisbackup import *
if hasattr(ssl, "_create_unverified_context"):
ssl._create_default_https_context = ssl._create_unverified_context
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", "delete_snapshot", "halt_vm"]
start_vm = "no"
max_copies = 1
halt_vm = "no"
delete_snapshot = "yes"
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")
if self.delete_snapshot == "yes" and iniconf.has_option("global", "delete_snapshot"):
self.delete_snapshot = iniconf.get("global", "delete_snapshot")
def copy_vm_to_sr(self, vm_name, storage_name, dry_run, delete_snapshot="yes"):
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 as 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 as error:
result = (1, "error get SR opaqueref %s" % (error))
return result
# get vm to copy opaqueRef
try:
vm = session.xenapi.VM.get_by_name_label(vm_name)[0]
except IndexError as error:
result = (1, "error get VM opaqueref %s" % (error))
return result
# get vm backup network opaqueRef
try:
networkRef = session.xenapi.network.get_by_name_label(self.network_name)[0]
except IndexError as error:
result = (1, "error get VM network opaqueref %s" % (error))
return result
if str2bool(self.halt_vm):
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] Shutdown in progress", self.backup_name)
if dry_run:
print("session.xenapi.VM.clean_shutdown(vm)")
else:
session.xenapi.VM.clean_shutdown(vm)
snapshot = vm
else:
# 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 as error:
result = (1, "error when snapshot %s" % (error))
return result
# 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 "Halted" not 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 "Halted" not 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):
if session.xenapi.VBD.get_type(vbd) == "CD" and not session.xenapi.VBD.get_record(vbd)["empty"]:
session.xenapi.VBD.eject(vbd)
else:
vdi = session.xenapi.VBD.get_VDI(vbd)
if "NULL" not in vdi:
session.xenapi.VDI.destroy(vdi)
session.xenapi.VM.destroy(oldest_backup_vm)
except XenAPI.Failure as error:
result = (1, "error when destroy old backup vm %s" % (error))
return result
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 as error:
result = (1, "error when copy %s" % (error))
return result
# 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 as error:
result = (1, "error get VIF opaqueref %s" % (error))
return result
for i in vifDestroy:
vifRecord = session.xenapi.VIF.get_record(i)
session.xenapi.VIF.destroy(i)
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 as error:
result = (1, error)
return result
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")))
size_backup = 0
for vbd in session.xenapi.VM.get_VBDs(backup_vm):
if session.xenapi.VBD.get_type(vbd) == "CD" and not session.xenapi.VBD.get_record(vbd)["empty"]:
session.xenapi.VBD.eject(vbd)
else:
vdi = session.xenapi.VBD.get_VDI(vbd)
if "NULL" not in vdi:
size_backup = size_backup + int(session.xenapi.VDI.get_record(vdi)["physical_utilisation"])
result = (0, size_backup)
if self.delete_snapshot == "no":
return result
# Disable automatic boot
if "auto_poweron" in session.xenapi.VM.get_other_config(backup_vm):
session.xenapi.VM.remove_from_other_config(backup_vm, "auto_poweron")
if not str2bool(self.halt_vm):
# delete the snapshot
try:
for vbd in session.xenapi.VM.get_VBDs(snapshot):
if session.xenapi.VBD.get_type(vbd) == "CD" and not session.xenapi.VBD.get_record(vbd)["empty"]:
session.xenapi.VBD.eject(vbd)
else:
vdi = session.xenapi.VBD.get_VDI(vbd)
if "NULL" not in vdi:
session.xenapi.VDI.destroy(vdi)
session.xenapi.VM.destroy(snapshot)
except XenAPI.Failure as error:
result = (1, "error when destroy snapshot %s" % (error))
return result
else:
if status_vm == "Running":
self.logger.debug("[%s] Starting in progress", self.backup_name)
if dry_run:
print("session.xenapi.VM.start(vm,False,True)")
else:
session.xenapi.VM.start(vm, False, True)
return result
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, delete_snapshot=self.delete_snapshot)
if cmd[0] == 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["total_bytes"] = cmd[1]
stats["backup_location"] = self.storage_name
else:
stats["status"] = "ERROR"
stats["log"] = cmd[1]
except BaseException as e:
stats["status"] = "ERROR"
stats["log"] = str(e)
raise
def register_existingbackups(self):
"""scan backup dir and insert stats in database"""
# This backup is on target server, no data available on this server
pass
register_driver(copy_vm_xcp)