Full system backup and restore

This will allow one to backup and restore the IPA files and data. This
does not cover individual entry restoration.

http://freeipa.org/page/V3/Backup_and_Restore

https://fedorahosted.org/freeipa/ticket/3128
This commit is contained in:
Rob Crittenden
2013-03-13 09:36:41 -04:00
parent c0cdba78b0
commit c8694cb19f
12 changed files with 1648 additions and 133 deletions

View File

@@ -421,6 +421,7 @@ mkdir -p %{buildroot}%{_unitdir}
install -m 644 init/systemd/ipa.service %{buildroot}%{_unitdir}/ipa.service
install -m 644 init/systemd/ipa_memcached.service %{buildroot}%{_unitdir}/ipa_memcached.service
# END
mkdir -p %{buildroot}/%{_localstatedir}/lib/ipa/backup
%endif # ! %{ONLY_CLIENT}
mkdir -p %{buildroot}%{_sysconfdir}/ipa/
@@ -568,6 +569,8 @@ fi
%files server -f server-python.list
%defattr(-,root,root,-)
%doc COPYING README Contributors.txt
%{_sbindir}/ipa-backup
%{_sbindir}/ipa-restore
%{_sbindir}/ipa-ca-install
%{_sbindir}/ipa-dns-install
%{_sbindir}/ipa-server-install
@@ -688,6 +691,7 @@ fi
%attr(755,root,root) %{plugin_dir}/libipa_dns.so
%attr(755,root,root) %{plugin_dir}/libipa_range_check.so
%dir %{_localstatedir}/lib/ipa
%attr(755,root,root) %dir %{_localstatedir}/lib/ipa/backup
%attr(700,root,root) %dir %{_localstatedir}/lib/ipa/sysrestore
%attr(700,root,root) %dir %{_localstatedir}/lib/ipa/sysupgrade
%attr(755,root,root) %dir %{_localstatedir}/lib/ipa/pki-ca
@@ -711,6 +715,8 @@ fi
%{_mandir}/man8/ipactl.8.gz
%{_mandir}/man8/ipa-upgradeconfig.8.gz
%{_mandir}/man1/ipa-compliance.1.gz
%{_mandir}/man1/ipa-backup.1.gz
%{_mandir}/man1/ipa-restore.1.gz
%files server-selinux
%defattr(-,root,root,-)
@@ -788,14 +794,18 @@ fi
%ghost %attr(0644,root,apache) %config(noreplace) %{_sysconfdir}/ipa/ca.crt
%changelog
* Thu Apr 4 2013 Alexander Bokovoy <abokovoy@redhat.com> - 3.1.99-3
* Fri Apr 5 2013 Rob Crittenden <rcritten@redhat.com> - 3.1.99-5
- Add backup and restore
- Own /var/lib/ipa/backup
* Thu Apr 4 2013 Alexander Bokovoy <abokovoy@redhat.com> - 3.1.99-4
- Make sure build against Krb5 1.11 in Fedora 18 environment creates proper dependencies
* Tue Apr 2 2013 Martin Kosek <mkosek@redhat.com> - 3.1.99-2
* Tue Apr 2 2013 Martin Kosek <mkosek@redhat.com> - 3.1.99-3
- Require 389-base-base >= 1.3.0.5 to pull the following fixes:
- upgrade deadlock caused by DNA plugin reconfiguration
- CVE-2013-1897: unintended information exposure when
nsslapd-allow-anonymous-access is set to rootdse
- upgrade deadlock caused by DNA plugin reconfiguration
- CVE-2013-1897: unintended information exposure when
nsslapd-allow-anonymous-access is set to rootdse
* Wed Mar 27 2013 Martin Kosek <mkosek@redhat.com> - 3.1.99-2
- Remove conflict with krb5-server > 1.11 as ipa-kdb is compatible

View File

@@ -22,6 +22,8 @@ sbin_SCRIPTS = \
ipa-ldap-updater \
ipa-upgradeconfig \
ipa-compliance \
ipa-backup \
ipa-restore \
$(NULL)
EXTRA_DIST = \

23
install/tools/ipa-backup Executable file
View File

@@ -0,0 +1,23 @@
#! /usr/bin/python -E
# Authors: Rob Crittenden <rcritten@redhat.com>
#
# Copyright (C) 2013 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program 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.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
#
from ipaserver.install.ipa_backup import Backup
Backup.run_cli()

View File

