summaryrefslogtreecommitdiffstats
path: root/ipaserver
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 /ipaserver
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
Diffstat (limited to 'ipaserver')
-rw-r--r--ipaserver/install/ipa_backup.py568
-rw-r--r--ipaserver/install/ipa_restore.py634
-rw-r--r--ipaserver/install/replication.py163
3 files changed, 1355 insertions, 10 deletions
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))