Imported Upstream version 4.3.1
This commit is contained in:
@@ -17,76 +17,73 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import logging
|
||||
import optparse # pylint: disable=deprecated-module
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import pwd
|
||||
|
||||
import six
|
||||
|
||||
from optparse import OptionGroup
|
||||
from ipaplatform.paths import paths
|
||||
from ipaplatform import services
|
||||
|
||||
from six.moves.configparser import SafeConfigParser
|
||||
|
||||
from ipalib import api, errors
|
||||
from ipapython import version
|
||||
from ipapython.ipautil import run, write_tmp_file
|
||||
from ipapython import admintool, certdb
|
||||
from ipapython import admintool
|
||||
from ipapython.config import IPAOptionParser
|
||||
from ipapython.dn import DN
|
||||
from ipaserver.install.replication import wait_for_task
|
||||
from ipaserver.install import installutils
|
||||
from ipapython import ipaldap
|
||||
from ipalib.session import ISO8601_DATETIME_FMT
|
||||
from ipalib.constants import CACERT
|
||||
from six.moves.configparser import SafeConfigParser
|
||||
from ipaplatform.constants import constants
|
||||
from ipaplatform.tasks import tasks
|
||||
|
||||
# pylint: disable=import-error
|
||||
if six.PY3:
|
||||
# The SafeConfigParser class has been renamed to ConfigParser in Py3
|
||||
from configparser import ConfigParser as SafeConfigParser
|
||||
else:
|
||||
from ConfigParser import SafeConfigParser
|
||||
# pylint: enable=import-error
|
||||
ISO8601_DATETIME_FMT = '%Y-%m-%dT%H:%M:%S'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
A test GnuPG key can be generated like this:
|
||||
A test gpg can be generated like this:
|
||||
|
||||
# cat >keygen <<EOF
|
||||
%echo Generating a standard key
|
||||
Key-Type: RSA
|
||||
Key-Length: 2048
|
||||
Name-Real: IPA Backup
|
||||
Name-Comment: IPA Backup
|
||||
Name-Email: root@example.com
|
||||
Expire-Date: 0
|
||||
Passphrase: SecretPassPhrase42
|
||||
%commit
|
||||
%echo done
|
||||
%echo Generating a standard key
|
||||
Key-Type: RSA
|
||||
Key-Length: 2048
|
||||
Name-Real: IPA Backup
|
||||
Name-Comment: IPA Backup
|
||||
Name-Email: root@example.com
|
||||
Expire-Date: 0
|
||||
%pubring /root/backup.pub
|
||||
%secring /root/backup.sec
|
||||
%commit
|
||||
%echo done
|
||||
EOF
|
||||
# export GNUPGHOME=/root/backup
|
||||
# mkdir -p $GNUPGHOME
|
||||
# gpg2 --batch --gen-key keygen
|
||||
# gpg2 --list-secret-keys
|
||||
# gpg --batch --gen-key keygen
|
||||
# gpg --no-default-keyring --secret-keyring /root/backup.sec \
|
||||
--keyring /root/backup.pub --list-secret-keys
|
||||
"""
|
||||
|
||||
|
||||
def encrypt_file(filename, remove_original=True):
|
||||
def encrypt_file(filename, keyring, remove_original=True):
|
||||
source = filename
|
||||
dest = filename + '.gpg'
|
||||
|
||||
args = [
|
||||
paths.GPG2,
|
||||
'--batch',
|
||||
'--default-recipient-self',
|
||||
'--output', dest,
|
||||
'--encrypt', source,
|
||||
]
|
||||
args = [paths.GPG,
|
||||
'--batch',
|
||||
'--default-recipient-self',
|
||||
'-o', dest]
|
||||
|
||||
if keyring is not None:
|
||||
args.append('--no-default-keyring')
|
||||
args.append('--keyring')
|
||||
args.append(keyring + '.pub')
|
||||
args.append('--secret-keyring')
|
||||
args.append(keyring + '.sec')
|
||||
|
||||
args.append('-e')
|
||||
args.append(source)
|
||||
|
||||
result = run(args, raiseonerr=False)
|
||||
if result.returncode != 0:
|
||||
@@ -107,19 +104,20 @@ class Backup(admintool.AdminTool):
|
||||
description = "Back up IPA files and databases."
|
||||
|
||||
dirs = (paths.IPA_HTML_DIR,
|
||||
paths.ROOT_PKI,
|
||||
paths.PKI_TOMCAT,
|
||||
paths.SYSCONFIG_PKI,
|
||||
paths.VAR_LIB_PKI_DIR,
|
||||
paths.SYSRESTORE,
|
||||
paths.IPA_CLIENT_SYSRESTORE,
|
||||
paths.IPA_DNSSEC_DIR,
|
||||
paths.SSSD_PUBCONF_KRB5_INCLUDE_D_DIR,
|
||||
paths.AUTHCONFIG_LAST,
|
||||
paths.VAR_LIB_CERTMONGER_DIR,
|
||||
paths.VAR_LIB_IPA,
|
||||
paths.VAR_RUN_DIRSRV_DIR,
|
||||
paths.DIRSRV_LOCK_DIR,
|
||||
paths.ROOT_PKI,
|
||||
paths.PKI_TOMCAT,
|
||||
paths.SYSCONFIG_PKI,
|
||||
paths.HTTPD_ALIAS_DIR,
|
||||
paths.VAR_LIB_PKI_DIR,
|
||||
paths.SYSRESTORE,
|
||||
paths.IPA_CLIENT_SYSRESTORE,
|
||||
paths.IPA_DNSSEC_DIR,
|
||||
paths.SSSD_PUBCONF_KRB5_INCLUDE_D_DIR,
|
||||
paths.AUTHCONFIG_LAST,
|
||||
paths.VAR_LIB_CERTMONGER_DIR,
|
||||
paths.VAR_LIB_IPA,
|
||||
paths.VAR_RUN_DIRSRV_DIR,
|
||||
paths.DIRSRV_LOCK_DIR,
|
||||
)
|
||||
|
||||
files = (
|
||||
@@ -128,6 +126,7 @@ class Backup(admintool.AdminTool):
|
||||
paths.RESOLV_CONF,
|
||||
paths.SYSCONFIG_PKI_TOMCAT,
|
||||
paths.SYSCONFIG_DIRSRV,
|
||||
paths.SYSCONFIG_NTPD,
|
||||
paths.SYSCONFIG_KRB5KDC_DIR,
|
||||
paths.SYSCONFIG_IPA_DNSKEYSYNCD,
|
||||
paths.SYSCONFIG_IPA_ODS_EXPORTER,
|
||||
@@ -143,37 +142,26 @@ class Backup(admintool.AdminTool):
|
||||
paths.OPENLDAP_LDAP_CONF,
|
||||
paths.LIMITS_CONF,
|
||||
paths.HTTPD_PASSWORD_CONF,
|
||||
paths.HTTP_KEYTAB,
|
||||
paths.IPA_KEYTAB,
|
||||
paths.HTTPD_IPA_KDCPROXY_CONF,
|
||||
paths.HTTPD_IPA_PKI_PROXY_CONF,
|
||||
paths.HTTPD_IPA_REWRITE_CONF,
|
||||
paths.HTTPD_SSL_CONF,
|
||||
paths.HTTPD_SSL_SITE_CONF,
|
||||
paths.HTTPD_CERT_FILE,
|
||||
paths.HTTPD_KEY_FILE,
|
||||
paths.HTTPD_NSS_CONF,
|
||||
paths.HTTPD_IPA_CONF,
|
||||
paths.SSHD_CONFIG,
|
||||
paths.SSH_CONFIG,
|
||||
paths.KRB5_CONF,
|
||||
paths.KDC_CA_BUNDLE_PEM,
|
||||
paths.CA_BUNDLE_PEM,
|
||||
paths.IPA_CA_CRT,
|
||||
CACERT,
|
||||
paths.IPA_DEFAULT_CONF,
|
||||
paths.DS_KEYTAB,
|
||||
paths.CHRONY_CONF,
|
||||
paths.NTP_CONF,
|
||||
paths.SMB_CONF,
|
||||
paths.SAMBA_KEYTAB,
|
||||
paths.DOGTAG_ADMIN_P12,
|
||||
paths.RA_AGENT_PEM,
|
||||
paths.RA_AGENT_KEY,
|
||||
paths.KRA_AGENT_PEM,
|
||||
paths.CACERT_P12,
|
||||
paths.KRACERT_P12,
|
||||
paths.KRB5KDC_KDC_CONF,
|
||||
paths.KDC_CERT,
|
||||
paths.KDC_KEY,
|
||||
paths.CACERT_PEM,
|
||||
paths.SYSTEMD_IPA_SERVICE,
|
||||
paths.SYSTEMD_SYSTEM_HTTPD_IPA_CONF,
|
||||
paths.SYSTEMD_SSSD_SERVICE,
|
||||
paths.SYSTEMD_CERTMONGER_SERVICE,
|
||||
paths.SYSTEMD_PKI_TOMCAT_SERVICE,
|
||||
@@ -186,13 +174,10 @@ class Backup(admintool.AdminTool):
|
||||
paths.DNSSEC_SOFTHSM_PIN_SO,
|
||||
paths.IPA_ODS_EXPORTER_KEYTAB,
|
||||
paths.IPA_DNSKEYSYNCD_KEYTAB,
|
||||
paths.IPA_CUSTODIA_KEYS,
|
||||
paths.IPA_CUSTODIA_CONF,
|
||||
paths.GSSPROXY_CONF,
|
||||
paths.HOSTS,
|
||||
) + tuple(
|
||||
os.path.join(paths.IPA_NSSDB_DIR, file)
|
||||
for file in (certdb.NSS_DBM_FILES + certdb.NSS_SQL_FILES)
|
||||
for file in ('cert8.db', 'key3.db', 'secmod.db')
|
||||
)
|
||||
|
||||
logs=(
|
||||
@@ -229,22 +214,16 @@ class Backup(admintool.AdminTool):
|
||||
def add_options(cls, parser):
|
||||
super(Backup, cls).add_options(parser, debug_option=True)
|
||||
|
||||
parser.add_option(
|
||||
"--gpg-keyring", dest="gpg_keyring",
|
||||
help=optparse.SUPPRESS_HELP)
|
||||
parser.add_option(
|
||||
"--gpg", dest="gpg", action="store_true",
|
||||
default=False, help="Encrypt the backup")
|
||||
parser.add_option(
|
||||
"--data", dest="data_only", action="store_true",
|
||||
parser.add_option("--gpg-keyring", dest="gpg_keyring",
|
||||
help="The gpg key name to be used (or full path)")
|
||||
parser.add_option("--gpg", dest="gpg", action="store_true",
|
||||
default=False, help="Encrypt the backup")
|
||||
parser.add_option("--data", dest="data_only", action="store_true",
|
||||
default=False, help="Backup only the data")
|
||||
parser.add_option(
|
||||
"--logs", dest="logs", action="store_true",
|
||||
parser.add_option("--logs", dest="logs", action="store_true",
|
||||
default=False, help="Include log files in backup")
|
||||
parser.add_option(
|
||||
"--online", dest="online", action="store_true",
|
||||
default=False,
|
||||
help="Perform the LDAP backups online, for data only.")
|
||||
parser.add_option("--online", dest="online", action="store_true",
|
||||
default=False, help="Perform the LDAP backups online, for data only.")
|
||||
|
||||
|
||||
def setup_logging(self, log_file_mode='a'):
|
||||
@@ -257,11 +236,9 @@ class Backup(admintool.AdminTool):
|
||||
installutils.check_server_configuration()
|
||||
|
||||
if options.gpg_keyring is not None:
|
||||
print(
|
||||
"--gpg-keyring is no longer supported, use GNUPGHOME "
|
||||
"environment variable to use a custom GnuPG2 directory.",
|
||||
file=sys.stderr
|
||||
)
|
||||
if not os.path.exists(options.gpg_keyring + '.pub'):
|
||||
raise admintool.ScriptError('No such key %s' %
|
||||
options.gpg_keyring)
|
||||
options.gpg = True
|
||||
|
||||
if options.online and not options.data_only:
|
||||
@@ -270,7 +247,7 @@ class Backup(admintool.AdminTool):
|
||||
|
||||
if options.gpg:
|
||||
tmpfd = write_tmp_file('encryptme')
|
||||
newfile = encrypt_file(tmpfd.name, False)
|
||||
newfile = encrypt_file(tmpfd.name, options.gpg_keyring, False)
|
||||
os.unlink(newfile)
|
||||
|
||||
if options.data_only and options.logs:
|
||||
@@ -282,10 +259,10 @@ class Backup(admintool.AdminTool):
|
||||
options = self.options
|
||||
super(Backup, self).run()
|
||||
|
||||
api.bootstrap(in_server=True, context='backup', confdir=paths.ETC_IPA)
|
||||
api.bootstrap(in_server=False, context='backup')
|
||||
api.finalize()
|
||||
|
||||
logger.info("Preparing backup on %s", api.env.host)
|
||||
self.log.info("Preparing backup on %s", api.env.host)
|
||||
|
||||
pent = pwd.getpwnam(constants.DS_USER)
|
||||
|
||||
@@ -296,7 +273,6 @@ class Backup(admintool.AdminTool):
|
||||
os.mkdir(self.dir)
|
||||
os.chmod(self.dir, 0o750)
|
||||
os.chown(self.dir, pent.pw_uid, pent.pw_gid)
|
||||
self.tarfile = None
|
||||
|
||||
self.header = os.path.join(self.top_dir, 'header')
|
||||
|
||||
@@ -314,11 +290,11 @@ class Backup(admintool.AdminTool):
|
||||
self.create_header(options.data_only)
|
||||
if options.data_only:
|
||||
if not options.online:
|
||||
logger.info('Stopping Directory Server')
|
||||
self.log.info('Stopping Directory Server')
|
||||
dirsrv.stop(capture_output=False)
|
||||
else:
|
||||
logger.info('Stopping IPA services')
|
||||
run([paths.IPACTL, 'stop'])
|
||||
self.log.info('Stopping IPA services')
|
||||
run(['ipactl', 'stop'])
|
||||
|
||||
instance = installutils.realm_to_serverid(api.env.realm)
|
||||
if os.path.exists(paths.VAR_LIB_SLAPD_INSTANCE_DIR_TEMPLATE %
|
||||
@@ -333,28 +309,21 @@ class Backup(admintool.AdminTool):
|
||||
auth_backup_path = os.path.join(paths.VAR_LIB_IPA, 'auth_backup')
|
||||
tasks.backup_auth_configuration(auth_backup_path)
|
||||
self.file_backup(options)
|
||||
self.finalize_backup(options.data_only, options.gpg, options.gpg_keyring)
|
||||
|
||||
if options.data_only:
|
||||
if not options.online:
|
||||
logger.info('Starting Directory Server')
|
||||
self.log.info('Starting Directory Server')
|
||||
dirsrv.start(capture_output=False)
|
||||
else:
|
||||
logger.info('Starting IPA service')
|
||||
run([paths.IPACTL, 'start'])
|
||||
|
||||
# Compress after services are restarted to minimize
|
||||
# the unavailability window
|
||||
if not options.data_only:
|
||||
self.compress_file_backup()
|
||||
|
||||
self.finalize_backup(options.data_only, options.gpg,
|
||||
options.gpg_keyring)
|
||||
self.log.info('Starting IPA service')
|
||||
run(['ipactl', 'start'])
|
||||
|
||||
finally:
|
||||
try:
|
||||
os.chdir(cwd)
|
||||
except Exception as e:
|
||||
logger.error('Cannot change directory to %s: %s', cwd, e)
|
||||
self.log.error('Cannot change directory to %s: %s' % (cwd, e))
|
||||
shutil.rmtree(self.top_dir)
|
||||
|
||||
|
||||
@@ -372,16 +341,9 @@ class Backup(admintool.AdminTool):
|
||||
if os.path.exists(dir):
|
||||
self.dirs.append(dir)
|
||||
|
||||
for file in (
|
||||
paths.SYSCONFIG_DIRSRV_INSTANCE % serverid,
|
||||
paths.ETC_TMPFILESD_DIRSRV % serverid,
|
||||
):
|
||||
if os.path.exists(file):
|
||||
self.files.append(file)
|
||||
|
||||
self.files.append(
|
||||
paths.HTTPD_PASSWD_FILE_FMT.format(host=api.env.host)
|
||||
)
|
||||
file = paths.SYSCONFIG_DIRSRV_INSTANCE % serverid
|
||||
if os.path.exists(file):
|
||||
self.files.append(file)
|
||||
|
||||
self.logs.append(paths.VAR_LOG_DIRSRV_INSTANCE_TEMPLATE % serverid)
|
||||
|
||||
@@ -393,14 +355,17 @@ class Backup(admintool.AdminTool):
|
||||
if self._conn is not None:
|
||||
return self._conn
|
||||
|
||||
ldap_uri = ipaldap.get_ldap_uri(protocol='ldapi', realm=api.env.realm)
|
||||
self._conn = ipaldap.LDAPClient(ldap_uri)
|
||||
self._conn = ipaldap.IPAdmin(host=api.env.host,
|
||||
ldapi=True,
|
||||
protocol='ldapi',
|
||||
realm=api.env.realm)
|
||||
|
||||
try:
|
||||
self._conn.external_bind()
|
||||
pw_name = pwd.getpwuid(os.geteuid()).pw_name
|
||||
self._conn.do_external_bind(pw_name)
|
||||
except Exception as e:
|
||||
logger.error("Unable to bind to LDAP server %s: %s",
|
||||
self._conn.host, e)
|
||||
self.log.error("Unable to bind to LDAP server %s: %s" %
|
||||
(self._conn.host, e))
|
||||
|
||||
return self._conn
|
||||
|
||||
@@ -414,8 +379,9 @@ class Backup(admintool.AdminTool):
|
||||
For SELinux reasons this writes out to the 389-ds backup location
|
||||
and we move it.
|
||||
'''
|
||||
logger.info('Backing up %s in %s to LDIF', backend, instance)
|
||||
self.log.info('Backing up %s in %s to LDIF' % (backend, instance))
|
||||
|
||||
now = time.localtime()
|
||||
cn = time.strftime('export_%Y_%m_%d_%H_%M_%S')
|
||||
dn = DN(('cn', cn), ('cn', 'export'), ('cn', 'tasks'), ('cn', 'config'))
|
||||
|
||||
@@ -444,7 +410,7 @@ class Backup(admintool.AdminTool):
|
||||
raise admintool.ScriptError('Unable to add LDIF task: %s'
|
||||
% e)
|
||||
|
||||
logger.info("Waiting for LDIF to finish")
|
||||
self.log.info("Waiting for LDIF to finish")
|
||||
wait_for_task(conn, dn)
|
||||
else:
|
||||
args = [paths.DB2LDIF,
|
||||
@@ -454,7 +420,7 @@ class Backup(admintool.AdminTool):
|
||||
'-a', ldiffile]
|
||||
result = run(args, raiseonerr=False)
|
||||
if result.returncode != 0:
|
||||
logger.critical('db2ldif failed: %s', result.error_log)
|
||||
self.log.critical('db2ldif failed: %s', result.error_log)
|
||||
|
||||
# Move the LDIF backup to our location
|
||||
shutil.move(ldiffile, os.path.join(self.dir, ldifname))
|
||||
@@ -466,7 +432,8 @@ class Backup(admintool.AdminTool):
|
||||
|
||||
If executed online create a task and wait for it to complete.
|
||||
'''
|
||||
logger.info('Backing up %s', instance)
|
||||
self.log.info('Backing up %s' % instance)
|
||||
now = time.localtime()
|
||||
cn = time.strftime('backup_%Y_%m_%d_%H_%M_%S')
|
||||
dn = DN(('cn', cn), ('cn', 'backup'), ('cn', 'tasks'), ('cn', 'config'))
|
||||
|
||||
@@ -491,13 +458,13 @@ class Backup(admintool.AdminTool):
|
||||
raise admintool.ScriptError('Unable to to add backup task: %s'
|
||||
% e)
|
||||
|
||||
logger.info("Waiting for BAK to finish")
|
||||
self.log.info("Waiting for BAK to finish")
|
||||
wait_for_task(conn, dn)
|
||||
else:
|
||||
args = [paths.DB2BAK, bakdir, '-Z', instance]
|
||||
result = run(args, raiseonerr=False)
|
||||
if result.returncode != 0:
|
||||
logger.critical('db2bak failed: %s', result.error_log)
|
||||
self.log.critical('db2bak failed: %s', result.error_log)
|
||||
|
||||
shutil.move(bakdir, self.dir)
|
||||
|
||||
@@ -507,15 +474,15 @@ class Backup(admintool.AdminTool):
|
||||
def verify_directories(dirs):
|
||||
return [s for s in dirs if os.path.exists(s)]
|
||||
|
||||
self.tarfile = os.path.join(self.dir, 'files.tar')
|
||||
tarfile = os.path.join(self.dir, 'files.tar')
|
||||
|
||||
logger.info("Backing up files")
|
||||
self.log.info("Backing up files")
|
||||
args = ['tar',
|
||||
'--exclude=/var/lib/ipa/backup',
|
||||
'--xattrs',
|
||||
'--selinux',
|
||||
'-cf',
|
||||
self.tarfile
|
||||
tarfile
|
||||
]
|
||||
|
||||
args.extend(verify_directories(self.dirs))
|
||||
@@ -541,7 +508,7 @@ class Backup(admintool.AdminTool):
|
||||
'--selinux',
|
||||
'--no-recursion',
|
||||
'-rf', # -r appends to an existing archive
|
||||
self.tarfile,
|
||||
tarfile,
|
||||
]
|
||||
args.extend(missing_directories)
|
||||
|
||||
@@ -552,20 +519,17 @@ class Backup(admintool.AdminTool):
|
||||
'when adding directory structure: %s' %
|
||||
(result.returncode, result.error_log))
|
||||
|
||||
def compress_file_backup(self):
|
||||
|
||||
# Compress the archive. This is done separately, since 'tar' cannot
|
||||
# append to a compressed archive.
|
||||
if self.tarfile:
|
||||
result = run([paths.GZIP, self.tarfile], raiseonerr=False)
|
||||
if result.returncode != 0:
|
||||
raise admintool.ScriptError(
|
||||
'gzip returned non-zero code %d '
|
||||
'when compressing the backup: %s' %
|
||||
(result.returncode, result.error_log))
|
||||
result = run(['gzip', tarfile], raiseonerr=False)
|
||||
if result.returncode != 0:
|
||||
raise admintool.ScriptError(
|
||||
'gzip returned non-zero code %d '
|
||||
'when compressing the backup: %s' %
|
||||
(result.returncode, result.error_log))
|
||||
|
||||
# Rename the archive back to files.tar to preserve compatibility
|
||||
os.rename(os.path.join(self.dir, 'files.tar.gz'), self.tarfile)
|
||||
# Rename the archive back to files.tar to preserve compatibility
|
||||
os.rename(os.path.join(self.dir, 'files.tar.gz'), tarfile)
|
||||
|
||||
|
||||
def create_header(self, data_only):
|
||||
@@ -590,11 +554,11 @@ class Backup(admintool.AdminTool):
|
||||
conn = self.get_connection()
|
||||
services = conn.get_entries(dn, conn.SCOPE_ONELEVEL)
|
||||
except errors.NetworkError:
|
||||
logger.critical(
|
||||
self.log.critical(
|
||||
"Unable to obtain list of master services, continuing anyway")
|
||||
except Exception as e:
|
||||
logger.error("Failed to read services from '%s': %s",
|
||||
conn.host, e)
|
||||
self.log.error("Failed to read services from '%s': %s" %
|
||||
(conn.host, e))
|
||||
else:
|
||||
services_cns = [s.single_value['cn'] for s in services]
|
||||
|
||||
@@ -626,6 +590,7 @@ class Backup(admintool.AdminTool):
|
||||
os.mkdir(backup_dir)
|
||||
os.chmod(backup_dir, 0o700)
|
||||
|
||||
cwd = os.getcwd()
|
||||
os.chdir(self.dir)
|
||||
args = ['tar',
|
||||
'--xattrs',
|
||||
@@ -641,9 +606,9 @@ class Backup(admintool.AdminTool):
|
||||
(result.returncode, result.error_log))
|
||||
|
||||
if encrypt:
|
||||
logger.info('Encrypting %s', filename)
|
||||
filename = encrypt_file(filename)
|
||||
self.log.info('Encrypting %s' % filename)
|
||||
filename = encrypt_file(filename, keyring)
|
||||
|
||||
shutil.move(self.header, backup_dir)
|
||||
|
||||
logger.info('Backed up to %s', backup_dir)
|
||||
self.log.info('Backed up to %s', backup_dir)
|
||||
|
||||
Reference in New Issue
Block a user