@@ -46,109 +46,6 @@ commands = {
}
def get_cs_replication_manager(realm, host, dirman_passwd):
"""Get a CSReplicationManager for a remote host
Detects if the host has a merged database, connects to appropriate port.
"""
# Try merged database port first. If it has the ipaca tree, return
# corresponding replication manager
# If we can't connect to it at all, we're not dealing with an IPA master
# anyway; let the exception propagate up
# Fall back to the old PKI-only DS port. Check that it has the ipaca tree
# (IPA with merged DB theoretically leaves port 7389 free for anyone).
# If it doesn't, raise exception.
ports = [
dogtag.Dogtag10Constants.DS_PORT,
dogtag.Dogtag9Constants.DS_PORT,
]
for port in ports:
root_logger.debug('Looking for PKI DS on %s:%s' % (host, port))
replication_manager = CSReplicationManager(
realm, host, dirman_passwd, port)
if replication_manager.has_ipaca():
root_logger.debug('PKI DS found on %s:%s' % (host, port))
return replication_manager
else:
root_logger.debug('PKI tree not found on %s:%s' % (host, port))
sys.exit('Cannot reach PKI DS at %s on ports %s' % (host, ports))
class CSReplicationManager(replication.ReplicationManager):
"""ReplicationManager specific to CA agreements
Note that in most cases we don't know if we're connecting to an old-style
separate PKI DS, or to a host with a merged DB.
Use the get_cs_replication_manager function to determine this and return
an appropriate CSReplicationManager.
"""
def __init__(self, realm, hostname, dirman_passwd, port):
super(CSReplicationManager, self).__init__(
realm, hostname, dirman_passwd, port, starttls=True)
self.suffix = DN(('o', 'ipaca'))
self.hostnames = [] # set before calling or agreement_dn() will fail
def agreement_dn(self, hostname, master=None):
"""
Construct a dogtag replication agreement name. This needs to be much
more agressive than the IPA replication agreements because the name
is different on each side.
hostname is the local hostname, not the remote one, for both sides
NOTE: The agreement number is hardcoded in dogtag as well
TODO: configurable instance name
"""
dn = None
cn = None
instance_name = dogtag.configured_constants(api).PKI_INSTANCE_NAME
# if master is not None we know what dn to return:
if master is not None:
if master is True:
name = "master"
else:
name = "clone"
cn="%sAgreement1-%s-%s" % (name, hostname, instance_name)
dn = DN(('cn', cn), self.replica_dn())
return (cn, dn)
for host in self.hostnames:
for master in ["master", "clone"]:
try:
cn="%sAgreement1-%s-%s" % (master, host, instance_name)
dn = DN(('cn', cn), self.replica_dn())
self.conn.get_entry(dn)
return (cn, dn)
except errors.NotFound:
dn = None
cn = None
raise errors.NotFound(reason='No agreement found for %s' % hostname)
def delete_referral(self, hostname, port):
dn = DN(('cn', self.suffix), ('cn', 'mapping tree'), ('cn', 'config'))
entry = self.conn.get_entry(dn)
try:
# TODO: should we detect proto somehow ?
entry['nsslapd-referral'].remove('ldap://%s/%s' %
(ipautil.format_netloc(hostname, port), self.suffix))
self.conn.update_entry(entry)
except Exception, e:
root_logger.debug("Failed to remove referral value: %s" % e)
def has_ipaca(self):
try:
entry = self.conn.get_entry(self.suffix)
except errors.NotFound:
return False
else:
return True
def parse_options():
from optparse import OptionParser
@@ -219,7 +116,11 @@ def list_replicas(realm, host, replica, dirman_passwd, verbose):
print '%s: %s' % (k, p[0])
return
repl = get_cs_replication_manager(realm, replica, dirman_passwd)
try:
repl = replication.get_cs_replication_manager(realm, replica, dirman_passwd)
except Exception, e:
sys.exit(str(e))
entries = repl.find_replication_agreements()
for entry in entries:
@@ -242,7 +143,7 @@ def del_link(realm, replica1, replica2, dirman_passwd, force=False):
repl2 = None
try:
repl1 = get_cs_replication_manager(realm, replica1, dirman_passwd)
repl1 = replication.get_cs_replication_manager(realm, replica1, dirman_passwd)
repl1.hostnames = [replica1, replica2]
@@ -266,7 +167,7 @@ def del_link(realm, replica1, replica2, dirman_passwd, force=False):
sys.exit("Failed to get data from '%s': %s" % (replica1, e))
try:
repl2 = get_cs_replication_manager(realm, replica2, dirman_passwd)
repl2 = replication.get_cs_replication_manager(realm, replica2, dirman_passwd)
repl2.hostnames = [replica1, replica2]
@@ -335,8 +236,8 @@ def del_master(realm, hostname, options):
# 1. Connect to the local dogtag DS server
try:
thisrepl = get_cs_replication_manager(realm, options.host,
options.dirman_passwd)
thisrepl = replication.get_cs_replication_manager(realm, options.host,
options.dirman_passwd)
except Exception, e:
sys.exit("Failed to connect to server %s: %s" % (options.host, e))
@@ -346,8 +247,8 @@ def del_master(realm, hostname, options):
# 3. Connect to the dogtag DS to be removed.
try:
delrepl = get_cs_replication_manager(realm, hostname,
options.dirman_passwd)
delrepl = replication.get_cs_replication_manager(realm, hostname,
options.dirman_passwd)
except Exception, e:
if not options.force:
print "Unable to delete replica %s: %s" % (hostname, e)
@@ -371,7 +272,11 @@ def del_master(realm, hostname, options):
sys.exit("There were issues removing a connection: %s" % e)
def add_link(realm, replica1, replica2, dirman_passwd, options):
repl2 = get_cs_replication_manager(realm, replica2, dirman_passwd)
try:
repl2 = replication.get_cs_replication_manager(realm, replica2,
dirman_passwd)
except Exception, e:
sys.exit(str(e))
try:
conn = ipaldap.IPAdmin(replica2, 636, cacert=CACERT)
conn.do_simple_bind(bindpw=dirman_passwd)
@@ -388,7 +293,8 @@ def add_link(realm, replica1, replica2, dirman_passwd, options):
sys.exit("Failed to get data while trying to bind to '%s': %s" % (replica1, str(e)))
try:
repl1 = get_cs_replication_manager(realm, replica1, dirman_passwd)
repl1 = replication.get_cs_replication_manager(realm, replica1,
dirman_passwd)
entries = repl1.find_replication_agreements()
for e in entries:
if e.single_value('nsDS5ReplicaHost', None) == replica2:
@@ -414,11 +320,16 @@ def re_initialize(realm, options):
if not options.fromhost:
sys.exit("re-initialize requires the option --from <host name>")
repl = get_cs_replication_manager(realm, options.fromhost,
options.dirman_passwd)
thishost = installutils.get_fqdn()
try:
repl = replication.get_cs_replication_manager(realm, options.fromhost,
options.dirman_passwd)
thisrepl = replication.get_cs_replication_manager(realm, thishost,
options.dirman_passwd)
except Exception, e:
sys.exit(str(e))
filter = repl.get_agreement_filter(host=thishost)
try:
entry = repl.conn.get_entries(
@@ -429,16 +340,21 @@ def re_initialize(realm, options):
if len(entry) > 1:
root_logger.error("Found multiple agreements for %s. Only initializing the first one returned: %s" % (thishost, entry[0].dn))
repl.hostnames = thisrepl.hostnames = [thishost, options.fromhost]
thisrepl.enable_agreement(options.fromhost)
repl.enable_agreement(thishost)
repl.initialize_replication(entry[0].dn, repl.conn)
repl.wait_for_repl_init(repl.conn, entry[0].dn)
def force_sync(realm, thishost, fromhost, dirman_passwd):
repl = get_cs_replication_manager(realm, fromhost, dirman_passwd)
try:
repl = replication.get_cs_replication_manager(realm, fromhost,
dirman_passwd)
repl.force_sync(repl.conn, thishost)
except Exception, e:
sys.exit(e)
sys.exit(str(e))
def main():
options, args = parse_options()

View File

@@ -827,6 +827,10 @@ def re_initialize(realm, thishost, fromhost, dirman_passwd):
else:
repl = replication.ReplicationManager(realm, fromhost, dirman_passwd)
agreement = repl.get_replication_agreement(thishost)
thisrepl.enable_agreement(fromhost)
repl.enable_agreement(thishost)
repl.force_sync(repl.conn, thishost)
repl.initialize_replication(agreement.dn, repl.conn)

23
install/tools/ipa-restore Executable file
View File

@@ -0,0 +1,23 @@
#! /usr/bin/python -E
# Authors: Rob Crittenden <rcritten@redhat.com>
#
# Copyright (C) 2013 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program 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.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
#
from ipaserver.install.ipa_restore import Restore
Restore.run_cli()

View File

@@ -19,7 +19,10 @@ man1_MANS = \
ipa-compat-manage.1 \
ipa-nis-manage.1 \
ipa-managed-entries.1 \
ipa-compliance.1
ipa-compliance.1 \
ipa-backup.1 \
ipa-restore.1 \
$(NULL)
man8_MANS = \
ipactl.8 \

View File

@@ -0,0 +1,84 @@
.\" A man page for ipa-backup
.\" Copyright (C) 2013 Red Hat, Inc.
.\"
.\" This program 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.
.\"
.\" This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
.\"
.\" Author: Rob Crittenden <rcritten@redhat.com>
.\"
.TH "ipa-backup" "1" "Mar 22 2013" "FreeIPA" "FreeIPA Manual Pages"
.SH "NAME"
ipa\-backup \- Back up an IPA master
.SH "SYNOPSIS"
ipa\-backup [\fIOPTION\fR]...
.SH "DESCRIPTION"
Two kinds of backups: full and data\-only.
.TP
The back up is optionally encrypted using either the default root GPG key or a named key. No passphrase is supported.
.TP
Backups are stored in a subdirectory in /var/lib/ipa/backup.
.TP
The naming convention for full backups is ipa\-full\-YEAR\-MM\-DD\-HH\-MM\-SS in the GMT time zone.
.TP
The naming convention for data backups is ipa\-data\-YEAR\-MM\-DD\-HH\-MM\-SS In the GMT time zone.
.TP
Within the subdirectory is file, header, that describes the back up including the type, system, date of backup, the version of IPA, the version of the backup and the services on the master.
.TP
A backup can not be restored on another host.
.TP
A backup can not be restored in a different version of IPA.
.SH "OPTIONS"
.TP
\fB\-\-data\fR
Back up data only. The default is to back up all IPA files plus data.
.TP
\fB\-\-gpg\fR
Encrypt the back up file.
.TP
\fB\-\-gpg\-keyring\fR=\fIGPG_KEYRING\fR
The full path to a GPG keyring. The keyring consists of two files, a public and a private key (.sec and .pub respectively). Specify the path without an extension.
.TP
\fB\-\-logs\fR
Include the IPA service log files in the backup.
.TP
\fB\-\-online\fR
Perform the backup on\-line. Requires the \-\-data option.
.TP
\fB\-\-v\fR, \fB\-\-verbose\fR
Print debugging information
.TP
\fB\-d\fR, \fB\-\-debug\fR
Alias for \-\-verbose
.TP
\fB\-q\fR, \fB\-\-quiet\fR
Output only errors
.TP
\fB\-\-log\-file\fR=\fIFILE\fR
Log to the given file
.SH "EXIT STATUS"
0 if the command was successful
1 if an error occurred
.SH "FILES"
.PP
\fI/var/lib/ipa/backup\fR
.RS 4
The default directory for storing backup files.
.RE
.PP
\fl/var/log/ipabackup.log\fR
.RS 4
The log file for backups
.PP
.SH "SEE ALSO"
ipa\-restore(1).

View File

@@ -0,0 +1,105 @@
.\" A man page for ipa-restore
.\" Copyright (C) 2013 Red Hat, Inc.
.\"
.\" This program 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.
.\"
.\" This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
.\"
.\" Author: Rob Crittenden <rcritten@redhat.com>
.\"
.TH "ipa-restore" "1" "Mar 22 2013" "FreeIPA" "FreeIPA Manual Pages"
.SH "NAME"
ipa\-restore \- Restore an IPA master
.SH "SYNOPSIS"
ipa\-restore [\fIOPTION\fR]... BACKUP
.SH "DESCRIPTION"
Only the name of the backup needs to be passed in, not the full path. Backups are stored in a subdirectory in /var/lib/ipa/backup. If a backup is in another location then the full path must be provided.
.TP
The naming convention for full backups is ipa\-full\-YEAR\-MM\-DD\-HH\-MM\-SS in the GMT time zone.
.TP
The naming convention for data backups is ipa\-data\-YEAR\-MM\-DD\-HH\-MM\-SS In the GMT time zone.
.TP
The type of backup is automatically detected. A data restore can be done from either type.
.TP
\fBWARNING\fR: A full restore will restore files like /etc/passwd, /etc/group, /etc/resolv.conf as well. Any file that IPA may have touched is backed up and restored.
.TP
An encrypted backup is also automatically detected and the root keyring is used by default. The \-\-keyring option can be used to define the full path to the private and public keys.
.TP
Within the subdirectory is file, header, that describes the back up including the type, system, date of backup, the version of IPA, the version of the backup and the services on the master.
.TP
A backup can not be restored on another host.
.TP
A backup can not be restored in a different version of IPA.
.TP
Restoring from backup sets the server as the new data master. All other masters will need to be re\-initialized. The first step in restoring a backup is to disable replication on all the other masters. This is to prevent the changelog from overwriting the data in the backup.
.TP
Use the ipa\-replica\-manage and ipa\-csreplica\-manage commands to re\-initialize other masters. ipa\-csreplica\-manage only needs to be executed on masters that have a CA installed.
.SH "REPLICATION"
The restoration on other masters needs to be done carefully, to match the replication topology, working outward from the restored master. For example, if your topology is A <\-> B <\-> C and you restored master A you would restore B first, then C.
.TP
Replication is disabled on all masters that are available when a restoration is done. If a master is down at the time of the restoration you will need to proceed with extreme caution. If this master is brought back up after the restoration is complete it may send out replication updates that apply the very changes you were trying to back out. The only safe answer is to reinstall the master. This would involve deleting all replication agreements to the master. This could have a cascading effect if the master is a hub to other masters. They would need to be connected to other masters before removing the downed master.
.TP
If the restore point is from a period prior to a replication agreement then the master will need to be re\-installed. For example, you have masters A and B and you create a backup. You then add master C from B. Then you restore from the backup. The restored data is going to lose the replication agreement to C. The master on C will have a replication agreement pointing to B, but B won't have the reverse agreement. Master C won't be registered as an IPA master. It may be possible to manually correct these and re\-connect C to B but it would be very prone to error.
.TP
If re\-initializing on an IPA master version prior to 3.2 then the replication agreements will need to be manually re\-enabled otherwise the re\-initialization will never complete. To manually enable an agreement use ldapsearch to find the agreement name in cn=mapping tree,cn=config. The value of nsds5ReplicaEnabled needs to be on, and enabled on both sides. Remember that CA replication is done through a separate agreement and will need to be updated separately.
.TP
If you have older masters you should consider re\-creating them rather than trying to re\-initialize them.
.SH "OPTIONS"
.TP
\fB\-p\fR, \fB\-\-password\fR=\fIPASSWORD\fR
The Directory Manager password.
\fB\-\-data\fR
Restore the data only. The default is to restore everything in the backup.
.TP
\fB\-\-gpg\-keyring\fR=\fIGPG_KEYRING\fR
The full path to a GPG keyring. The keyring consists of two files, a public and a private key (.sec and .pub respectively). Specify the path without an extension.
.TP
\fB\-\-no\-logs\fR
Exclude the IPA service log files in the backup (if they were backed up). Applicable only with a full backup.
.TP
\fB\-\-online\fR
Perform the restore on\-line. Requires the \-\-data option.
.TP
\fB\-\-instance\fR=\fIINSTANCE\fR
The backend to restore within an instance or instances.
.TP
Restore only the databases in this 389\-ds instance. The default is to restore all found (at most this is the IPA REALM instance and the PKI\-IPA instance).
.TP
\fB\-\-backend\fR=\fIBACKEND\fR
\fB\-\-v\fR, \fB\-\-verbose\fR
Print debugging information
.TP
\fB\-d\fR, \fB\-\-debug\fR
Alias for \-\-verbose
.TP
\fB\-q\fR, \fB\-\-quiet\fR
Output only errors
.TP
\fB\-\-log\-file\fR=\fIFILE\fR
Log to the given file
.SH "EXIT STATUS"
0 if the command was successful
1 if an error occurred
.SH "FILES"
.PP
\fI/var/lib/ipa/backup\fR
.RS 4
The default directory for storing backup files.
.RE
.PP
\fl/var/log/iparestore.log\fR
.RS 4
The log file for restoration
.PP
.SH "SEE ALSO"
ipa\-backup(1).

View File

@@ -0,0 +1,568 @@
#!/usr/bin/python
# Authors: Rob Crittenden <rcritten@redhat.com
#
# Copyright (C) 2013 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program 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.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
#
import os
import sys
import shutil
import tempfile
import time
import pwd
from optparse import OptionGroup
from ConfigParser import SafeConfigParser
from ipalib import api, errors
from ipapython import version
from ipapython.ipautil import run, write_tmp_file
from ipapython import admintool
from ipapython.config import IPAOptionParser
from ipapython.dn import DN
from ipaserver.install.dsinstance import realm_to_serverid, DS_USER
from ipaserver.install.replication import wait_for_task
from ipaserver.install import installutils
from ipapython import services as ipaservices
from ipapython import ipaldap
from ipalib.session import ISO8601_DATETIME_FMT
from ConfigParser import SafeConfigParser
"""
A test gpg can be generated list 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
%pubring /root/backup.pub
%secring /root/backup.sec
%commit
%echo done
EOF
# gpg --batch --gen-key keygen
# gpg --no-default-keyring --secret-keyring /root/backup.sec \
--keyring /root/backup.pub --list-secret-keys
"""
BACKUP_DIR = '/var/lib/ipa/backup'
def encrypt_file(filename, keyring, remove_original=True):
source = filename
dest = filename + '.gpg'
args = ['/usr/bin/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)
(stdout, stderr, rc) = run(args, raiseonerr=False)
if rc != 0:
raise admintool.ScriptError('gpg failed: %s' % stderr)
if remove_original:
os.unlink(source)
return dest
class Backup(admintool.AdminTool):
command_name = 'ipa-backup'
log_file_name = '/var/log/ipabackup.log'
usage = "%prog [options]"
description = "Back up IPA files and databases."
dirs = ('/usr/share/ipa/html',
'/root/.pki',
'/etc/pki-ca',
'/etc/pki/pki-tomcat',
'/etc/sysconfig/pki',
'/etc/httpd/alias',
'/var/lib/pki',
'/var/lib/pki-ca',
'/var/lib/ipa/sysrestore',
'/var/lib/ipa-client/sysrestore',
'/var/lib/sss/pubconf/krb5.include.d',
'/var/lib/authconfig/last',
'/var/lib/certmonger',
'/var/lib/ipa',
'/var/run/dirsrv',
'/var/lock/dirsrv',
)
files = (
'/etc/named.conf',
'/etc/named.keytab',
'/etc/resolv.conf',
'/etc/sysconfig/pki-ca',
'/etc/sysconfig/pki-tomcat',
'/etc/sysconfig/dirsrv',
'/etc/sysconfig/ntpd',
'/etc/sysconfig/krb5kdc',
'/etc/sysconfig/pki/ca/pki-ca',
'/etc/sysconfig/authconfig',
'/etc/pki/nssdb/cert8.db',
'/etc/pki/nssdb/key3.db',
'/etc/pki/nssdb/secmod.db',
'/etc/nsswitch.conf',
'/etc/krb5.keytab',
'/etc/sssd/sssd.conf',
'/etc/openldap/ldap.conf',
'/etc/security/limits.conf',
'/etc/httpd/conf/password.conf',
'/etc/httpd/conf/ipa.keytab',
'/etc/httpd/conf.d/ipa-pki-proxy.conf',
'/etc/httpd/conf.d/ipa-rewrite.conf',
'/etc/httpd/conf.d/nss.conf',
'/etc/httpd/conf.d/ipa.conf',
'/etc/ssh/sshd_config',
'/etc/ssh/ssh_config',
'/etc/krb5.conf',
'/etc/group',
'/etc/passwd',
'/etc/ipa/ca.crt',
'/etc/ipa/default.conf',
'/etc/dirsrv/ds.keytab',
'/etc/ntp.conf',
'/etc/samba/smb.conf',
'/etc/samba/samba.keytab',
'/root/ca-agent.p12',
'/root/cacert.p12',
'/var/kerberos/krb5kdc/kdc.conf',
'/etc/systemd/system/multi-user.target.wants/ipa.service',
'/etc/systemd/system/multi-user.target.wants/sssd.service',
'/etc/systemd/system/multi-user.target.wants/certmonger.service',
'/etc/systemd/system/pki-tomcatd.target.wants/pki-tomcatd@pki-tomcat.service',
'/var/run/ipa/services.list',
)
logs=(
'/var/log/pki-ca',
'/var/log/pki/',
'/var/log/dirsrv/slapd-PKI-IPA',
'/var/log/httpd',
'/var/log/ipaserver-install.log',
'/var/log/kadmind.log',
'/var/log/pki-ca-install.log',
'/var/log/messages',
'/var/log/ipaclient-install.log',
'/var/log/secure',
'/var/log/ipaserver-uninstall.log',
'/var/log/pki-ca-uninstall.log',
'/var/log/ipaclient-uninstall.log',
'/var/named/data/named.run',
)
def __init__(self, options, args):
super(Backup, self).__init__(options, args)
self._conn = None
self.files = list(self.files)
self.dirs = list(self.dirs)
self.logs = list(self.logs)
@classmethod
def add_options(cls, parser):
super(Backup, cls).add_options(parser, debug_option=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",
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.")
def setup_logging(self, log_file_mode='a'):
super(Backup, self).setup_logging(log_file_mode='a')
def validate_options(self):
options = self.options
super(Backup, self).validate_options(needs_root=True)
installutils.check_server_configuration()
if options.gpg_keyring is not None:
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:
self.option_parser.error("You cannot specify --online "
"without --data")
if options.gpg:
tmpfd = write_tmp_file('encryptme')
newfile = encrypt_file(tmpfd.name, options.gpg_keyring, False)
os.unlink(newfile)
if options.data_only and options.logs:
self.option_parser.error("You cannot specify --data "
"with --logs")
def run(self):
options = self.options
super(Backup, self).run()
api.bootstrap(in_server=False, context='backup')
api.finalize()
self.log.info("Preparing backup on %s", api.env.host)
pent = pwd.getpwnam(DS_USER)
self.top_dir = tempfile.mkdtemp("ipa")
os.chown(self.top_dir, pent.pw_uid, pent.pw_gid)
os.chmod(self.top_dir, 0750)
self.dir = os.path.join(self.top_dir, "ipa")
os.mkdir(self.dir, 0750)
os.chown(self.dir, pent.pw_uid, pent.pw_gid)
self.header = os.path.join(self.top_dir, 'header')
cwd = os.getcwd()
try:
dirsrv = ipaservices.knownservices.dirsrv
self.add_instance_specific_data()
# We need the dirsrv running to get the list of services
dirsrv.start(capture_output=False)
self.get_connection()
self.create_header(options.data_only)
if options.data_only:
if not options.online:
self.log.info('Stopping Directory Server')
dirsrv.stop(capture_output=False)
else:
self.log.info('Stopping IPA services')
run(['ipactl', 'stop'])
for instance in [realm_to_serverid(api.env.realm), 'PKI-IPA']:
if os.path.exists('/var/lib/dirsrv/slapd-%s' % instance):
if os.path.exists('/var/lib/dirsrv/slapd-%s/db/ipaca' % instance):
self.db2ldif(instance, 'ipaca', online=options.online)
self.db2ldif(instance, 'userRoot', online=options.online)
self.db2bak(instance, online=options.online)
if not options.data_only:
self.file_backup(options)
self.finalize_backup(options.data_only, options.gpg, options.gpg_keyring)
if options.data_only:
if not options.online:
self.log.info('Starting Directory Server')
dirsrv.start(capture_output=False)
else:
self.log.info('Starting IPA service')
run(['ipactl', 'start'])
finally:
try:
os.chdir(cwd)
except Exception, e:
self.log.error('Cannot change directory to %s: %s' % (cwd, e))
shutil.rmtree(self.top_dir)
def add_instance_specific_data(self):
'''
Add instance-specific files and directories.
NOTE: this adds some things that may not get backed up, like the PKI-IPA
instance.
'''
for dir in [
'/etc/dirsrv/slapd-%s' % realm_to_serverid(api.env.realm),
'/var/lib/dirsrv/scripts-%s' % realm_to_serverid(api.env.realm),
'/var/lib/dirsrv/slapd-%s' % realm_to_serverid(api.env.realm),
'/usr/lib64/dirsrv/slapd-PKI-IPA',
'/usr/lib/dirsrv/slapd-PKI-IPA',
'/etc/dirsrv/slapd-PKI-IPA',
'/var/lib/dirsrv/slapd-PKI-IPA',
self.__find_scripts_dir('PKI-IPA'),
]:
if os.path.exists(dir):
self.dirs.append(dir)
for file in [
'/etc/sysconfig/dirsrv-%s' % realm_to_serverid(api.env.realm),
'/etc/sysconfig/dirsrv-PKI-IPA']:
if os.path.exists(file):
self.files.append(file)
for log in [
'/var/log/dirsrv/slapd-%s' % realm_to_serverid(api.env.realm),]:
self.logs.append(log)
def get_connection(self):
'''
Create an ldapi connection and bind to it using autobind as root.
'''
if self._conn is not None:
return self._conn
self._conn = ipaldap.IPAdmin(host=api.env.host,
ldapi=True,
protocol='ldapi',
realm=api.env.realm)
try:
pw_name = pwd.getpwuid(os.geteuid()).pw_name
self._conn.do_external_bind(pw_name)
except Exception, e:
self.log.error("Unable to bind to LDAP server %s: %s" %
(self._conn.host, e))
return self._conn
def db2ldif(self, instance, backend, online=True):
'''
Create a LDIF backup of the data in this instance.
If executed online create a task and wait for it to complete.
For SELinux reasons this writes out to the 389-ds backup location
and we move it.
'''
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'))
ldifname = '%s-%s.ldif' % (instance, backend)
ldiffile = os.path.join(
'/var/lib/dirsrv/slapd-%s/ldif' % instance,
ldifname)
if online:
conn = self.get_connection()
ent = conn.make_entry(
dn,
{
'objectClass': ['top', 'extensibleObject'],
'cn': [cn],
'nsInstance': [backend],
'nsFilename': [ldiffile],
'nsUseOneFile': ['true'],
'nsExportReplica': ['true'],
}
)
try:
conn.add_entry(ent)
except Exception, e:
raise admintool.ScriptError('Unable to add LDIF task: %s'
% e)
self.log.info("Waiting for LDIF to finish")
wait_for_task(conn, dn)
else:
args = ['%s/db2ldif' % self.__find_scripts_dir(instance),
'-r',
'-n', backend,
'-a', ldiffile]
(stdout, stderr, rc) = run(args, raiseonerr=False)
if rc != 0:
self.log.critical("db2ldif failed: %s", stderr)
# Move the LDIF backup to our location
shutil.move(ldiffile, os.path.join(self.dir, ldifname))
def db2bak(self, instance, online=True):
'''
Create a BAK backup of the data and changelog in this instance.
If executed online create a task and wait for it to complete.
'''
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'))
bakdir = os.path.join('/var/lib/dirsrv/slapd-%s/bak/%s' % (instance, instance))
if online:
conn = self.get_connection()
ent = conn.make_entry(
dn,
{
'objectClass': ['top', 'extensibleObject'],
'cn': [cn],
'nsInstance': ['userRoot'],
'nsArchiveDir': [bakdir],
'nsDatabaseType': ['ldbm database'],
}
)
try:
conn.add_entry(ent)
except Exception, e:
raise admintool.ScriptError('Unable to to add backup task: %s'
% e)
self.log.info("Waiting for BAK to finish")
wait_for_task(conn, dn)
else:
args = ['%s/db2bak' % self.__find_scripts_dir(instance), bakdir]
(stdout, stderr, rc) = run(args, raiseonerr=False)
if rc != 0:
self.log.critical("db2bak failed: %s" % stderr)
shutil.move(bakdir, self.dir)
def file_backup(self, options):
def verify_directories(dirs):
return [s for s in dirs if os.path.exists(s)]
self.log.info("Backing up files")
args = ['tar',
'--xattrs',
'--selinux',
'-czf',
os.path.join(self.dir, 'files.tar')
]
args.extend(verify_directories(self.dirs))
args.extend(verify_directories(self.files))
if options.logs:
args.extend(verify_directories(self.logs))
(stdout, stderr, rc) = run(args, raiseonerr=False)
if rc != 0:
raise admintool.ScriptError('tar returned non-zero %d: %s' % (rc, stdout))
def create_header(self, data_only):
'''
Create the backup file header that contains the meta data about
this particular backup.
'''
config = SafeConfigParser()
config.add_section("ipa")
if data_only:
config.set('ipa', 'type', 'DATA')
else:
config.set('ipa', 'type', 'FULL')
config.set('ipa', 'time', time.strftime(ISO8601_DATETIME_FMT, time.gmtime()))
config.set('ipa', 'host', api.env.host)
config.set('ipa', 'ipa_version', str(version.VERSION))
config.set('ipa', 'version', '1')
dn = DN(('cn', api.env.host), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn)
services_cns = []
try:
conn = self.get_connection()
services = conn.get_entries(dn, conn.SCOPE_ONELEVEL)
except errors.NetworkError:
self.log.critical(
"Unable to obtain list of master services, continuing anyway")
except Exception, 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]
config.set('ipa', 'services', ','.join(services_cns))
with open(self.header, 'w') as fd:
config.write(fd)
def finalize_backup(self, data_only=False, encrypt=False, keyring=None):
'''
Create the final location of the backup files and move the files
we've backed up there, optionally encrypting them.
This is done in a couple of steps. We have a directory that
contains the tarball of the files, a directory that contains
the db2bak output and an LDIF.
These, along with the header, are moved into a new subdirectory
in /var/lib/ipa/backup.
'''
if data_only:
backup_dir = os.path.join(BACKUP_DIR, time.strftime('ipa-data-%Y-%m-%d-%H-%M-%S'))
filename = os.path.join(backup_dir, "ipa-data.tar")
else:
backup_dir = os.path.join(BACKUP_DIR, time.strftime('ipa-full-%Y-%m-%d-%H-%M-%S'))
filename = os.path.join(backup_dir, "ipa-full.tar")
os.mkdir(backup_dir, 0700)
cwd = os.getcwd()
os.chdir(self.dir)
args = ['tar',
'--xattrs',
'--selinux',
'-czf',
filename,
'.'
]
(stdout, stderr, rc) = run(args, raiseonerr=False)
if rc != 0:
raise admintool.ScriptError('tar returned non-zero %d: %s' % (rc, stdout))
if encrypt:
self.log.info('Encrypting %s' % filename)
filename = encrypt_file(filename, keyring)
shutil.move(self.header, backup_dir)
def __find_scripts_dir(self, instance):
"""
IPA stores its 389-ds scripts in a different directory than dogtag
does so we need to probe for it.
"""
if instance != 'PKI-IPA':
return os.path.join('/var/lib/dirsrv', 'scripts-%s' % instance)
else:
if sys.maxsize > 2**32:
libpath = 'lib64'
else:
libpath = 'lib'
return os.path.join('/usr', libpath, 'dirsrv', 'slapd-PKI-IPA')

View File

@@ -0,0 +1,634 @@
#!/usr/bin/python
# Authors: Rob Crittenden <rcritten@redhat.com
#
# Copyright (C) 2013 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program 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.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
#
import os
import sys
import shutil
import glob
import tempfile
import time
import pwd
from optparse import OptionGroup
from ConfigParser import SafeConfigParser
from ipalib import api, errors
from ipapython import version
from ipapython.ipautil import run, user_input
from ipapython import admintool
from ipapython.config import IPAOptionParser
from ipapython.dn import DN
from ipaserver.install.dsinstance import realm_to_serverid, DS_USER
from ipaserver.install.cainstance import PKI_USER
from ipaserver.install.replication import (wait_for_task, ReplicationManager,
CSReplicationManager, get_cs_replication_manager)
from ipaserver.install import installutils
from ipapython import services as ipaservices
from ipapython import ipaldap
from ipapython import version
from ipalib.session import ISO8601_DATETIME_FMT
from ipaserver.install.ipa_backup import BACKUP_DIR
def recursive_chown(path, uid, gid):
'''
Change ownership of all files and directories in a path.
'''
for root, dirs, files in os.walk(path):
for dir in dirs:
os.chown(os.path.join(root, dir), uid, gid)
os.chmod(os.path.join(root, dir), 0750)
for file in files:
os.chown(os.path.join(root, file), uid, gid)
os.chmod(os.path.join(root, file), 0640)
def decrypt_file(tmpdir, filename, keyring):
source = filename
(dest, ext) = os.path.splitext(filename)
if ext != '.gpg':
raise admintool.ScriptError('Trying to decrypt a non-gpg file')
dest = os.path.basename(dest)
dest = os.path.join(tmpdir, dest)
args = ['/usr/bin/gpg',
'--batch',
'-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('-d')
args.append(source)
(stdout, stderr, rc) = run(args, raiseonerr=False)
if rc != 0:
raise admintool.ScriptError('gpg failed: %s' % stderr)
return dest
class Restore(admintool.AdminTool):
command_name = 'ipa-restore'
log_file_name = '/var/log/iparestore.log'
usage = "%prog [options] backup"
description = "Restore IPA files and databases."
def __init__(self, options, args):
super(Restore, self).__init__(options, args)
self._conn = None
@classmethod
def add_options(cls, parser):
super(Restore, cls).add_options(parser, debug_option=True)
parser.add_option("-p", "--password", dest="password",
help="Directory Manager password")
parser.add_option("--gpg-keyring", dest="gpg_keyring",
help="The gpg key name to be used")
parser.add_option("--data", dest="data_only", action="store_true",
default=False, help="Restore only the data")
parser.add_option("--online", dest="online", action="store_true",
default=False, help="Perform the LDAP restores online, for data only.")
parser.add_option("--instance", dest="instance",
help="The 389-ds instance to restore (defaults to all found)")
parser.add_option("--backend", dest="backend",
help="The backend to restore within the instance or instances")
parser.add_option('--no-logs', dest="no_logs", action="store_true",
default=False, help="Do not restore log files from the backup")
parser.add_option('-U', '--unattended', dest="unattended",
action="store_true", default=False,
help="Unattended restoration never prompts the user")
def setup_logging(self, log_file_mode='a'):
super(Restore, self).setup_logging(log_file_mode='a')
def validate_options(self):
options = self.options
super(Restore, self).validate_options(needs_root=True)
if options.data_only:
installutils.check_server_configuration()
if len(self.args) < 1:
self.option_parser.error(
"must provide the backup to restore")
elif len(self.args) > 1:
self.option_parser.error(
"must provide exactly one name for the backup")
dirname = self.args[0]
if not os.path.isabs(dirname):
self.backup_dir = os.path.join(BACKUP_DIR, dirname)
else:
self.backup_dir = dirname
if options.gpg_keyring:
if (not os.path.exists(options.gpg_keyring + '.pub') or
not os.path.exists(options.gpg_keyring + '.sec')):
raise admintool.ScriptError('No such key %s' %
options.gpg_keyring)
def ask_for_options(self):
options = self.options
super(Restore, self).ask_for_options()
# get the directory manager password
self.dirman_password = options.password
if not options.password:
if not options.unattended:
self.dirman_password = installutils.read_password(
"Directory Manager (existing master)",
confirm=False, validate=False)
if self.dirman_password is None:
raise admintool.ScriptError(
"Directory Manager password required")
def run(self):
options = self.options
super(Restore, self).run()
api.bootstrap(in_server=False, context='restore')
api.finalize()
self.log.info("Preparing restore from %s on %s",
self.backup_dir, api.env.host)
if not options.instance:
instances = []
for instance in [realm_to_serverid(api.env.realm), 'PKI-IPA']:
if os.path.exists('/var/lib/dirsrv/slapd-%s' % instance):
instances.append(instance)
else:
instances = [options.instance]
if options.data_only and not instances:
raise admintool.ScriptError('No instances to restore to')
pent = pwd.getpwnam(DS_USER)
# Temporary directory for decrypting files before restoring
self.top_dir = tempfile.mkdtemp("ipa")
os.chown(self.top_dir, pent.pw_uid, pent.pw_gid)
os.chmod(self.top_dir, 0750)
self.dir = os.path.join(self.top_dir, "ipa")
os.mkdir(self.dir, 0750)
os.chown(self.dir, pent.pw_uid, pent.pw_gid)
self.header = os.path.join(self.backup_dir, 'header')
cwd = os.getcwd()
try:
dirsrv = ipaservices.knownservices.dirsrv
self.read_header()
# These two checks would normally be in the validate method but
# we need to know the type of backup we're dealing with.
if (self.backup_type != 'FULL' and not options.data_only and
not instances):
raise admintool.ScriptError('Cannot restore a data backup into an empty system')
if (self.backup_type == 'FULL' and not options.data_only and
(options.instance or options.backend)):
raise admintool.ScriptError('Restore must be in data-only mode when restoring a specific instance or backend.')
if self.backup_host != api.env.host:
self.log.warning('Host name %s does not match backup name %s' %
(api.env.host, self.backup_host))
if (not options.unattended and
not user_input("Continue to restore?", False)):
raise admintool.ScriptError("Aborted")
if self.backup_ipa_version != str(version.VERSION):
self.log.warning(
"Restoring data from a different release of IPA.\n"
"Data is version %s.\n"
"Server is running %s." %
(self.backup_ipa_version, str(version.VERSION)))
if (not options.unattended and
not user_input("Continue to restore?", False)):
raise admintool.ScriptError("Aborted")
# Big fat warning
if (not options.unattended and
not user_input("Restoring data will overwrite existing live data. Continue to restore?", False)):
raise admintool.ScriptError("Aborted")
self.log.info(
"Each master will individually need to be re-initialized or")
self.log.info(
"re-created from this one. The replication agreements on")
self.log.info(
"masters running IPA 3.1 or earlier will need to be manually")
self.log.info(
"re-enabled. See the man page for details.")
self.log.info("Disabling all replication.")
self.disable_agreements()
self.extract_backup(options.gpg_keyring)
if options.data_only:
if not options.online:
self.log.info('Stopping Directory Server')
dirsrv.stop(capture_output=False)
else:
self.log.info('Starting Directory Server')
dirsrv.start(capture_output=False)
else:
self.log.info('Stopping IPA services')
(stdout, stderr, rc) = run(['ipactl', 'stop'], raiseonerr=False)
if rc not in [0, 6]:
self.log.warn('Stopping IPA failed: %s' % stderr)
# We do either a full file restore or we restore data.
if self.backup_type == 'FULL' and not options.data_only:
if options.online:
raise admintool.ScriptError('File restoration cannot be done online.')
self.file_restore(options.no_logs)
if 'CA' in self.backup_services:
self.__create_dogtag_log_dirs()
# Always restore the data from ldif
# If we are restoring PKI-IPA then we need to restore the
# userRoot backend in it and the main IPA instance. If we
# have a unified instance we need to restore both userRoot and
# ipaca.
for instance in instances:
if os.path.exists('/var/lib/dirsrv/slapd-%s' % instance):
if options.backend is None:
self.ldif2db(instance, 'userRoot', online=options.online)
if os.path.exists('/var/lib/dirsrv/slapd-%s/db/ipaca' % instance):
self.ldif2db(instance, 'ipaca', online=options.online)
else:
self.ldif2db(instance, options.backend, online=options.online)
else:
raise admintool.ScriptError('389-ds instance %s does not exist' % instance)
if options.data_only:
if not options.online:
self.log.info('Starting Directory Server')
dirsrv.start(capture_output=False)
else:
# explicitly enable then disable the pki tomcatd service to
# re-register its instance. FIXME, this is really wierd.
ipaservices.knownservices.pki_tomcatd.enable()
ipaservices.knownservices.pki_tomcatd.disable()
self.log.info('Starting IPA services')
run(['ipactl', 'start'])
self.log.info('Restarting SSSD')
sssd = ipaservices.service('sssd')
sssd.restart()
finally:
try:
os.chdir(cwd)
except Exception, e:
self.log.error('Cannot change directory to %s: %s' % (cwd, e))
shutil.rmtree(self.top_dir)
def get_connection(self):
'''
Create an ldapi connection and bind to it using autobind as root.
'''
if self._conn is not None:
return self._conn
self._conn = ipaldap.IPAdmin(host=api.env.host,
ldapi=True,
protocol='ldapi',
realm=api.env.realm)
try:
pw_name = pwd.getpwuid(os.geteuid()).pw_name
self._conn.do_external_bind(pw_name)
except Exception, e:
raise admintool.ScriptError('Unable to bind to LDAP server: %s'
% e)
return self._conn
def disable_agreements(self):
'''
Find all replication agreements on all masters and disable them.
Warn very loudly about any agreements/masters we cannot contact.
'''
try:
conn = self.get_connection()
except Exception, e :
self.log.error('Unable to get connection, skipping disabling agreements: %s' % e)
return
masters = []
dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn)
try:
entries = conn.get_entries(dn, conn.SCOPE_ONELEVEL)
except Exception, e:
raise admintool.ScriptError(
"Failed to read master data: %s" % e)
else:
masters = [ent.single_value('cn') for ent in entries]
for master in masters:
if master == api.env.host:
continue
try:
repl = ReplicationManager(api.env.realm, master,
self.dirman_password)
except Exception, e:
self.log.critical("Unable to disable agreement on %s: %s" % (master, e))
master_dn = DN(('cn', master), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn)
try:
services = repl.conn.get_entries(master_dn,
repl.conn.SCOPE_ONELEVEL)
except errors.NotFound:
continue
services_cns = [s.single_value('cn') for s in services]
hosts = repl.find_ipa_replication_agreements()
for host in hosts:
self.log.info('Disabling replication agreement on %s to %s' % (master, host))
repl.disable_agreement(host)
if 'CA' in services_cns:
try:
repl = get_cs_replication_manager(api.env.realm, master,
self.dirman_password)
except Exception, e:
self.log.critical("Unable to disable agreement on %s: %s" % (master, e))
hosts = repl.find_ipa_replication_agreements()
for host in hosts:
self.log.info('Disabling CA replication agreement on %s to %s' % (master, host))
repl.hostnames = [master, host]
repl.disable_agreement(host)
def ldif2db(self, instance, backend, online=True):
'''
Restore a LDIF backup of the data in this instance.
If executed online create a task and wait for it to complete.
'''
self.log.info('Restoring from %s in %s' % (backend, instance))
now = time.localtime()
cn = time.strftime('import_%Y_%m_%d_%H_%M_%S')
dn = DN(('cn', cn), ('cn', 'import'), ('cn', 'tasks'), ('cn', 'config'))
ldifname = '%s-%s.ldif' % (instance, backend)
ldiffile = os.path.join(self.dir, ldifname)
if online:
conn = self.get_connection()
ent = conn.make_entry(
dn,
{
'objectClass': ['top', 'extensibleObject'],
'cn': [cn],
'nsFilename': [ldiffile],
'nsUseOneFile': ['true'],
}
)
ent['nsInstance'] = [backend]
try:
conn.add_entry(ent)
except Exception, e:
raise admintool.ScriptError(
'Unable to bind to LDAP server: %s' % e)
self.log.info("Waiting for LDIF to finish")
wait_for_task(conn, dn)
else:
args = ['%s/ldif2db' % self.__find_scripts_dir(instance),
'-i', ldiffile]
if backend is not None:
args.append('-n')
args.append(backend)
else:
args.append('-n')
args.append('userRoot')
(stdout, stderr, rc) = run(args, raiseonerr=False)
if rc != 0:
self.log.critical("ldif2db failed: %s" % stderr)
def bak2db(self, instance, backend, online=True):
'''
Restore a BAK backup of the data and changelog in this instance.
If backend is None then all backends are restored.
If executed online create a task and wait for it to complete.
instance here is a loaded term. It can mean either a separate
389-ds install instance or a separate 389-ds backend. We only need
to treat PKI-IPA and ipaca specially.
'''
if backend is not None:
self.log.info('Restoring %s in %s' % (backend, instance))
else:
self.log.info('Restoring %s' % instance)
cn = time.strftime('restore_%Y_%m_%d_%H_%M_%S')
dn = DN(('cn', cn), ('cn', 'restore'), ('cn', 'tasks'), ('cn', 'config'))
if online:
conn = self.get_connection()
ent = conn.make_entry(
dn,
{
'objectClass': ['top', 'extensibleObject'],
'cn': [cn],
'nsArchiveDir': [os.path.join(self.dir, instance)],
'nsDatabaseType': ['ldbm database'],
}
)
if backend is not None:
ent['nsInstance'] = [backend]
try:
conn.add_entry(ent)
except Exception, e:
raise admintool.ScriptError('Unable to bind to LDAP server: %s'
% e)
self.log.info("Waiting for restore to finish")
wait_for_task(conn, dn)
else:
args = ['%s/bak2db' % self.__find_scripts_dir(instance),
os.path.join(self.dir, instance)]
if backend is not None:
args.append('-n')
args.append(backend)
(stdout, stderr, rc) = run(args, raiseonerr=False)
if rc != 0:
self.log.critical("bak2db failed: %s" % stderr)
def file_restore(self, nologs=False):
'''
Restore all the files in the tarball.
This MUST be done offline because we directly backup the 389-ds
databases.
'''
self.log.info("Restoring files")
cwd = os.getcwd()
os.chdir('/')
args = ['tar',
'-xzf',
os.path.join(self.dir, 'files.tar')
]
if nologs:
args.append('--exclude')
args.append('var/log')
(stdout, stderr, rc) = run(args, raiseonerr=False)
if rc != 0:
self.log.critical('Restoring files failed: %s', stderr)
os.chdir(cwd)
def read_header(self):
'''
Read the backup file header that contains the meta data about
this particular backup.
'''
fd = open(self.header)
config = SafeConfigParser()
config.readfp(fd)
self.backup_type = config.get('ipa', 'type')
self.backup_time = config.get('ipa', 'time')
self.backup_host = config.get('ipa', 'host')
self.backup_ipa_version = config.get('ipa', 'ipa_version')
self.backup_version = config.get('ipa', 'version')
self.backup_services = config.get('ipa', 'services')
def extract_backup(self, keyring=None):
'''
Extract the contents of the tarball backup into a temporary location,
decrypting if necessary.
'''
encrypt = False
filename = None
if self.backup_type == 'FULL':
filename = os.path.join(self.backup_dir, 'ipa-full.tar')
else:
filename = os.path.join(self.backup_dir, 'ipa-data.tar')
if not os.path.exists(filename):
if not os.path.exists(filename + '.gpg'):
raise admintool.ScriptError('Unable to find backup file in %s' % self.backup_dir)
else:
filename = filename + '.gpg'
encrypt = True
if encrypt:
self.log.info('Decrypting %s' % filename)
filename = decrypt_file(self.dir, filename, keyring)
cwd = os.getcwd()
os.chdir(self.dir)
args = ['tar',
'-xzf',
filename,
'.'
]
run(args)
pent = pwd.getpwnam(DS_USER)
os.chown(self.top_dir, pent.pw_uid, pent.pw_gid)
recursive_chown(self.dir, pent.pw_uid, pent.pw_gid)
if encrypt:
# We can remove the decoded tarball
os.unlink(filename)
def __find_scripts_dir(self, instance):
"""
IPA stores its 389-ds scripts in a different directory than dogtag
does so we need to probe for it.
"""
if instance != 'PKI-IPA':
return os.path.join('/var/lib/dirsrv', 'scripts-%s' % instance)
else:
if sys.maxsize > 2**32:
libpath = 'lib64'
else:
libpath = 'lib'
return os.path.join('/usr', libpath, 'dirsrv', 'slapd-PKI-IPA')
def __create_dogtag_log_dirs(self):
"""
If we are doing a full restore and the dogtag log directories do
not exist then tomcat will fail to start.
The directory is different depending on whether we have a d9-based
or a d10-based installation. We can tell based on whether there is
a PKI-IPA 389-ds instance.
"""
if os.path.exists('/etc/dirsrv/slapd-PKI-IPA'): # dogtag 9
topdir = '/var/log/pki-ca'
dirs = [topdir,
'/var/log/pki-ca/signedAudit,']
else: # dogtag 10
topdir = '/var/log/pki/pki-tomcat'
dirs = [topdir,
'/var/log/pki/pki-tomcat/ca',
'/var/log/pki/pki-tomcat/ca/archive',
'/var/log/pki/pki-tomcat/ca/signedAudit',]
if os.path.exists(topdir):
return
try:
pent = pwd.getpwnam(PKI_USER)
except KeyError:
self.log.debug("No %s user exists, skipping CA directory creation" % PKI_USER)
return
self.log.debug('Creating log directories for dogtag')
for dir in dirs:
try:
self.log.debug('Creating %s' % dir)
os.mkdir(dir, 0770)
os.chown(dir, pent.pw_uid, pent.pw_gid)
ipaservices.restore_context(dir)
except Exception, e:
# This isn't so fatal as to side-track the restore
self.log.error('Problem with %s: %s' % (dir, e))

View File

@@ -18,6 +18,7 @@
#
import time
import datetime
import sys
import os
@@ -794,7 +795,7 @@ class ReplicationManager(object):
except Exception, e:
root_logger.debug("Failed to remove referral value: %s" % str(e))
def check_repl_init(self, conn, agmtdn):
def check_repl_init(self, conn, agmtdn, start):
done = False
hasError = 0
attrlist = ['cn', 'nsds5BeginReplicaRefresh',
@@ -819,16 +820,20 @@ class ReplicationManager(object):
done = True
hasError = 2
elif status.find("Total update succeeded") > -1:
print "Update succeeded"
print "\nUpdate succeeded"
done = True
elif inprogress.lower() == 'true':
print "Update in progress yet not in progress"
print "\nUpdate in progress yet not in progress"
else:
print "[%s] reports: Update failed! Status: [%s]" % (conn.host, status)
print "\n[%s] reports: Update failed! Status: [%s]" % (conn.host, status)
hasError = 1
done = True
else:
print "Update in progress"
now = datetime.datetime.now()
d = now - start
sys.stdout.write('\r')
sys.stdout.write("Update in progress, %d seconds elapsed" % int(d.total_seconds()))
sys.stdout.flush()
return done, hasError
@@ -873,9 +878,11 @@ class ReplicationManager(object):
def wait_for_repl_init(self, conn, agmtdn):
done = False
haserror = 0
start = datetime.datetime.now()
while not done and not haserror:
time.sleep(1) # give it a few seconds to get going
done, haserror = self.check_repl_init(conn, agmtdn)
done, haserror = self.check_repl_init(conn, agmtdn, start)
print ""
return haserror
def wait_for_repl_update(self, conn, agmtdn, maxtries=600):
@@ -1070,7 +1077,8 @@ class ReplicationManager(object):
self.setup_agreement(r_conn, self.conn.host, isgssapi=True)
def initialize_replication(self, dn, conn):
mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')]
mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start'),
(ldap.MOD_REPLACE, 'nsds5ReplicaEnabled', 'on')]
try:
conn.modify_s(dn, mod)
except ldap.ALREADY_EXISTS:
@@ -1081,9 +1089,10 @@ class ReplicationManager(object):
newschedule = '2358-2359 0'
filter = self.get_agreement_filter(host=hostname)
entries = conn.get_entries(
DN(('cn', 'config')), ldap.SCOPE_SUBTREE, filter)
if len(entries) == 0:
try:
entries = conn.get_entries(
DN(('cn', 'config')), ldap.SCOPE_SUBTREE, filter)
except errors.NotFound:
root_logger.error("Unable to find replication agreement for %s" %
(hostname))
raise RuntimeError("Unable to proceed")
@@ -1405,3 +1414,137 @@ class ReplicationManager(object):
self.conn.update_entry(entry)
return True
def disable_agreement(self, hostname):
"""
Disable the replication agreement to hostname.
"""
cn, dn = self.agreement_dn(hostname)
entry = self.conn.get_entry(dn)
entry['nsds5ReplicaEnabled'] = 'off'
try:
self.conn.update_entry(entry)
except errors.EmptyModlist:
pass
def enable_agreement(self, hostname):
"""
Enable the replication agreement to hostname.
Note: for replication to work it needs to be enabled both ways.
"""
cn, dn = self.agreement_dn(hostname)
entry = self.conn.get_entry(dn)
entry['nsds5ReplicaEnabled'] = 'on'
try:
self.conn.update_entry(entry)
except errors.EmptyModlist:
pass
class CSReplicationManager(ReplicationManager):
"""ReplicationManager specific to CA agreements
Note that in most cases we don't know if we're connecting to an old-style
separate PKI DS, or to a host with a merged DB.
Use the get_cs_replication_manager function to determine this and return
an appropriate CSReplicationManager.
"""
def __init__(self, realm, hostname, dirman_passwd, port):
super(CSReplicationManager, self).__init__(
realm, hostname, dirman_passwd, port, starttls=True)
self.suffix = DN(('o', 'ipaca'))
self.hostnames = [] # set before calling or agreement_dn() will fail
def agreement_dn(self, hostname, master=None):
"""
Construct a dogtag replication agreement name. This needs to be much
more agressive than the IPA replication agreements because the name
is different on each side.
hostname is the local hostname, not the remote one, for both sides
NOTE: The agreement number is hardcoded in dogtag as well
TODO: configurable instance name
"""
dn = None
cn = None
if self.conn.port == 7389:
instance_name = 'pki-ca'
else:
instance_name = dogtag.configured_constants(api).PKI_INSTANCE_NAME
# if master is not None we know what dn to return:
if master is not None:
if master is True:
name = "master"
else:
name = "clone"
cn="%sAgreement1-%s-%s" % (name, hostname, instance_name)
dn = DN(('cn', cn), self.replica_dn())
return (cn, dn)
for host in self.hostnames:
for master in ["master", "clone"]:
try:
cn="%sAgreement1-%s-%s" % (master, host, instance_name)
dn = DN(('cn', cn), self.replica_dn())
self.conn.get_entry(dn)
return (cn, dn)
except errors.NotFound:
dn = None
cn = None
raise errors.NotFound(reason='No agreement found for %s' % hostname)
def delete_referral(self, hostname, port):
dn = DN(('cn', self.suffix), ('cn', 'mapping tree'), ('cn', 'config'))
entry = self.conn.get_entry(dn)
try:
# TODO: should we detect proto somehow ?
entry['nsslapd-referral'].remove('ldap://%s/%s' %
(ipautil.format_netloc(hostname, port), self.suffix))
self.conn.update_entry(entry)
except Exception, e:
root_logger.debug("Failed to remove referral value: %s" % e)
def has_ipaca(self):
try:
entry = self.conn.get_entry(self.suffix)
except errors.NotFound:
return False
else:
return True
def get_cs_replication_manager(realm, host, dirman_passwd):
"""Get a CSReplicationManager for a remote host
Detects if the host has a merged database, connects to appropriate port.
"""
# Try merged database port first. If it has the ipaca tree, return
# corresponding replication manager
# If we can't connect to it at all, we're not dealing with an IPA master
# anyway; let the exception propagate up
# Fall back to the old PKI-only DS port. Check that it has the ipaca tree
# (IPA with merged DB theoretically leaves port 7389 free for anyone).
# If it doesn't, raise exception.
ports = [
dogtag.Dogtag10Constants.DS_PORT,
dogtag.Dogtag9Constants.DS_PORT,
]
for port in ports:
root_logger.debug('Looking for PKI DS on %s:%s' % (host, port))
replication_manager = CSReplicationManager(
realm, host, dirman_passwd, port)
if replication_manager.has_ipaca():
root_logger.debug('PKI DS found on %s:%s' % (host, port))
return replication_manager
else:
root_logger.debug('PKI tree not found on %s:%s' % (host, port))
raise errors.NotFound(reason='Cannot reach PKI DS at %s on ports %s' % (host, ports))