summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRob Crittenden <rcritten@redhat.com>2013-03-13 09:36:41 -0400
committerRob Crittenden <rcritten@redhat.com>2013-04-12 09:59:17 -0400
commitc8694cb19f2b0bd20a0b3fc9df7aacec3b23a928 (patch)
treec13c5965a41c328e9cae04530e550522ae49a678
parentc0cdba78b01317a9ea5c423eda548a69ee046e26 (diff)
downloadfreeipa-c8694cb19f2b0bd20a0b3fc9df7aacec3b23a928.tar.gz
freeipa-c8694cb19f2b0bd20a0b3fc9df7aacec3b23a928.tar.xz
freeipa-c8694cb19f2b0bd20a0b3fc9df7aacec3b23a928.zip
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
-rw-r--r--freeipa.spec.in20
-rw-r--r--install/tools/Makefile.am2
-rwxr-xr-xinstall/tools/ipa-backup23
-rwxr-xr-xinstall/tools/ipa-csreplica-manage150
-rwxr-xr-xinstall/tools/ipa-replica-manage4
-rwxr-xr-xinstall/tools/ipa-restore23
-rw-r--r--install/tools/man/Makefile.am5
-rw-r--r--install/tools/man/ipa-backup.184
-rw-r--r--install/tools/man/ipa-restore.1105
-rw-r--r--ipaserver/install/ipa_backup.py568
-rw-r--r--ipaserver/install/ipa_restore.py634
-rw-r--r--ipaserver/install/replication.py163
12 files changed, 1648 insertions, 133 deletions
diff --git a/freeipa.spec.in b/freeipa.spec.in
index 3461e06a0..7e9e99405 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -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
diff --git a/install/tools/Makefile.am b/install/tools/Makefile.am
index 7f1504cd5..76cd1196b 100644
--- a/install/tools/Makefile.am
+++ b/install/tools/Makefile.am
@@ -22,6 +22,8 @@ sbin_SCRIPTS = \
ipa-ldap-updater \
ipa-upgradeconfig \
ipa-compliance \
+ ipa-backup \
+ ipa-restore \
$(NULL)
EXTRA_DIST = \
diff --git a/install/tools/ipa-backup b/install/tools/ipa-backup
new file mode 100755
index 000000000..5bcaa1d8c
--- /dev/null
+++ b/install/tools/ipa-backup
@@ -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()
diff --git a/install/tools/ipa-csreplica-manage b/install/tools/ipa-csreplica-manage
index cd2fd010b..b9fa05f94 100755
--- a/install/tools/ipa-csreplica-manage
+++ b/install/tools/ipa-csreplica-manage
@@ -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()
diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage
index 636529caa..956fb2f14 100755
--- a/install/tools/ipa-replica-manage
+++ b/install/tools/ipa-replica-manage
@@ -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)
diff --git a/install/tools/ipa-restore b/install/tools/ipa-restore
new file mode 100755
index 000000000..604175be2
--- /dev/null
+++ b/install/tools/ipa-restore
@@ -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()
diff --git a/install/tools/man/Makefile.am b/install/tools/man/Makefile.am
index 8d5d416d9..a1bf076bf 100644
--- a/install/tools/man/Makefile.am
+++ b/install/tools/man/Makefile.am
@@ -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 \
diff --git a/install/tools/man/ipa-backup.1 b/install/tools/man/ipa-backup.1
new file mode 100644
index 000000000..ff9759ec7
--- /dev/null
+++ b/install/tools/man/ipa-backup.1
@@ -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).
diff --git a/install/tools/man/ipa-restore.1 b/install/tools/man/ipa-restore.1
new file mode 100644
index 000000000..31734b259
--- /dev/null
+++ b/install/tools/man/ipa-restore.1
@@ -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).
diff --git a/ipaserver/install/ipa_backup.py b/ipaserver/install/ipa_backup.py
new file mode 100644
index 000000000..54694c99d
--- /dev/null
+++ b/ipaserver/install/ipa_backup.py
@@ -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')
diff --git a/ipaserver/install/ipa_restore.py b/ipaserver/install/ipa_restore.py
new file mode 100644
index 000000000..04d42100c
--- /dev/null
+++ b/ipaserver/install/ipa_restore.py
@@ -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))
diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py
index c1ca0aaa6..64c3902a5 100644
--- a/ipaserver/install/replication.py
+++ b/ipaserver/install/replication.py
@@ -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))