summaryrefslogtreecommitdiffstats
path: root/ipaserver/install
diff options
context:
space:
mode:
authorRob Crittenden <rcritten@redhat.com>2009-01-29 16:26:07 -0500
committerRob Crittenden <rcritten@redhat.com>2009-02-03 15:27:14 -0500
commite30cd6ba42c256d2016db45146d616f329455e86 (patch)
treed4c5291095c80c92bc4803fe7f20fc2838124ffa /ipaserver/install
parentc4ed025001895bfc65c613cabbbfcb27c19cc29f (diff)
downloadfreeipa-e30cd6ba42c256d2016db45146d616f329455e86.tar.gz
freeipa-e30cd6ba42c256d2016db45146d616f329455e86.tar.xz
freeipa-e30cd6ba42c256d2016db45146d616f329455e86.zip
Mass tree reorganization for IPAv2. To view previous history of files use:
% git log --follow -- <file> renamed: ipa-server/autogen.sh -> autogen.sh renamed: ipa-server/ipa-kpasswd/Makefile.am -> daemons/ipa-kpasswd/Makefile.am renamed: ipa-server/ipa-kpasswd/README -> daemons/ipa-kpasswd/README renamed: ipa-server/ipa-kpasswd/ipa_kpasswd.c -> daemons/ipa-kpasswd/ipa_kpasswd.c renamed: ipa-server/ipa-kpasswd/ipa_kpasswd.init -> daemons/ipa-kpasswd/ipa_kpasswd.init renamed: ipa-server/ipa-slapi-plugins/Makefile.am -> daemons/ipa-slapi-plugins/Makefile.am renamed: ipa-server/ipa-slapi-plugins/README -> daemons/ipa-slapi-plugins/README renamed: ipa-server/ipa-slapi-plugins/dna/Makefile.am -> daemons/ipa-slapi-plugins/dna/Makefile.am renamed: ipa-server/ipa-slapi-plugins/dna/dna-conf.ldif -> daemons/ipa-slapi-plugins/dna/dna-conf.ldif renamed: ipa-server/ipa-slapi-plugins/dna/dna.c -> daemons/ipa-slapi-plugins/dna/dna.c renamed: ipa-server/ipa-slapi-plugins/ipa-memberof/Makefile.am -> daemons/ipa-slapi-plugins/ipa-memberof/Makefile.am renamed: ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.c -> daemons/ipa-slapi-plugins/ipa-memberof/ipa-memberof.c renamed: ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.h -> daemons/ipa-slapi-plugins/ipa-memberof/ipa-memberof.h renamed: ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof_config.c -> daemons/ipa-slapi-plugins/ipa-memberof/ipa-memberof_config.c renamed: ipa-server/ipa-slapi-plugins/ipa-memberof/memberof-conf.ldif -> daemons/ipa-slapi-plugins/ipa-memberof/memberof-conf.ldif renamed: ipa-server/ipa-slapi-plugins/ipa-pwd-extop/Makefile.am -> daemons/ipa-slapi-plugins/ipa-pwd-extop/Makefile.am renamed: ipa-server/ipa-slapi-plugins/ipa-pwd-extop/README -> daemons/ipa-slapi-plugins/ipa-pwd-extop/README renamed: ipa-server/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c -> daemons/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c renamed: ipa-server/ipa-slapi-plugins/ipa-pwd-extop/pwd-extop-conf.ldif -> daemons/ipa-slapi-plugins/ipa-pwd-extop/pwd-extop-conf.ldif renamed: ipa-server/ipa-slapi-plugins/ipa-winsync/Makefile.am -> daemons/ipa-slapi-plugins/ipa-winsync/Makefile.am renamed: ipa-server/ipa-slapi-plugins/ipa-winsync/README -> daemons/ipa-slapi-plugins/ipa-winsync/README renamed: ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync-conf.ldif -> daemons/ipa-slapi-plugins/ipa-winsync/ipa-winsync-conf.ldif renamed: ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync-config.c -> daemons/ipa-slapi-plugins/ipa-winsync/ipa-winsync-config.c renamed: ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync.c -> daemons/ipa-slapi-plugins/ipa-winsync/ipa-winsync.c renamed: ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync.h -> daemons/ipa-slapi-plugins/ipa-winsync/ipa-winsync.h renamed: ipa-server/xmlrpc-server/ipa-rewrite.conf -> install/conf/ipa-rewrite.conf renamed: ipa-server/xmlrpc-server/ipa.conf -> install/conf/ipa.conf renamed: ipa-server/xmlrpc-server/ssbrowser.html -> install/html/ssbrowser.html renamed: ipa-server/xmlrpc-server/unauthorized.html -> install/html/unauthorized.html renamed: ipa-server/ipa-install/share/60ipaconfig.ldif -> install/share/60ipaconfig.ldif renamed: ipa-server/ipa-install/share/60kerberos.ldif -> install/share/60kerberos.ldif renamed: ipa-server/ipa-install/share/60radius.ldif -> install/share/60radius.ldif renamed: ipa-server/ipa-install/share/60samba.ldif -> install/share/60samba.ldif renamed: ipa-server/ipa-install/share/Makefile.am -> install/share/Makefile.am renamed: ipa-server/ipa-install/share/bind.named.conf.template -> install/share/bind.named.conf.template renamed: ipa-server/ipa-install/share/bind.zone.db.template -> install/share/bind.zone.db.template renamed: ipa-server/ipa-install/share/bootstrap-template.ldif -> install/share/bootstrap-template.ldif renamed: ipa-server/ipa-install/share/certmap.conf.template -> install/share/certmap.conf.template renamed: ipa-server/ipa-install/share/default-aci.ldif -> install/share/default-aci.ldif renamed: ipa-server/ipa-install/share/default-keytypes.ldif -> install/share/default-keytypes.ldif renamed: ipa-server/ipa-install/share/dna-posix.ldif -> install/share/dna-posix.ldif renamed: ipa-server/ipa-install/share/encrypted_attribute.ldif -> install/share/encrypted_attribute.ldif renamed: ipa-server/ipa-install/share/fedora-ds.init.patch -> install/share/fedora-ds.init.patch renamed: ipa-server/ipa-install/share/indices.ldif -> install/share/indices.ldif renamed: ipa-server/ipa-install/share/kdc.conf.template -> install/share/kdc.conf.template renamed: ipa-server/ipa-install/share/kerberos.ldif -> install/share/kerberos.ldif renamed: ipa-server/ipa-install/share/krb.con.template -> install/share/krb.con.template renamed: ipa-server/ipa-install/share/krb5.conf.template -> install/share/krb5.conf.template renamed: ipa-server/ipa-install/share/krb5.ini.template -> install/share/krb5.ini.template renamed: ipa-server/ipa-install/share/krbrealm.con.template -> install/share/krbrealm.con.template renamed: ipa-server/ipa-install/share/master-entry.ldif -> install/share/master-entry.ldif renamed: ipa-server/ipa-install/share/memberof-task.ldif -> install/share/memberof-task.ldif renamed: ipa-server/ipa-install/share/ntp.conf.server.template -> install/share/ntp.conf.server.template renamed: ipa-server/ipa-install/share/ntpd.sysconfig.template -> install/share/ntpd.sysconfig.template renamed: ipa-server/ipa-install/share/preferences.html.template -> install/share/preferences.html.template renamed: ipa-server/ipa-install/share/referint-conf.ldif -> install/share/referint-conf.ldif renamed: ipa-server/ipa-install/share/schema_compat.uldif -> install/share/schema_compat.uldif renamed: ipa-server/ipa-install/share/unique-attributes.ldif -> install/share/unique-attributes.ldif renamed: ipa-server/ipa-install/Makefile.am -> install/tools/Makefile.am renamed: ipa-server/ipa-install/README -> install/tools/README renamed: ipa-server/ipa-compat-manage -> install/tools/ipa-compat-manage renamed: ipa-server/ipa-fix-CVE-2008-3274 -> install/tools/ipa-fix-CVE-2008-3274 renamed: ipa-server/ipa-ldap-updater -> install/tools/ipa-ldap-updater renamed: ipa-server/ipa-install/ipa-replica-install -> install/tools/ipa-replica-install renamed: ipa-server/ipa-install/ipa-replica-manage -> install/tools/ipa-replica-manage renamed: ipa-server/ipa-install/ipa-replica-prepare -> install/tools/ipa-replica-prepare renamed: ipa-server/ipa-install/ipa-server-certinstall -> install/tools/ipa-server-certinstall renamed: ipa-server/ipa-install/ipa-server-install -> install/tools/ipa-server-install renamed: ipa-server/ipa-upgradeconfig -> install/tools/ipa-upgradeconfig renamed: ipa-server/ipa-install/ipactl -> install/tools/ipactl renamed: ipa-server/man/Makefile.am -> install/tools/man/Makefile.am renamed: ipa-server/man/ipa-compat-manage.1 -> install/tools/man/ipa-compat-manage.1 renamed: ipa-server/man/ipa-ldap-updater.1 -> install/tools/man/ipa-ldap-updater.1 renamed: ipa-server/man/ipa-replica-install.1 -> install/tools/man/ipa-replica-install.1 renamed: ipa-server/man/ipa-replica-manage.1 -> install/tools/man/ipa-replica-manage.1 renamed: ipa-server/man/ipa-replica-prepare.1 -> install/tools/man/ipa-replica-prepare.1 renamed: ipa-server/man/ipa-server-certinstall.1 -> install/tools/man/ipa-server-certinstall.1 renamed: ipa-server/man/ipa-server-install.1 -> install/tools/man/ipa-server-install.1 renamed: ipa-server/man/ipa_kpasswd.8 -> install/tools/man/ipa_kpasswd.8 renamed: ipa-server/man/ipa_webgui.8 -> install/tools/man/ipa_webgui.8 renamed: ipa-server/man/ipactl.8 -> install/tools/man/ipactl.8 renamed: ipa-server/ipa-install/updates/Makefile.am -> install/updates/Makefile.am renamed: ipa-server/ipa-install/updates/RFC2307bis.update -> install/updates/RFC2307bis.update renamed: ipa-server/ipa-install/updates/RFC4876.update -> install/updates/RFC4876.update renamed: ipa-server/ipa-install/updates/indices.update -> install/updates/indices.update renamed: ipa-server/ipa-install/updates/nss_ldap.update -> install/updates/nss_ldap.update renamed: ipa-server/ipa-install/updates/replication.update -> install/updates/replication.update renamed: ipa-server/ipa-install/updates/winsync_index.update -> install/updates/winsync_index.update renamed: ipa-server/ipaserver/Makefile.am -> ipaserver/install/Makefile.am renamed: ipa-server/ipaserver/__init__.py -> ipaserver/install/__init__.py renamed: ipa-server/ipaserver/bindinstance.py -> ipaserver/install/bindinstance.py renamed: ipa-server/ipaserver/certs.py -> ipaserver/install/certs.py renamed: ipa-server/ipaserver/dsinstance.py -> ipaserver/install/dsinstance.py renamed: ipa-server/ipaserver/httpinstance.py -> ipaserver/install/httpinstance.py renamed: ipa-server/ipaserver/installutils.py -> ipaserver/install/installutils.py renamed: ipa-server/ipaserver/ipaldap.py -> ipaserver/install/ipaldap.py renamed: ipa-server/ipaserver/krbinstance.py -> ipaserver/install/krbinstance.py renamed: ipa-server/ipaserver/ldapupdate.py -> ipaserver/install/ldapupdate.py renamed: ipa-server/ipaserver/ntpinstance.py -> ipaserver/install/ntpinstance.py renamed: ipa-server/ipaserver/replication.py -> ipaserver/install/replication.py renamed: ipa-server/ipaserver/service.py -> ipaserver/install/service.py renamed: ipa-server/selinux/Makefile -> selinux/Makefile renamed: ipa-server/selinux/ipa-server-selinux.spec.in -> selinux/ipa-server-selinux.spec.in renamed: ipa-server/selinux/ipa_kpasswd/ipa_kpasswd.fc -> selinux/ipa_kpasswd/ipa_kpasswd.fc renamed: ipa-server/selinux/ipa_kpasswd/ipa_kpasswd.te -> selinux/ipa_kpasswd/ipa_kpasswd.te renamed: ipa-server/selinux/ipa_webgui/ipa_webgui.fc -> selinux/ipa_webgui/ipa_webgui.fc renamed: ipa-server/selinux/ipa_webgui/ipa_webgui.te -> selinux/ipa_webgui/ipa_webgui.te renamed: ipa-server/version.m4.in -> version.m4.in
Diffstat (limited to 'ipaserver/install')
-rw-r--r--ipaserver/install/Makefile.am24
-rw-r--r--ipaserver/install/__init__.py21
-rw-r--r--ipaserver/install/bindinstance.py156
-rw-r--r--ipaserver/install/certs.py424
-rw-r--r--ipaserver/install/dsinstance.py479
-rw-r--r--ipaserver/install/httpinstance.py231
-rw-r--r--ipaserver/install/installutils.py248
-rw-r--r--ipaserver/install/ipaldap.py701
-rw-r--r--ipaserver/install/krbinstance.py428
-rwxr-xr-xipaserver/install/ldapupdate.py593
-rw-r--r--ipaserver/install/ntpinstance.py107
-rw-r--r--ipaserver/install/replication.py532
-rw-r--r--ipaserver/install/service.py169
13 files changed, 4113 insertions, 0 deletions
diff --git a/ipaserver/install/Makefile.am b/ipaserver/install/Makefile.am
new file mode 100644
index 000000000..999dcf248
--- /dev/null
+++ b/ipaserver/install/Makefile.am
@@ -0,0 +1,24 @@
+NULL =
+
+appdir = $(pythondir)/ipaserver
+app_PYTHON = \
+ __init__.py \
+ bindinstance.py \
+ dsinstance.py \
+ ipaldap.py \
+ krbinstance.py \
+ httpinstance.py \
+ ntpinstance.py \
+ service.py \
+ installutils.py \
+ replication.py \
+ certs.py \
+ ldapupdate.py \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipaserver/install/__init__.py b/ipaserver/install/__init__.py
new file mode 100644
index 000000000..ef86f9ec5
--- /dev/null
+++ b/ipaserver/install/__init__.py
@@ -0,0 +1,21 @@
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+# see inline
+#
+# Copyright (C) 2007 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; version 2 or later
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+__all__ = ["dsinstance", "krbinstance"]
diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py
new file mode 100644
index 000000000..5badf8603
--- /dev/null
+++ b/ipaserver/install/bindinstance.py
@@ -0,0 +1,156 @@
+# Authors: Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import string
+import tempfile
+import shutil
+import os
+import socket
+import logging
+
+import service
+from ipa import sysrestore
+from ipa import ipautil
+
+def check_inst():
+ # So far this file is always present in both RHEL5 and Fedora if all the necessary
+ # bind packages are installed (RHEL5 requires also the pkg: caching-nameserver)
+ if not os.path.exists('/etc/named.rfc1912.zones'):
+ return False
+
+ return True
+
+class BindInstance(service.Service):
+ def __init__(self, fstore=None):
+ service.Service.__init__(self, "named")
+ self.fqdn = None
+ self.domain = None
+ self.host = None
+ self.ip_address = None
+ self.realm = None
+ self.sub_dict = None
+
+ if fstore:
+ self.fstore = fstore
+ else:
+ self.fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore')
+
+ def setup(self, fqdn, ip_address, realm_name, domain_name):
+ self.fqdn = fqdn
+ self.ip_address = ip_address
+ self.realm = realm_name
+ self.domain = domain_name
+ self.host = fqdn.split(".")[0]
+
+ self.__setup_sub_dict()
+
+ def create_sample_bind_zone(self):
+ bind_txt = ipautil.template_file(ipautil.SHARE_DIR + "bind.zone.db.template", self.sub_dict)
+ [bind_fd, bind_name] = tempfile.mkstemp(".db","sample.zone.")
+ os.write(bind_fd, bind_txt)
+ os.close(bind_fd)
+ print "Sample zone file for bind has been created in "+bind_name
+
+ def create_instance(self):
+
+ try:
+ self.stop()
+ except:
+ pass
+
+ self.step("Setting up our zone", self.__setup_zone)
+ self.step("Setting up named.conf", self.__setup_named_conf)
+
+ self.step("restarting named", self.__start)
+ self.step("configuring named to start on boot", self.__enable)
+
+ self.step("Changing resolv.conf to point to ourselves", self.__setup_resolv_conf)
+ self.start_creation("Configuring bind:")
+
+ def __start(self):
+ try:
+ self.backup_state("running", self.is_running())
+ self.restart()
+ except:
+ print "named service failed to start"
+
+ def __enable(self):
+ self.backup_state("enabled", self.is_running())
+ self.chkconfig_on()
+
+ def __setup_sub_dict(self):
+ self.sub_dict = dict(FQDN=self.fqdn,
+ IP=self.ip_address,
+ DOMAIN=self.domain,
+ HOST=self.host,
+ REALM=self.realm)
+
+ def __setup_zone(self):
+ self.backup_state("domain", self.domain)
+ zone_txt = ipautil.template_file(ipautil.SHARE_DIR + "bind.zone.db.template", self.sub_dict)
+ self.fstore.backup_file('/var/named/'+self.domain+'.zone.db')
+ zone_fd = open('/var/named/'+self.domain+'.zone.db', 'w')
+ zone_fd.write(zone_txt)
+ zone_fd.close()
+
+ def __setup_named_conf(self):
+ self.fstore.backup_file('/etc/named.conf')
+ named_txt = ipautil.template_file(ipautil.SHARE_DIR + "bind.named.conf.template", self.sub_dict)
+ named_fd = open('/etc/named.conf', 'w')
+ named_fd.seek(0)
+ named_fd.truncate(0)
+ named_fd.write(named_txt)
+ named_fd.close()
+
+ def __setup_resolv_conf(self):
+ self.fstore.backup_file('/etc/resolv.conf')
+ resolv_txt = "search "+self.domain+"\nnameserver "+self.ip_address+"\n"
+ resolv_fd = open('/etc/resolv.conf', 'w')
+ resolv_fd.seek(0)
+ resolv_fd.truncate(0)
+ resolv_fd.write(resolv_txt)
+ resolv_fd.close()
+
+ def uninstall(self):
+ running = self.restore_state("running")
+ enabled = self.restore_state("enabled")
+ domain = self.restore_state("domain")
+
+ if not running is None:
+ self.stop()
+
+ if not domain is None:
+ try:
+ self.fstore.restore_file(os.path.join ("/var/named/", domain + ".zone.db"))
+ except ValueError, error:
+ logging.debug(error)
+ pass
+
+ for f in ["/etc/named.conf", "/etc/resolv.conf"]:
+ try:
+ self.fstore.restore_file(f)
+ except ValueError, error:
+ logging.debug(error)
+ pass
+
+ if not enabled is None and not enabled:
+ self.chkconfig_off()
+
+ if not running is None and running:
+ self.start()
diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py
new file mode 100644
index 000000000..8cb1d0883
--- /dev/null
+++ b/ipaserver/install/certs.py
@@ -0,0 +1,424 @@
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os, stat, subprocess, re
+import sha
+import errno
+import tempfile
+import shutil
+
+from ipa import sysrestore
+from ipa import ipautil
+
+CA_SERIALNO="/var/lib/ipa/ca_serialno"
+
+class CertDB(object):
+ def __init__(self, dir, fstore=None):
+ self.secdir = dir
+
+ self.noise_fname = self.secdir + "/noise.txt"
+ self.passwd_fname = self.secdir + "/pwdfile.txt"
+ self.certdb_fname = self.secdir + "/cert8.db"
+ self.keydb_fname = self.secdir + "/key3.db"
+ self.secmod_fname = self.secdir + "/secmod.db"
+ self.cacert_fname = self.secdir + "/cacert.asc"
+ self.pk12_fname = self.secdir + "/cacert.p12"
+ self.pin_fname = self.secdir + "/pin.txt"
+ self.reqdir = tempfile.mkdtemp('', 'ipa-', '/var/lib/ipa')
+ self.certreq_fname = self.reqdir + "/tmpcertreq"
+ self.certder_fname = self.reqdir + "/tmpcert.der"
+
+ # Making this a starting value that will generate
+ # unique values for the current DB is the
+ # responsibility of the caller for now. In the
+ # future we might automatically determine this
+ # for a given db.
+ self.cur_serial = -1
+
+ self.cacert_name = "CA certificate"
+ self.valid_months = "120"
+ self.keysize = "1024"
+
+ # We are going to set the owner of all of the cert
+ # files to the owner of the containing directory
+ # instead of that of the process. This works when
+ # this is called by root for a daemon that runs as
+ # a normal user
+ mode = os.stat(self.secdir)
+ self.uid = mode[stat.ST_UID]
+ self.gid = mode[stat.ST_GID]
+
+ if fstore:
+ self.fstore = fstore
+ else:
+ self.fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore')
+
+ def __del__(self):
+ shutil.rmtree(self.reqdir, ignore_errors=True)
+
+ def set_serial_from_pkcs12(self):
+ """A CA cert was loaded from a PKCS#12 file. Set up our serial file"""
+
+ self.cur_serial = self.find_cacert_serial()
+ try:
+ f=open(CA_SERIALNO,"w")
+ f.write(str(self.cur_serial))
+ f.close()
+ except IOError, e:
+ raise RuntimeError("Unable to increment serial number: %s" % str(e))
+
+ def next_serial(self):
+ try:
+ f=open(CA_SERIALNO,"r")
+ r = f.readline()
+ try:
+ self.cur_serial = int(r) + 1
+ except ValueError:
+ raise RuntimeError("The value in %s is not an integer" % CA_SERIALNO)
+ f.close()
+ except IOError, e:
+ if e.errno == errno.ENOENT:
+ self.cur_serial = 1000
+ f=open(CA_SERIALNO,"w")
+ f.write(str(self.cur_serial))
+ f.close()
+ else:
+ raise RuntimeError("Unable to determine serial number: %s" % str(e))
+
+ try:
+ f=open(CA_SERIALNO,"w")
+ f.write(str(self.cur_serial))
+ f.close()
+ except IOError, e:
+ raise RuntimeError("Unable to increment serial number: %s" % str(e))
+
+ return str(self.cur_serial)
+
+ def set_perms(self, fname, write=False):
+ os.chown(fname, self.uid, self.gid)
+ perms = stat.S_IRUSR
+ if write:
+ perms |= stat.S_IWUSR
+ os.chmod(fname, perms)
+
+ def gen_password(self):
+ return sha.sha(ipautil.ipa_generate_password()).hexdigest()
+
+ def run_certutil(self, args, stdin=None):
+ new_args = ["/usr/bin/certutil", "-d", self.secdir]
+ new_args = new_args + args
+ return ipautil.run(new_args, stdin)
+
+ def run_signtool(self, args, stdin=None):
+ new_args = ["/usr/bin/signtool", "-d", self.secdir]
+ new_args = new_args + args
+ ipautil.run(new_args, stdin)
+
+ def create_noise_file(self):
+ ipautil.backup_file(self.noise_fname)
+ f = open(self.noise_fname, "w")
+ f.write(self.gen_password())
+ self.set_perms(self.noise_fname)
+
+ def create_passwd_file(self, passwd=None):
+ ipautil.backup_file(self.passwd_fname)
+ f = open(self.passwd_fname, "w")
+ if passwd is not None:
+ f.write("%s\n" % passwd)
+ else:
+ f.write(self.gen_password())
+ f.close()
+ self.set_perms(self.passwd_fname)
+
+ def create_certdbs(self):
+ ipautil.backup_file(self.certdb_fname)
+ ipautil.backup_file(self.keydb_fname)
+ ipautil.backup_file(self.secmod_fname)
+ self.run_certutil(["-N",
+ "-f", self.passwd_fname])
+ self.set_perms(self.passwd_fname, write=True)
+
+ def create_ca_cert(self):
+ # Generate the encryption key
+ self.run_certutil(["-G", "-z", self.noise_fname, "-f", self.passwd_fname])
+ # Generate the self-signed cert
+ self.run_certutil(["-S", "-n", self.cacert_name,
+ "-s", "cn=IPA Test Certificate Authority",
+ "-x",
+ "-t", "CT,,C",
+ "-m", self.next_serial(),
+ "-v", self.valid_months,
+ "-z", self.noise_fname,
+ "-f", self.passwd_fname])
+
+ def export_ca_cert(self, nickname, create_pkcs12=False):
+ """create_pkcs12 tells us whether we should create a PKCS#12 file
+ of the CA or not. If we are running on a replica then we won't
+ have the private key to make a PKCS#12 file so we don't need to
+ do that step."""
+ # export the CA cert for use with other apps
+ ipautil.backup_file(self.cacert_fname)
+ self.run_certutil(["-L", "-n", nickname,
+ "-a",
+ "-o", self.cacert_fname])
+ self.set_perms(self.cacert_fname)
+ if create_pkcs12:
+ ipautil.backup_file(self.pk12_fname)
+ ipautil.run(["/usr/bin/pk12util", "-d", self.secdir,
+ "-o", self.pk12_fname,
+ "-n", self.cacert_name,
+ "-w", self.passwd_fname,
+ "-k", self.passwd_fname])
+ self.set_perms(self.pk12_fname)
+
+ def load_cacert(self, cacert_fname):
+ self.run_certutil(["-A", "-n", self.cacert_name,
+ "-t", "CT,,C",
+ "-a",
+ "-i", cacert_fname])
+
+ def find_cacert_serial(self):
+ (out,err) = self.run_certutil(["-L", "-n", self.cacert_name])
+ data = out.split('\n')
+ for line in data:
+ x = re.match(r'\s+Serial Number: (\d+) .*', line)
+ if x is not None:
+ return x.group(1)
+
+ raise RuntimeError("Unable to find serial number")
+
+ def create_server_cert(self, nickname, name, other_certdb=None):
+ cdb = other_certdb
+ if not cdb:
+ cdb = self
+ self.request_cert(name)
+ cdb.issue_server_cert(self.certreq_fname, self.certder_fname)
+ self.add_cert(self.certder_fname, nickname)
+ os.unlink(self.certreq_fname)
+ os.unlink(self.certder_fname)
+
+ def create_signing_cert(self, nickname, name, other_certdb=None):
+ cdb = other_certdb
+ if not cdb:
+ cdb = self
+ self.request_cert(name)
+ cdb.issue_signing_cert(self.certreq_fname, self.certder_fname)
+ self.add_cert(self.certder_fname, nickname)
+ os.unlink(self.certreq_fname)
+ os.unlink(self.certder_fname)
+
+ def request_cert(self, name):
+ self.run_certutil(["-R", "-s", name,
+ "-o", self.certreq_fname,
+ "-g", self.keysize,
+ "-z", self.noise_fname,
+ "-f", self.passwd_fname])
+
+ def issue_server_cert(self, certreq_fname, cert_fname):
+ p = subprocess.Popen(["/usr/bin/certutil",
+ "-d", self.secdir,
+ "-C", "-c", self.cacert_name,
+ "-i", certreq_fname,
+ "-o", cert_fname,
+ "-m", self.next_serial(),
+ "-v", self.valid_months,
+ "-f", self.passwd_fname,
+ "-1", "-5"],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+
+ # Bah - this sucks, but I guess it isn't possible to fully
+ # control this with command line arguments.
+ #
+ # What this is requesting is:
+ # -1 (Create key usage extension)
+ # 2 - Key encipherment
+ # 9 - done
+ # n - not critical
+ #
+ # -5 (Create netscape cert type extension)
+ # 1 - SSL Server
+ # 9 - done
+ # n - not critical
+ p.stdin.write("2\n9\nn\n1\n9\nn\n")
+ p.wait()
+
+ def issue_signing_cert(self, certreq_fname, cert_fname):
+ p = subprocess.Popen(["/usr/bin/certutil",
+ "-d", self.secdir,
+ "-C", "-c", self.cacert_name,
+ "-i", certreq_fname,
+ "-o", cert_fname,
+ "-m", self.next_serial(),
+ "-v", self.valid_months,
+ "-f", self.passwd_fname,
+ "-1", "-5"],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+
+ # Bah - this sucks, but I guess it isn't possible to fully
+ # control this with command line arguments.
+ #
+ # What this is requesting is:
+ # -1 (Create key usage extension)
+ # 0 - Digital Signature
+ # 5 - Cert signing key
+ # 9 - done
+ # n - not critical
+ #
+ # -5 (Create netscape cert type extension)
+ # 3 - Object Signing
+ # 9 - done
+ # n - not critical
+ p.stdin.write("0\n5\n9\nn\n3\n9\nn\n")
+ p.wait()
+
+ def add_cert(self, cert_fname, nickname):
+ self.run_certutil(["-A", "-n", nickname,
+ "-t", "u,u,u",
+ "-i", cert_fname,
+ "-f", cert_fname])
+
+ def create_pin_file(self):
+ ipautil.backup_file(self.pin_fname)
+ f = open(self.pin_fname, "w")
+ f.write("Internal (Software) Token:")
+ pwd = open(self.passwd_fname)
+ f.write(pwd.read())
+ f.close()
+ self.set_perms(self.pin_fname)
+
+ def find_root_cert(self, nickname):
+ p = subprocess.Popen(["/usr/bin/certutil", "-d", self.secdir,
+ "-O", "-n", nickname], stdout=subprocess.PIPE)
+
+ chain = p.stdout.read()
+ chain = chain.split("\n")
+
+ root_nickname = re.match('\ *"(.*)".*', chain[0]).groups()[0]
+
+ return root_nickname
+
+ def trust_root_cert(self, nickname):
+ root_nickname = self.find_root_cert(nickname)
+
+ self.run_certutil(["-M", "-n", root_nickname,
+ "-t", "CT,CT,"])
+
+ def find_server_certs(self):
+ p = subprocess.Popen(["/usr/bin/certutil", "-d", self.secdir,
+ "-L"], stdout=subprocess.PIPE)
+
+ certs = p.stdout.read()
+
+ certs = certs.split("\n")
+
+ server_certs = []
+
+ for cert in certs:
+ fields = cert.split()
+ if not len(fields):
+ continue
+ flags = fields[-1]
+ if 'u' in flags:
+ name = " ".join(fields[0:-1])
+ # NSS 3.12 added a header to the certutil output
+ if name == "Certificate Nickname Trust":
+ continue
+ server_certs.append((name, flags))
+
+ return server_certs
+
+ def import_pkcs12(self, pkcs12_fname, passwd_fname=None):
+ args = ["/usr/bin/pk12util", "-d", self.secdir,
+ "-i", pkcs12_fname,
+ "-k", self.passwd_fname]
+ if passwd_fname:
+ args = args + ["-w", passwd_fname]
+ try:
+ ipautil.run(args)
+ except ipautil.CalledProcessError, e:
+ if e.returncode == 17:
+ raise RuntimeError("incorrect password")
+ else:
+ raise RuntimeError("unknown error import pkcs#12 file")
+
+ def export_pkcs12(self, pkcs12_fname, pkcs12_pwd_fname, nickname="CA certificate"):
+ ipautil.run(["/usr/bin/pk12util", "-d", self.secdir,
+ "-o", pkcs12_fname,
+ "-n", nickname,
+ "-k", self.passwd_fname,
+ "-w", pkcs12_pwd_fname])
+
+ def create_self_signed(self, passwd=None):
+ self.create_noise_file()
+ self.create_passwd_file(passwd)
+ self.create_certdbs()
+ self.create_ca_cert()
+ self.export_ca_cert(self.cacert_name, True)
+ self.create_pin_file()
+
+ def create_from_cacert(self, cacert_fname, passwd=""):
+ self.create_noise_file()
+ self.create_passwd_file(passwd)
+ self.create_certdbs()
+ self.load_cacert(cacert_fname)
+
+ def create_from_pkcs12(self, pkcs12_fname, pkcs12_pwd_fname, passwd=None):
+ """Create a new NSS database using the certificates in a PKCS#12 file.
+
+ pkcs12_fname: the filename of the PKCS#12 file
+ pkcs12_pwd_fname: the file containing the pin for the PKCS#12 file
+ nickname: the nickname/friendly-name of the cert we are loading
+ passwd: The password to use for the new NSS database we are creating
+ """
+ self.create_noise_file()
+ self.create_passwd_file(passwd)
+ self.create_certdbs()
+ self.import_pkcs12(pkcs12_fname, pkcs12_pwd_fname)
+ server_certs = self.find_server_certs()
+ if len(server_certs) == 0:
+ raise RuntimeError("Could not find a suitable server cert in import in %s" % pkcs12_fname)
+
+ # We only handle one server cert
+ nickname = server_certs[0][0]
+
+ self.cacert_name = self.find_root_cert(nickname)
+ self.trust_root_cert(nickname)
+ self.create_pin_file()
+ self.export_ca_cert(self.cacert_name, False)
+
+ # This file implies that we have our own self-signed CA. Ensure
+ # that it no longer exists (from previous installs, for example).
+ try:
+ os.remove(CA_SERIALNO)
+ except:
+ pass
+
+ def backup_files(self):
+ self.fstore.backup_file(self.noise_fname)
+ self.fstore.backup_file(self.passwd_fname)
+ self.fstore.backup_file(self.certdb_fname)
+ self.fstore.backup_file(self.keydb_fname)
+ self.fstore.backup_file(self.secmod_fname)
+ self.fstore.backup_file(self.cacert_fname)
+ self.fstore.backup_file(self.pk12_fname)
+ self.fstore.backup_file(self.pin_fname)
+ self.fstore.backup_file(self.certreq_fname)
+ self.fstore.backup_file(self.certder_fname)
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
new file mode 100644
index 000000000..e9826bf68
--- /dev/null
+++ b/ipaserver/install/dsinstance.py
@@ -0,0 +1,479 @@
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+# Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import shutil
+import logging
+import pwd
+import glob
+import sys
+import os
+import re
+import time
+import tempfile
+import stat
+
+from ipa import ipautil
+
+import service
+import installutils
+import certs
+import ipaldap, ldap
+from ipaserver import ldapupdate
+
+SERVER_ROOT_64 = "/usr/lib64/dirsrv"
+SERVER_ROOT_32 = "/usr/lib/dirsrv"
+
+def realm_to_suffix(realm_name):
+ s = realm_name.split(".")
+ terms = ["dc=" + x.lower() for x in s]
+ return ",".join(terms)
+
+def find_server_root():
+ if ipautil.dir_exists(SERVER_ROOT_64):
+ return SERVER_ROOT_64
+ else:
+ return SERVER_ROOT_32
+
+def realm_to_serverid(realm_name):
+ return "-".join(realm_name.split("."))
+
+def config_dirname(serverid):
+ return "/etc/dirsrv/slapd-" + serverid + "/"
+
+def schema_dirname(serverid):
+ return config_dirname(serverid) + "/schema/"
+
+def erase_ds_instance_data(serverid):
+ try:
+ shutil.rmtree("/etc/dirsrv/slapd-%s" % serverid)
+ except:
+ pass
+ try:
+ shutil.rmtree("/usr/lib/dirsrv/slapd-%s" % serverid)
+ except:
+ pass
+ try:
+ shutil.rmtree("/usr/lib64/dirsrv/slapd-%s" % serverid)
+ except:
+ pass
+ try:
+ shutil.rmtree("/var/lib/dirsrv/slapd-%s" % serverid)
+ except:
+ pass
+ try:
+ shutil.rmtree("/var/lock/dirsrv/slapd-%s" % serverid)
+ except:
+ pass
+# try:
+# shutil.rmtree("/var/log/dirsrv/slapd-%s" % serverid)
+# except:
+# pass
+
+def check_existing_installation():
+ dirs = glob.glob("/etc/dirsrv/slapd-*")
+ if not dirs:
+ return []
+
+ serverids = []
+ for d in dirs:
+ serverids.append(os.path.basename(d).split("slapd-", 1)[1])
+
+ return serverids
+
+def check_ports():
+ ds_unsecure = installutils.port_available(389)
+ ds_secure = installutils.port_available(636)
+ return (ds_unsecure, ds_secure)
+
+def is_ds_running():
+ """The DS init script always returns 0 when requesting status so it cannot
+ be used to determine if the server is running. We have to look at the
+ output.
+ """
+ ret = True
+ try:
+ (sout, serr) = ipautil.run(["/sbin/service", "dirsrv", "status"])
+ if sout.find("is stopped") >= 0:
+ ret = False
+ except ipautil.CalledProcessError:
+ ret = False
+ return ret
+
+
+INF_TEMPLATE = """
+[General]
+FullMachineName= $FQHN
+SuiteSpotUserID= $USER
+ServerRoot= $SERVER_ROOT
+[slapd]
+ServerPort= 389
+ServerIdentifier= $SERVERID
+Suffix= $SUFFIX
+RootDN= cn=Directory Manager
+RootDNPwd= $PASSWORD
+InstallLdifFile= /var/lib/dirsrv/boot.ldif
+"""
+
+BASE_TEMPLATE = """
+dn: $SUFFIX
+objectClass: top
+objectClass: domain
+objectClass: pilotObject
+dc: $BASEDC
+info: IPA V1.0
+"""
+
+class DsInstance(service.Service):
+ def __init__(self, realm_name=None, domain_name=None, dm_password=None):
+ service.Service.__init__(self, "dirsrv")
+ self.realm_name = realm_name
+ self.dm_password = dm_password
+ self.sub_dict = None
+ self.domain = domain_name
+ self.serverid = None
+ self.host_name = None
+ self.pkcs12_info = None
+ self.ds_user = None
+ if realm_name:
+ self.suffix = realm_to_suffix(self.realm_name)
+ self.__setup_sub_dict()
+ else:
+ self.suffix = None
+
+ def create_instance(self, ds_user, realm_name, host_name, domain_name, dm_password, pkcs12_info=None):
+ self.ds_user = ds_user
+ self.realm_name = realm_name.upper()
+ self.serverid = realm_to_serverid(self.realm_name)
+ self.suffix = realm_to_suffix(self.realm_name)
+ self.host_name = host_name
+ self.dm_password = dm_password
+ self.domain = domain_name
+ self.pkcs12_info = pkcs12_info
+ self.__setup_sub_dict()
+
+ self.step("creating directory server user", self.__create_ds_user)
+ self.step("creating directory server instance", self.__create_instance)
+ self.step("adding default schema", self.__add_default_schemas)
+ self.step("enabling memberof plugin", self.__add_memberof_module)
+ self.step("enabling referential integrity plugin", self.__add_referint_module)
+ self.step("enabling distributed numeric assignment plugin", self.__add_dna_module)
+ self.step("enabling winsync plugin", self.__add_winsync_module)
+ self.step("configuring uniqueness plugin", self.__set_unique_attrs)
+ self.step("creating indices", self.__create_indices)
+ self.step("configuring ssl for ds instance", self.__enable_ssl)
+ self.step("configuring certmap.conf", self.__certmap_conf)
+ self.step("restarting directory server", self.__restart_instance)
+ self.step("adding default layout", self.__add_default_layout)
+ self.step("configuring Posix uid/gid generation as first master",
+ self.__config_uidgid_gen_first_master)
+ self.step("adding master entry as first master",
+ self.__add_master_entry_first_master)
+ self.step("initializing group membership",
+ self.init_memberof)
+
+ self.step("configuring directory to start on boot", self.__enable)
+
+ self.start_creation("Configuring directory server:")
+
+ def __enable(self):
+ self.backup_state("enabled", self.is_enabled())
+ self.chkconfig_on()
+
+ def __setup_sub_dict(self):
+ server_root = find_server_root()
+ self.sub_dict = dict(FQHN=self.host_name, SERVERID=self.serverid,
+ PASSWORD=self.dm_password, SUFFIX=self.suffix.lower(),
+ REALM=self.realm_name, USER=self.ds_user,
+ SERVER_ROOT=server_root, DOMAIN=self.domain,
+ TIME=int(time.time()))
+
+ def __create_ds_user(self):
+ user_exists = True
+ try:
+ pwd.getpwnam(self.ds_user)
+ logging.debug("ds user %s exists" % self.ds_user)
+ except KeyError:
+ user_exists = False
+ logging.debug("adding ds user %s" % self.ds_user)
+ args = ["/usr/sbin/useradd", "-c", "DS System User", "-d", "/var/lib/dirsrv", "-M", "-r", "-s", "/sbin/nologin", self.ds_user]
+ try:
+ ipautil.run(args)
+ logging.debug("done adding user")
+ except ipautil.CalledProcessError, e:
+ logging.critical("failed to add user %s" % e)
+
+ self.backup_state("user", self.ds_user)
+ self.backup_state("user_exists", user_exists)
+
+ def __create_instance(self):
+ self.backup_state("running", is_ds_running())
+ self.backup_state("serverid", self.serverid)
+
+ self.sub_dict['BASEDC'] = self.realm_name.split('.')[0].lower()
+ base_txt = ipautil.template_str(BASE_TEMPLATE, self.sub_dict)
+ logging.debug(base_txt)
+ base_fd = file("/var/lib/dirsrv/boot.ldif", "w")
+ base_fd.write(base_txt)
+ base_fd.flush()
+ base_fd.close()
+
+ inf_txt = ipautil.template_str(INF_TEMPLATE, self.sub_dict)
+ logging.debug("writing inf template")
+ inf_fd = ipautil.write_tmp_file(inf_txt)
+ inf_txt = re.sub(r"RootDNPwd=.*\n", "", inf_txt)
+ logging.debug(inf_txt)
+ if ipautil.file_exists("/usr/sbin/setup-ds.pl"):
+ args = ["/usr/sbin/setup-ds.pl", "--silent", "--logfile", "-", "-f", inf_fd.name]
+ logging.debug("calling setup-ds.pl")
+ else:
+ args = ["/usr/bin/ds_newinst.pl", inf_fd.name]
+ logging.debug("calling ds_newinst.pl")
+ try:
+ ipautil.run(args)
+ logging.debug("completed creating ds instance")
+ except ipautil.CalledProcessError, e:
+ logging.critical("failed to restart ds instance %s" % e)
+ logging.debug("restarting ds instance")
+ try:
+ self.restart()
+ logging.debug("done restarting ds instance")
+ except ipautil.CalledProcessError, e:
+ print "failed to restart ds instance", e
+ logging.debug("failed to restart ds instance %s" % e)
+ inf_fd.close()
+ os.remove("/var/lib/dirsrv/boot.ldif")
+
+ def __add_default_schemas(self):
+ shutil.copyfile(ipautil.SHARE_DIR + "60kerberos.ldif",
+ schema_dirname(self.serverid) + "60kerberos.ldif")
+ shutil.copyfile(ipautil.SHARE_DIR + "60samba.ldif",
+ schema_dirname(self.serverid) + "60samba.ldif")
+ shutil.copyfile(ipautil.SHARE_DIR + "60radius.ldif",
+ schema_dirname(self.serverid) + "60radius.ldif")
+ shutil.copyfile(ipautil.SHARE_DIR + "60ipaconfig.ldif",
+ schema_dirname(self.serverid) + "60ipaconfig.ldif")
+
+ def __restart_instance(self):
+ try:
+ self.restart()
+ if not is_ds_running():
+ logging.critical("Failed to restart the directory server. See the installation log for details.")
+ sys.exit(1)
+ except SystemExit, e:
+ raise e
+ except Exception, e:
+ # TODO: roll back here?
+ logging.critical("Failed to restart the directory server. See the installation log for details.")
+
+ def __ldap_mod(self, ldif, sub_dict = None):
+ fd = None
+ path = ipautil.SHARE_DIR + ldif
+
+ if not sub_dict is None:
+ txt = ipautil.template_file(path, sub_dict)
+ fd = ipautil.write_tmp_file(txt)
+ path = fd.name
+
+ [pw_fd, pw_name] = tempfile.mkstemp()
+ os.write(pw_fd, self.dm_password)
+ os.close(pw_fd)
+
+ args = ["/usr/bin/ldapmodify", "-h", "127.0.0.1", "-xv",
+ "-D", "cn=Directory Manager", "-y", pw_name, "-f", path]
+
+ try:
+ try:
+ ipautil.run(args)
+ except ipautil.CalledProcessError, e:
+ logging.critical("Failed to load %s: %s" % (ldif, str(e)))
+ finally:
+ os.remove(pw_name)
+
+ if not fd is None:
+ fd.close()
+
+ def __add_memberof_module(self):
+ self.__ldap_mod("memberof-conf.ldif")
+
+ def init_memberof(self):
+ self.__ldap_mod("memberof-task.ldif", self.sub_dict)
+
+ def apply_updates(self):
+ ld = ldapupdate.LDAPUpdate(dm_password=self.dm_password)
+ files = ld.get_all_files(ldapupdate.UPDATES_DIR)
+ ld.update(files)
+
+ def __add_referint_module(self):
+ self.__ldap_mod("referint-conf.ldif")
+
+ def __add_dna_module(self):
+ self.__ldap_mod("dna-conf.ldif")
+
+ def __set_unique_attrs(self):
+ self.__ldap_mod("unique-attributes.ldif", self.sub_dict)
+
+ def __config_uidgid_gen_first_master(self):
+ self.__ldap_mod("dna-posix.ldif", self.sub_dict)
+
+ def __add_master_entry_first_master(self):
+ self.__ldap_mod("master-entry.ldif", self.sub_dict)
+
+ def __add_winsync_module(self):
+ self.__ldap_mod("ipa-winsync-conf.ldif")
+
+ def __enable_ssl(self):
+ dirname = config_dirname(self.serverid)
+ ca = certs.CertDB(dirname)
+ if self.pkcs12_info:
+ ca.create_from_pkcs12(self.pkcs12_info[0], self.pkcs12_info[1])
+ server_certs = ca.find_server_certs()
+ if len(server_certs) == 0:
+ raise RuntimeError("Could not find a suitable server cert in import in %s" % pkcs12_info[0])
+
+ # We only handle one server cert
+ nickname = server_certs[0][0]
+ else:
+ ca.create_self_signed()
+ ca.create_server_cert("Server-Cert", "cn=%s,ou=Fedora Directory Server" % self.host_name)
+ nickname = "Server-Cert"
+
+ conn = ipaldap.IPAdmin("127.0.0.1")
+ conn.simple_bind_s("cn=directory manager", self.dm_password)
+
+ mod = [(ldap.MOD_REPLACE, "nsSSLClientAuth", "allowed"),
+ (ldap.MOD_REPLACE, "nsSSL3Ciphers",
+ "-rsa_null_md5,+rsa_rc4_128_md5,+rsa_rc4_40_md5,+rsa_rc2_40_md5,\
++rsa_des_sha,+rsa_fips_des_sha,+rsa_3des_sha,+rsa_fips_3des_sha,+fortezza,\
++fortezza_rc4_128_sha,+fortezza_null,+tls_rsa_export1024_with_rc4_56_sha,\
++tls_rsa_export1024_with_des_cbc_sha")]
+ conn.modify_s("cn=encryption,cn=config", mod)
+
+ mod = [(ldap.MOD_ADD, "nsslapd-security", "on"),
+ (ldap.MOD_REPLACE, "nsslapd-ssl-check-hostname", "off")]
+ conn.modify_s("cn=config", mod)
+
+ entry = ipaldap.Entry("cn=RSA,cn=encryption,cn=config")
+
+ entry.setValues("objectclass", "top", "nsEncryptionModule")
+ entry.setValues("cn", "RSA")
+ entry.setValues("nsSSLPersonalitySSL", nickname)
+ entry.setValues("nsSSLToken", "internal (software)")
+ entry.setValues("nsSSLActivation", "on")
+
+ conn.addEntry(entry)
+
+ conn.unbind()
+
+ def __add_default_layout(self):
+ self.__ldap_mod("bootstrap-template.ldif", self.sub_dict)
+
+ def __create_indices(self):
+ self.__ldap_mod("indices.ldif")
+
+ def __certmap_conf(self):
+ shutil.copyfile(ipautil.SHARE_DIR + "certmap.conf.template",
+ config_dirname(self.serverid) + "certmap.conf")
+
+ def change_admin_password(self, password):
+ logging.debug("Changing admin password")
+ dirname = config_dirname(self.serverid)
+ if ipautil.dir_exists("/usr/lib64/mozldap"):
+ app = "/usr/lib64/mozldap/ldappasswd"
+ else:
+ app = "/usr/lib/mozldap/ldappasswd"
+ args = [app,
+ "-D", "cn=Directory Manager", "-w", self.dm_password,
+ "-P", dirname+"/cert8.db", "-ZZZ", "-s", password,
+ "uid=admin,cn=users,cn=accounts,"+self.suffix]
+ try:
+ ipautil.run(args)
+ logging.debug("ldappasswd done")
+ except ipautil.CalledProcessError, e:
+ print "Unable to set admin password", e
+ logging.debug("Unable to set admin password %s" % e)
+
+ def uninstall(self):
+ running = self.restore_state("running")
+ enabled = self.restore_state("enabled")
+
+ if not running is None:
+ self.stop()
+
+ if not enabled is None and not enabled:
+ self.chkconfig_off()
+
+ serverid = self.restore_state("serverid")
+ if not serverid is None:
+ erase_ds_instance_data(serverid)
+
+ ds_user = self.restore_state("user")
+ user_exists = self.restore_state("user_exists")
+
+ if not ds_user is None and not user_exists is None and not user_exists:
+ try:
+ ipautil.run(["/usr/sbin/userdel", ds_user])
+ except ipautil.CalledProcessError, e:
+ logging.critical("failed to delete user %s" % e)
+
+ if self.restore_state("running"):
+ self.start()
+
+ # we could probably move this function into the service.Service
+ # class - it's very generic - all we need is a way to get an
+ # instance of a particular Service
+ def add_ca_cert(self, cacert_fname, cacert_name=''):
+ """Add a CA certificate to the directory server cert db. We
+ first have to shut down the directory server in case it has
+ opened the cert db read-only. Then we use the CertDB class
+ to add the CA cert. We have to provide a nickname, and we
+ do not use 'CA certificate' since that's the default, so
+ we use 'Imported CA' if none specified. Then we restart
+ the server."""
+ # first make sure we have a valid cacert_fname
+ try:
+ if not os.access(cacert_fname, os.R_OK):
+ logging.critical("The given CA cert file named [%s] could not be read" %
+ cacert_fname)
+ return False
+ except OSError, e:
+ logging.critical("The given CA cert file named [%s] could not be read: %s" %
+ (cacert_fname, str(e)))
+ return False
+ # ok - ca cert file can be read
+ # shutdown the server
+ self.stop()
+
+ dirname = config_dirname(realm_to_serverid(self.realm_name))
+ certdb = certs.CertDB(dirname)
+ if not cacert_name or len(cacert_name) == 0:
+ cacert_name = "Imported CA"
+ # we can't pass in the nickname, so we set the instance variable
+ certdb.cacert_name = cacert_name
+ status = True
+ try:
+ certdb.load_cacert(cacert_fname)
+ except ipalib.CalledProcessError, e:
+ logging.critical("Error importing CA cert file named [%s]: %s" %
+ (cacert_fname, str(e)))
+ status = False
+ # restart the directory server
+ self.start()
+
+ return status
diff --git a/ipaserver/install/httpinstance.py b/ipaserver/install/httpinstance.py
new file mode 100644
index 000000000..f5a903b30
--- /dev/null
+++ b/ipaserver/install/httpinstance.py
@@ -0,0 +1,231 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os
+import os.path
+import subprocess
+import string
+import tempfile
+import logging
+import pwd
+import fileinput
+import sys
+import shutil
+
+import service
+import certs
+import dsinstance
+import installutils
+from ipa import sysrestore
+from ipa import ipautil
+
+HTTPD_DIR = "/etc/httpd"
+SSL_CONF = HTTPD_DIR + "/conf.d/ssl.conf"
+NSS_CONF = HTTPD_DIR + "/conf.d/nss.conf"
+NSS_DIR = HTTPD_DIR + "/alias"
+
+selinux_warning = """WARNING: could not set selinux boolean httpd_can_network_connect to true.
+The web interface may not function correctly until this boolean is
+successfully change with the command:
+ /usr/sbin/setsebool -P httpd_can_network_connect true
+Try updating the policycoreutils and selinux-policy packages.
+"""
+
+class WebGuiInstance(service.SimpleServiceInstance):
+ def __init__(self):
+ service.SimpleServiceInstance.__init__(self, "ipa_webgui")
+
+class HTTPInstance(service.Service):
+ def __init__(self, fstore = None):
+ service.Service.__init__(self, "httpd")
+ if fstore:
+ self.fstore = fstore
+ else:
+ self.fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore')
+
+ def create_instance(self, realm, fqdn, domain_name, autoconfig=True, pkcs12_info=None):
+ self.fqdn = fqdn
+ self.realm = realm
+ self.domain = domain_name
+ self.pkcs12_info = pkcs12_info
+ self.sub_dict = { "REALM" : realm, "FQDN": fqdn, "DOMAIN" : self.domain }
+
+ self.step("disabling mod_ssl in httpd", self.__disable_mod_ssl)
+ self.step("Setting mod_nss port to 443", self.__set_mod_nss_port)
+ self.step("Adding URL rewriting rules", self.__add_include)
+ self.step("configuring httpd", self.__configure_http)
+ self.step("creating a keytab for httpd", self.__create_http_keytab)
+ self.step("Setting up ssl", self.__setup_ssl)
+ if autoconfig:
+ self.step("Setting up browser autoconfig", self.__setup_autoconfig)
+ self.step("configuring SELinux for httpd", self.__selinux_config)
+ self.step("restarting httpd", self.__start)
+ self.step("configuring httpd to start on boot", self.__enable)
+
+ self.start_creation("Configuring the web interface")
+
+ def __start(self):
+ self.backup_state("running", self.is_running())
+ self.restart()
+
+ def __enable(self):
+ self.backup_state("enabled", self.is_running())
+ self.chkconfig_on()
+
+ def __selinux_config(self):
+ selinux=0
+ try:
+ if (os.path.exists('/usr/sbin/selinuxenabled')):
+ ipautil.run(["/usr/sbin/selinuxenabled"])
+ selinux=1
+ except ipautil.CalledProcessError:
+ # selinuxenabled returns 1 if not enabled
+ pass
+
+ if selinux:
+ try:
+ # returns e.g. "httpd_can_network_connect --> off"
+ (stdout, stderr) = ipautils.run(["/usr/sbin/getsebool",
+ "httpd_can_network_connect"])
+ self.backup_state("httpd_can_network_connect", stdout.split()[2])
+ except:
+ pass
+
+ # Allow apache to connect to the turbogears web gui
+ # This can still fail even if selinux is enabled
+ try:
+ ipautil.run(["/usr/sbin/setsebool", "-P", "httpd_can_network_connect", "true"])
+ except:
+ self.print_msg(selinux_warning)
+
+ def __create_http_keytab(self):
+ http_principal = "HTTP/" + self.fqdn + "@" + self.realm
+ installutils.kadmin_addprinc(http_principal)
+ installutils.create_keytab("/etc/httpd/conf/ipa.keytab", http_principal)
+
+ pent = pwd.getpwnam("apache")
+ os.chown("/etc/httpd/conf/ipa.keytab", pent.pw_uid, pent.pw_gid)
+
+ def __configure_http(self):
+ http_txt = ipautil.template_file(ipautil.SHARE_DIR + "ipa.conf", self.sub_dict)
+ self.fstore.backup_file("/etc/httpd/conf.d/ipa.conf")
+ http_fd = open("/etc/httpd/conf.d/ipa.conf", "w")
+ http_fd.write(http_txt)
+ http_fd.close()
+
+ http_txt = ipautil.template_file(ipautil.SHARE_DIR + "ipa-rewrite.conf", self.sub_dict)
+ self.fstore.backup_file("/etc/httpd/conf.d/ipa-rewrite.conf")
+ http_fd = open("/etc/httpd/conf.d/ipa-rewrite.conf", "w")
+ http_fd.write(http_txt)
+ http_fd.close()
+
+ def __disable_mod_ssl(self):
+ if os.path.exists(SSL_CONF):
+ self.fstore.backup_file(SSL_CONF)
+ os.unlink(SSL_CONF)
+
+ def __set_mod_nss_port(self):
+ self.fstore.backup_file(NSS_CONF)
+ if installutils.update_file(NSS_CONF, '8443', '443') != 0:
+ print "Updating port in %s failed." % NSS_CONF
+
+ def __set_mod_nss_nickname(self, nickname):
+ installutils.set_directive(NSS_CONF, 'NSSNickname', nickname)
+
+ def __add_include(self):
+ """This should run after __set_mod_nss_port so is already backed up"""
+ if installutils.update_file(NSS_CONF, '</VirtualHost>', 'Include conf.d/ipa-rewrite.conf\n</VirtualHost>') != 0:
+ print "Adding Include conf.d/ipa-rewrite to %s failed." % NSS_CONF
+
+ def __setup_ssl(self):
+ ds_ca = certs.CertDB(dsinstance.config_dirname(dsinstance.realm_to_serverid(self.realm)))
+ ca = certs.CertDB(NSS_DIR)
+ if self.pkcs12_info:
+ ca.create_from_pkcs12(self.pkcs12_info[0], self.pkcs12_info[1], passwd="")
+ server_certs = ca.find_server_certs()
+ if len(server_certs) == 0:
+ raise RuntimeError("Could not find a suitable server cert in import in %s" % pkcs12_info[0])
+
+ # We only handle one server cert
+ nickname = server_certs[0][0]
+
+ self.__set_mod_nss_nickname(nickname)
+ else:
+ ca.create_from_cacert(ds_ca.cacert_fname)
+ ca.create_server_cert("Server-Cert", "cn=%s,ou=Apache Web Server" % self.fqdn, ds_ca)
+ ca.create_signing_cert("Signing-Cert", "cn=%s,ou=Signing Certificate,o=Identity Policy Audit" % self.fqdn, ds_ca)
+
+ # Fix the database permissions
+ os.chmod(NSS_DIR + "/cert8.db", 0640)
+ os.chmod(NSS_DIR + "/key3.db", 0640)
+ os.chmod(NSS_DIR + "/secmod.db", 0640)
+
+ pent = pwd.getpwnam("apache")
+ os.chown(NSS_DIR + "/cert8.db", 0, pent.pw_gid )
+ os.chown(NSS_DIR + "/key3.db", 0, pent.pw_gid )
+ os.chown(NSS_DIR + "/secmod.db", 0, pent.pw_gid )
+
+ def __setup_autoconfig(self):
+ prefs_txt = ipautil.template_file(ipautil.SHARE_DIR + "preferences.html.template", self.sub_dict)
+ prefs_fd = open("/usr/share/ipa/html/preferences.html", "w")
+ prefs_fd.write(prefs_txt)
+ prefs_fd.close()
+
+ # The signing cert is generated in __setup_ssl
+ ds_ca = certs.CertDB(dsinstance.config_dirname(dsinstance.realm_to_serverid(self.realm)))
+ ca = certs.CertDB(NSS_DIR)
+
+ # Publish the CA certificate
+ shutil.copy(ds_ca.cacert_fname, "/usr/share/ipa/html/ca.crt")
+ os.chmod("/usr/share/ipa/html/ca.crt", 0444)
+
+ tmpdir = tempfile.mkdtemp(prefix = "tmp-")
+ shutil.copy("/usr/share/ipa/html/preferences.html", tmpdir)
+ ca.run_signtool(["-k", "Signing-Cert",
+ "-Z", "/usr/share/ipa/html/configure.jar",
+ "-e", ".html",
+ tmpdir])
+ shutil.rmtree(tmpdir)
+
+ def uninstall(self):
+ running = self.restore_state("running")
+ enabled = self.restore_state("enabled")
+
+ if not running is None:
+ self.stop()
+
+ if not enabled is None and not enabled:
+ self.chkconfig_off()
+
+ for f in ["/etc/httpd/conf.d/ipa.conf", SSL_CONF, NSS_CONF]:
+ try:
+ self.fstore.restore_file(f)
+ except ValueError, error:
+ logging.debug(error)
+ pass
+
+ sebool_state = self.restore_state("httpd_can_network_connect")
+ if not sebool_state is None:
+ try:
+ ipautil.run(["/usr/sbin/setsebool", "-P", "httpd_can_network_connect", sebool_state])
+ except:
+ self.print_msg(selinux_warning)
+
+ if not running is None and running:
+ self.start()
diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
new file mode 100644
index 000000000..563b168e8
--- /dev/null
+++ b/ipaserver/install/installutils.py
@@ -0,0 +1,248 @@
+# Authors: Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import logging
+import socket
+import errno
+import getpass
+import os
+import re
+import fileinput
+import sys
+import time
+import struct
+import fcntl
+
+from ipa import ipautil
+from ipa import dnsclient
+
+def get_fqdn():
+ fqdn = ""
+ try:
+ fqdn = socket.getfqdn()
+ except:
+ try:
+ fqdn = socket.gethostname()
+ except:
+ fqdn = ""
+ return fqdn
+
+def verify_fqdn(host_name,no_host_dns=False):
+
+ if len(host_name.split(".")) < 2 or host_name == "localhost.localdomain":
+ raise RuntimeError("Invalid hostname: " + host_name)
+
+ try:
+ hostaddr = socket.getaddrinfo(host_name, None)
+ except:
+ raise RuntimeError("Unable to resolve host name, check /etc/hosts or DNS name resolution")
+
+ if len(hostaddr) == 0:
+ raise RuntimeError("Unable to resolve host name, check /etc/hosts or DNS name resolution")
+
+ for a in hostaddr:
+ if a[4][0] == '127.0.0.1' or a[4][0] == '::1':
+ raise RuntimeError("The IPA Server hostname cannot resolve to localhost (%s). A routable IP address must be used. Check /etc/hosts to see if %s is an alias for %s" % (a[4][0], host_name, a[4][0]))
+ try:
+ revname = socket.gethostbyaddr(a[4][0])[0]
+ except:
+ raise RuntimeError("Unable to resolve the reverse ip address, check /etc/hosts or DNS name resolution")
+ if revname != host_name:
+ raise RuntimeError("The host name %s does not match the reverse lookup %s" % (host_name, revname))
+
+ if no_host_dns:
+ print "Warning: skipping DNS resolution of host", host_name
+ return
+
+ # Verify this is NOT a CNAME
+ rs = dnsclient.query(host_name+".", dnsclient.DNS_C_IN, dnsclient.DNS_T_CNAME)
+ if len(rs) != 0:
+ for rsn in rs:
+ if rsn.dns_type == dnsclient.DNS_T_CNAME:
+ raise RuntimeError("The IPA Server Hostname cannot be a CNAME, only A names are allowed.")
+
+ # Verify that it is a DNS A record
+ rs = dnsclient.query(host_name+".", dnsclient.DNS_C_IN, dnsclient.DNS_T_A)
+ if len(rs) == 0:
+ print "Warning: Hostname (%s) not found in DNS" % host_name
+ return
+
+ rec = None
+ for rsn in rs:
+ if rsn.dns_type == dnsclient.DNS_T_A:
+ rec = rsn
+ break
+
+ if rec == None:
+ print "Warning: Hostname (%s) not found in DNS" % host_name
+ return
+
+ # Compare the forward and reverse
+ forward = rec.dns_name
+
+ addr = socket.inet_ntoa(struct.pack('<L',rec.rdata.address))
+ ipaddr = socket.inet_ntoa(struct.pack('!L',rec.rdata.address))
+
+ addr = addr + ".in-addr.arpa."
+ rs = dnsclient.query(addr, dnsclient.DNS_C_IN, dnsclient.DNS_T_PTR)
+ if len(rs) == 0:
+ raise RuntimeError("Cannot find Reverse Address for %s (%s)" % (host_name, addr))
+
+ rev = None
+ for rsn in rs:
+ if rsn.dns_type == dnsclient.DNS_T_PTR:
+ rev = rsn
+ break
+
+ if rev == None:
+ raise RuntimeError("Cannot find Reverse Address for %s (%s)" % (host_name, addr))
+
+ reverse = rev.rdata.ptrdname
+
+ if forward != reverse:
+ raise RuntimeError("The DNS forward record %s does not match the reverse address %s" % (forward, reverse))
+
+def port_available(port):
+ """Try to bind to a port on the wildcard host
+ Return 1 if the port is available
+ Return 0 if the port is in use
+ """
+ rv = 1
+
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ fcntl.fcntl(s, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
+ s.bind(('', port))
+ s.close()
+ except socket.error, e:
+ if e[0] == errno.EADDRINUSE:
+ rv = 0
+
+ if rv:
+ try:
+ s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
+ fcntl.fcntl(s, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
+ s.bind(('', port))
+ s.close()
+ except socket.error, e:
+ if e[0] == errno.EADDRINUSE:
+ rv = 0
+
+ return rv
+
+def standard_logging_setup(log_filename, debug=False):
+ old_umask = os.umask(077)
+ # Always log everything (i.e., DEBUG) to the log
+ # file.
+ logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(levelname)s %(message)s',
+ filename=log_filename,
+ filemode='w')
+ os.umask(old_umask)
+
+ console = logging.StreamHandler()
+ # If the debug option is set, also log debug messages to the console
+ if debug:
+ console.setLevel(logging.DEBUG)
+ else:
+ # Otherwise, log critical and error messages
+ console.setLevel(logging.ERROR)
+ formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
+ console.setFormatter(formatter)
+ logging.getLogger('').addHandler(console)
+
+def get_password(prompt):
+ if os.isatty(sys.stdin.fileno()):
+ return getpass.getpass(prompt)
+ else:
+ return sys.stdin.readline().rstrip()
+
+def read_password(user, confirm=True, validate=True):
+ correct = False
+ pwd = ""
+ while not correct:
+ pwd = get_password(user + " password: ")
+ if not pwd:
+ continue
+ if validate and len(pwd) < 8:
+ print "Password must be at least 8 characters long"
+ continue
+ if not confirm:
+ correct = True
+ continue
+ pwd_confirm = get_password("Password (confirm): ")
+ if pwd != pwd_confirm:
+ print "Password mismatch!"
+ print ""
+ else:
+ correct = True
+ print ""
+ return pwd
+
+def update_file(filename, orig, subst):
+ if os.path.exists(filename):
+ pattern = "%s" % re.escape(orig)
+ p = re.compile(pattern)
+ for line in fileinput.input(filename, inplace=1):
+ if not p.search(line):
+ sys.stdout.write(line)
+ else:
+ sys.stdout.write(p.sub(subst, line))
+ fileinput.close()
+ return 0
+ else:
+ print "File %s doesn't exist." % filename
+ return 1
+
+def set_directive(filename, directive, value):
+ """Set a name/value pair directive in a configuration file.
+
+ This has only been tested with nss.conf
+ """
+ fd = open(filename)
+ file = []
+ for line in fd:
+ if directive in line:
+ file.append('%s "%s"\n' % (directive, value))
+ else:
+ file.append(line)
+ fd.close()
+
+ fd = open(filename, "w")
+ fd.write("".join(file))
+ fd.close()
+
+def kadmin(command):
+ ipautil.run(["/usr/kerberos/sbin/kadmin.local", "-q", command])
+
+def kadmin_addprinc(principal):
+ kadmin("addprinc -randkey " + principal)
+
+def kadmin_modprinc(principal, options):
+ kadmin("modprinc " + options + " " + principal)
+
+def create_keytab(path, principal):
+ try:
+ if ipautil.file_exists(path):
+ os.remove(path)
+ except os.error:
+ logging.critical("Failed to remove %s." % path)
+
+ kadmin("ktadd -k " + path + " " + principal)
+
diff --git a/ipaserver/install/ipaldap.py b/ipaserver/install/ipaldap.py
new file mode 100644
index 000000000..c2dbe4e2d
--- /dev/null
+++ b/ipaserver/install/ipaldap.py
@@ -0,0 +1,701 @@
+# Authors: Rich Megginson <richm@redhat.com>
+# Rob Crittenden <rcritten@redhat.com
+#
+# Copyright (C) 2007 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+import os
+import os.path
+import popen2
+import base64
+import urllib
+import urllib2
+import socket
+import ldif
+import re
+import string
+import ldap
+import cStringIO
+import time
+import operator
+import struct
+import ldap.sasl
+from ldap.controls import LDAPControl,DecodeControlTuples,EncodeControlTuples
+from ldap.ldapobject import SimpleLDAPObject
+from ipa import ipaerror, ipautil
+
+# Global variable to define SASL auth
+sasl_auth = ldap.sasl.sasl({},'GSSAPI')
+
+class Entry:
+ """This class represents an LDAP Entry object. An LDAP entry consists of a DN
+ and a list of attributes. Each attribute consists of a name and a list of
+ values. In python-ldap, entries are returned as a list of 2-tuples.
+ Instance variables:
+ dn - string - the string DN of the entry
+ data - CIDict - case insensitive dict of the attributes and values"""
+
+ def __init__(self,entrydata):
+ """data is the raw data returned from the python-ldap result method, which is
+ a search result entry or a reference or None.
+ If creating a new empty entry, data is the string DN."""
+ if entrydata:
+ if isinstance(entrydata,tuple):
+ self.dn = entrydata[0]
+ self.data = ipautil.CIDict(entrydata[1])
+ elif isinstance(entrydata,str) or isinstance(entrydata,unicode):
+ self.dn = entrydata
+ self.data = ipautil.CIDict()
+ else:
+ self.dn = ''
+ self.data = ipautil.CIDict()
+
+ def __nonzero__(self):
+ """This allows us to do tests like if entry: returns false if there is no data,
+ true otherwise"""
+ return self.data != None and len(self.data) > 0
+
+ def hasAttr(self,name):
+ """Return True if this entry has an attribute named name, False otherwise"""
+ return self.data and self.data.has_key(name)
+
+ def __getattr__(self,name):
+ """If name is the name of an LDAP attribute, return the first value for that
+ attribute - equivalent to getValue - this allows the use of
+ entry.cn
+ instead of
+ entry.getValue('cn')
+ This also allows us to return None if an attribute is not found rather than
+ throwing an exception"""
+ return self.getValue(name)
+
+ def getValues(self,name):
+ """Get the list (array) of values for the attribute named name"""
+ return self.data.get(name)
+
+ def getValue(self,name):
+ """Get the first value for the attribute named name"""
+ return self.data.get(name,[None])[0]
+
+ def setValue(self,name,*value):
+ """Value passed in may be a single value, several values, or a single sequence.
+ For example:
+ ent.setValue('name', 'value')
+ ent.setValue('name', 'value1', 'value2', ..., 'valueN')
+ ent.setValue('name', ['value1', 'value2', ..., 'valueN'])
+ ent.setValue('name', ('value1', 'value2', ..., 'valueN'))
+ Since *value is a tuple, we may have to extract a list or tuple from that
+ tuple as in the last two examples above"""
+ if isinstance(value[0],list) or isinstance(value[0],tuple):
+ self.data[name] = value[0]
+ else:
+ self.data[name] = value
+
+ setValues = setValue
+
+ def toTupleList(self):
+ """Convert the attrs and values to a list of 2-tuples. The first element
+ of the tuple is the attribute name. The second element is either a
+ single value or a list of values."""
+ return self.data.items()
+
+ def __str__(self):
+ """Convert the Entry to its LDIF representation"""
+ return self.__repr__()
+
+ # the ldif class base64 encodes some attrs which I would rather see in raw form - to
+ # encode specific attrs as base64, add them to the list below
+ ldif.safe_string_re = re.compile('^$')
+ base64_attrs = ['nsstate', 'krbprincipalkey', 'krbExtraData']
+
+ def __repr__(self):
+ """Convert the Entry to its LDIF representation"""
+ sio = cStringIO.StringIO()
+ # what's all this then? the unparse method will currently only accept
+ # a list or a dict, not a class derived from them. self.data is a
+ # cidict, so unparse barfs on it. I've filed a bug against python-ldap,
+ # but in the meantime, we have to convert to a plain old dict for printing
+ # I also don't want to see wrapping, so set the line width really high (1000)
+ newdata = {}
+ newdata.update(self.data)
+ ldif.LDIFWriter(sio,Entry.base64_attrs,1000).unparse(self.dn,newdata)
+ return sio.getvalue()
+
+def wrapper(f,name):
+ """This is the method that wraps all of the methods of the superclass. This seems
+ to need to be an unbound method, that's why it's outside of IPAdmin. Perhaps there
+ is some way to do this with the new classmethod or staticmethod of 2.4.
+ Basically, we replace every call to a method in SimpleLDAPObject (the superclass
+ of IPAdmin) with a call to inner. The f argument to wrapper is the bound method
+ of IPAdmin (which is inherited from the superclass). Bound means that it will implicitly
+ be called with the self argument, it is not in the args list. name is the name of
+ the method to call. If name is a method that returns entry objects (e.g. result),
+ we wrap the data returned by an Entry class. If name is a method that takes an entry
+ argument, we extract the raw data from the entry object to pass in."""
+ def inner(*args, **kargs):
+ if name == 'result':
+ type, data = f(*args, **kargs)
+ # data is either a 2-tuple or a list of 2-tuples
+ # print data
+ if data:
+ if isinstance(data,tuple):
+ return type, Entry(data)
+ elif isinstance(data,list):
+ return type, [Entry(x) for x in data]
+ else:
+ raise TypeError, "unknown data type %s returned by result" % type(data)
+ else:
+ return type, data
+ elif name.startswith('add'):
+ # the first arg is self
+ # the second and third arg are the dn and the data to send
+ # We need to convert the Entry into the format used by
+ # python-ldap
+ ent = args[0]
+ if isinstance(ent,Entry):
+ return f(ent.dn, ent.toTupleList(), *args[2:])
+ else:
+ return f(*args, **kargs)
+ else:
+ return f(*args, **kargs)
+ return inner
+
+class LDIFConn(ldif.LDIFParser):
+ def __init__(
+ self,
+ input_file,
+ ignored_attr_types=None,max_entries=0,process_url_schemes=None
+ ):
+ """
+ See LDIFParser.__init__()
+
+ Additional Parameters:
+ all_records
+ List instance for storing parsed records
+ """
+ self.dndict = {} # maps dn to Entry
+ self.dnlist = [] # contains entries in order read
+ myfile = input_file
+ if isinstance(input_file,str) or isinstance(input_file,unicode):
+ myfile = open(input_file, "r")
+ ldif.LDIFParser.__init__(self,myfile,ignored_attr_types,max_entries,process_url_schemes)
+ self.parse()
+ if isinstance(input_file,str) or isinstance(input_file,unicode):
+ myfile.close()
+
+ def handle(self,dn,entry):
+ """
+ Append single record to dictionary of all records.
+ """
+ if not dn:
+ dn = ''
+ newentry = Entry((dn, entry))
+ self.dndict[IPAdmin.normalizeDN(dn)] = newentry
+ self.dnlist.append(newentry)
+
+ def get(self,dn):
+ ndn = IPAdmin.normalizeDN(dn)
+ return self.dndict.get(ndn, Entry(None))
+
+class IPAdmin(SimpleLDAPObject):
+ CFGSUFFIX = "o=NetscapeRoot"
+ DEFAULT_USER_ID = "nobody"
+
+ def getDseAttr(self,attrname):
+ conffile = self.confdir + '/dse.ldif'
+ dseldif = LDIFConn(conffile)
+ cnconfig = dseldif.get("cn=config")
+ if cnconfig:
+ return cnconfig.getValue(attrname)
+ return None
+
+ def __initPart2(self):
+ if self.binddn and len(self.binddn) and not hasattr(self,'sroot'):
+ try:
+ ent = self.getEntry('cn=config', ldap.SCOPE_BASE, '(objectclass=*)',
+ [ 'nsslapd-instancedir', 'nsslapd-errorlog',
+ 'nsslapd-certdir', 'nsslapd-schemadir' ])
+ self.errlog = ent.getValue('nsslapd-errorlog')
+ self.confdir = ent.getValue('nsslapd-certdir')
+ if not self.confdir:
+ self.confdir = ent.getValue('nsslapd-schemadir')
+ if self.confdir:
+ self.confdir = os.path.dirname(self.confdir)
+ instdir = ent.getValue('nsslapd-instancedir')
+ ent = self.getEntry('cn=config,cn=ldbm database,cn=plugins,cn=config',
+ ldap.SCOPE_BASE, '(objectclass=*)',
+ [ 'nsslapd-directory' ])
+ self.dbdir = os.path.dirname(ent.getValue('nsslapd-directory'))
+ except (ldap.INSUFFICIENT_ACCESS, ldap.CONNECT_ERROR):
+ pass # usually means
+ except ldap.OPERATIONS_ERROR, e:
+ pass # usually means this is Active Directory
+ except ldap.LDAPError, e:
+ print "caught exception ", e
+ raise
+
+ def __localinit__(self):
+ """If a CA certificate is provided then it is assumed that we are
+ doing SSL client authentication with proxy auth.
+
+ If a CA certificate is not present then it is assumed that we are
+ using a forwarded kerberos ticket for SASL auth. SASL provides
+ its own encryption.
+ """
+ if self.cacert is not None:
+ SimpleLDAPObject.__init__(self,'ldaps://%s:%d' % (self.host,self.port))
+ else:
+ SimpleLDAPObject.__init__(self,'ldap://%s:%d' % (self.host,self.port))
+
+ def __init__(self,host,port=389,cacert=None,bindcert=None,bindkey=None,proxydn=None,debug=None):
+ """We just set our instance variables and wrap the methods - the real
+ work is done in __localinit__ and __initPart2 - these are separated
+ out this way so that we can call them from places other than
+ instance creation e.g. when we just need to reconnect, not create a
+ new instance"""
+ if debug and debug.lower() == "on":
+ ldap.set_option(ldap.OPT_DEBUG_LEVEL,255)
+ if cacert is not None:
+ ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,cacert)
+ if bindcert is not None:
+ ldap.set_option(ldap.OPT_X_TLS_CERTFILE,bindcert)
+ if bindkey is not None:
+ ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey)
+
+ self.__wrapmethods()
+ self.port = port
+ self.host = host
+ self.cacert = cacert
+ self.bindcert = bindcert
+ self.bindkey = bindkey
+ self.proxydn = proxydn
+ self.suffixes = {}
+ self.__localinit__()
+
+ def __str__(self):
+ return self.host + ":" + str(self.port)
+
+ def __get_server_controls__(self):
+ """Create the proxy user server control. The control has the form
+ 0x04 = Octet String
+ 4|0x80 sets the length of the string length field at 4 bytes
+ the struct() gets us the length in bytes of string self.proxydn
+ self.proxydn is the proxy dn to send"""
+
+ import sys
+
+ if self.proxydn is not None:
+ proxydn = chr(0x04) + chr(4|0x80) + struct.pack('l', socket.htonl(len(self.proxydn))) + self.proxydn;
+
+ # Create the proxy control
+ sctrl=[]
+ sctrl.append(LDAPControl('2.16.840.1.113730.3.4.18',True,proxydn))
+ else:
+ sctrl=None
+
+ return sctrl
+
+ def toLDAPURL(self):
+ return "ldap://%s:%d/" % (self.host,self.port)
+
+ def set_proxydn(self, proxydn):
+ self.proxydn = proxydn
+
+ def set_krbccache(self, krbccache, principal):
+ if krbccache is not None:
+ os.environ["KRB5CCNAME"] = krbccache
+ self.sasl_interactive_bind_s("", sasl_auth)
+ self.principal = principal
+ self.proxydn = None
+
+ def do_simple_bind(self, binddn="cn=directory manager", bindpw=""):
+ self.binddn = binddn
+ self.bindpwd = bindpw
+ self.simple_bind_s(binddn, bindpw)
+ self.__initPart2()
+
+ def getEntry(self,*args):
+ """This wraps the search function. It is common to just get one entry"""
+
+ sctrl = self.__get_server_controls__()
+
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+
+ try:
+ res = self.search(*args)
+ type, obj = self.result(res)
+ except ldap.NO_SUCH_OBJECT:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND,
+ notfound(args))
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+
+ if not obj:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND,
+ notfound(args))
+ elif isinstance(obj,Entry):
+ return obj
+ else: # assume list/tuple
+ return obj[0]
+
+ def getList(self,*args):
+ """This wraps the search function to find all users."""
+
+ sctrl = self.__get_server_controls__()
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+
+ try:
+ res = self.search(*args)
+ type, obj = self.result(res)
+ except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED), e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR,
+ "Too many results returned by search", e)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+
+ if not obj:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND,
+ notfound(args))
+
+ all_users = []
+ for s in obj:
+ all_users.append(s)
+
+ return all_users
+
+ def getListAsync(self,*args):
+ """This version performs an asynchronous search, to allow
+ results even if we hit a limit.
+
+ It returns a list: counter followed by the results.
+ If the results are truncated, counter will be set to -1.
+ """
+
+ sctrl = self.__get_server_controls__()
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+
+ entries = []
+ partial = 0
+
+ try:
+ msgid = self.search_ext(*args)
+ type, result_list = self.result(msgid, 0)
+ while result_list:
+ for result in result_list:
+ entries.append(result)
+ type, result_list = self.result(msgid, 0)
+ except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED,
+ ldap.TIMELIMIT_EXCEEDED), e:
+ partial = 1
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+
+ if not entries:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND,
+ notfound(args))
+
+ if partial == 1:
+ counter = -1
+ else:
+ counter = len(entries)
+
+ return [counter] + entries
+
+ def addEntry(self,*args):
+ """This wraps the add function. It assumes that the entry is already
+ populated with all of the desired objectclasses and attributes"""
+
+ sctrl = self.__get_server_controls__()
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.add_s(*args)
+ except ldap.ALREADY_EXISTS:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+ return "Success"
+
+ def updateRDN(self, dn, newrdn):
+ """Wrap the modrdn function."""
+
+ sctrl = self.__get_server_controls__()
+
+ if dn == newrdn:
+ # no need to report an error
+ return "Success"
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.modrdn_s(dn, newrdn, delold=1)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+ return "Success"
+
+ def updateEntry(self,dn,olduser,newuser):
+ """This wraps the mod function. It assumes that the entry is already
+ populated with all of the desired objectclasses and attributes"""
+
+ sctrl = self.__get_server_controls__()
+
+ modlist = self.generateModList(olduser, newuser)
+
+ if len(modlist) == 0:
+ raise ipaerror.gen_exception(ipaerror.LDAP_EMPTY_MODLIST)
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.modify_s(dn, modlist)
+ # this is raised when a 'delete' attribute isn't found.
+ # it indicates the previous attribute was removed by another
+ # update, making the olduser stale.
+ except ldap.NO_SUCH_ATTRIBUTE:
+ raise ipaerror.gen_exception(ipaerror.LDAP_MIDAIR_COLLISION)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+ return "Success"
+
+ def generateModList(self, old_entry, new_entry):
+ """A mod list generator that computes more precise modification lists
+ than the python-ldap version. This version purposely generates no
+ REPLACE operations, to deal with multi-user updates more properly."""
+ modlist = []
+
+ old_entry = ipautil.CIDict(old_entry)
+ new_entry = ipautil.CIDict(new_entry)
+
+ keys = set(map(string.lower, old_entry.keys()))
+ keys.update(map(string.lower, new_entry.keys()))
+
+ for key in keys:
+ new_values = new_entry.get(key, [])
+ if not(isinstance(new_values,list) or isinstance(new_values,tuple)):
+ new_values = [new_values]
+ new_values = filter(lambda value:value!=None, new_values)
+ new_values = set(new_values)
+
+ old_values = old_entry.get(key, [])
+ if not(isinstance(old_values,list) or isinstance(old_values,tuple)):
+ old_values = [old_values]
+ old_values = filter(lambda value:value!=None, old_values)
+ old_values = set(old_values)
+
+ adds = list(new_values.difference(old_values))
+ removes = list(old_values.difference(new_values))
+
+ if len(removes) > 0:
+ modlist.append((ldap.MOD_DELETE, key, removes))
+ if len(adds) > 0:
+ modlist.append((ldap.MOD_ADD, key, adds))
+
+ return modlist
+
+ def inactivateEntry(self,dn,has_key):
+ """Rather than deleting entries we mark them as inactive.
+ has_key defines whether the entry already has nsAccountlock
+ set so we can determine which type of mod operation to run."""
+
+ sctrl = self.__get_server_controls__()
+ modlist=[]
+
+ if has_key == True:
+ operation = ldap.MOD_REPLACE
+ else:
+ operation = ldap.MOD_ADD
+
+ modlist.append((operation, "nsAccountlock", "true"))
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.modify_s(dn, modlist)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+ return "Success"
+
+ def deleteEntry(self,*args):
+ """This wraps the delete function. Use with caution."""
+
+ sctrl = self.__get_server_controls__()
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.delete_s(*args)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+ return "Success"
+
+ def modifyPassword(self,dn,oldpass,newpass):
+ """Set the user password using RFC 3062, LDAP Password Modify Extended
+ Operation. This ends up calling the IPA password slapi plugin
+ handler so the Kerberos password gets set properly.
+
+ oldpass is not mandatory
+ """
+
+ sctrl = self.__get_server_controls__()
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.passwd_s(dn, oldpass, newpass)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+ return "Success"
+
+ def __wrapmethods(self):
+ """This wraps all methods of SimpleLDAPObject, so that we can intercept
+ the methods that deal with entries. Instead of using a raw list of tuples
+ of lists of hashes of arrays as the entry object, we want to wrap entries
+ in an Entry class that provides some useful methods"""
+ for name in dir(self.__class__.__bases__[0]):
+ attr = getattr(self, name)
+ if callable(attr):
+ setattr(self, name, wrapper(attr, name))
+
+ def exportLDIF(self, file, suffix, forrepl=False, verbose=False):
+ cn = "export" + str(int(time.time()))
+ dn = "cn=%s, cn=export, cn=tasks, cn=config" % cn
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top', 'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('nsFilename', file)
+ entry.setValues('nsIncludeSuffix', suffix)
+ if forrepl:
+ entry.setValues('nsExportReplica', 'true')
+
+ rc = self.startTaskAndWait(entry, verbose)
+
+ if rc:
+ if verbose:
+ print "Error: export task %s for file %s exited with %d" % (cn,file,rc)
+ else:
+ if verbose:
+ print "Export task %s for file %s completed successfully" % (cn,file)
+ return rc
+
+ def waitForEntry(self, dn, timeout=7200, attr='', quiet=True):
+ scope = ldap.SCOPE_BASE
+ filter = "(objectclass=*)"
+ attrlist = []
+ if attr:
+ filter = "(%s=*)" % attr
+ attrlist.append(attr)
+ timeout += int(time.time())
+
+ if isinstance(dn,Entry):
+ dn = dn.dn
+
+ # wait for entry and/or attr to show up
+ if not quiet:
+ sys.stdout.write("Waiting for %s %s:%s " % (self,dn,attr))
+ sys.stdout.flush()
+ entry = None
+ while not entry and int(time.time()) < timeout:
+ try:
+ entry = self.getEntry(dn, scope, filter, attrlist)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ pass # found entry, but no attr
+ except ldap.NO_SUCH_OBJECT:
+ pass # no entry yet
+ except ldap.LDAPError, e: # badness
+ print "\nError reading entry", dn, e
+ break
+ if not entry:
+ if not quiet:
+ sys.stdout.write(".")
+ sys.stdout.flush()
+ time.sleep(1)
+
+ if not entry and int(time.time()) > timeout:
+ print "\nwaitForEntry timeout for %s for %s" % (self,dn)
+ elif entry and not quiet:
+ print "\nThe waited for entry is:", entry
+ elif not entry:
+ print "\nError: could not read entry %s from %s" % (dn,self)
+
+ return entry
+
+ def addSchema(self, attr, val):
+ dn = "cn=schema"
+ self.modify_s(dn, [(ldap.MOD_ADD, attr, val)])
+
+ def addAttr(self, *args):
+ return self.addSchema('attributeTypes', args)
+
+ def addObjClass(self, *args):
+ return self.addSchema('objectClasses', args)
+
+ ###########################
+ # Static methods start here
+ ###########################
+ def normalizeDN(dn):
+ # not great, but will do until we use a newer version of python-ldap
+ # that has DN utilities
+ ary = ldap.explode_dn(dn.lower())
+ return ",".join(ary)
+ normalizeDN = staticmethod(normalizeDN)
+
+ def getfqdn(name=''):
+ return socket.getfqdn(name)
+ getfqdn = staticmethod(getfqdn)
+
+ def getdomainname(name=''):
+ fqdn = IPAdmin.getfqdn(name)
+ index = fqdn.find('.')
+ if index >= 0:
+ return fqdn[index+1:]
+ else:
+ return fqdn
+ getdomainname = staticmethod(getdomainname)
+
+ def getdefaultsuffix(name=''):
+ dm = IPAdmin.getdomainname(name)
+ if dm:
+ return "dc=" + dm.replace('.', ', dc=')
+ else:
+ return 'dc=localdomain'
+ getdefaultsuffix = staticmethod(getdefaultsuffix)
+
+ def is_a_dn(dn):
+ """Returns True if the given string is a DN, False otherwise."""
+ return (dn.find("=") > 0)
+ is_a_dn = staticmethod(is_a_dn)
+
+
+def notfound(args):
+ """Return a string suitable for displaying as an error when a
+ search returns no results.
+
+ This just returns whatever is after the equals sign"""
+ if len(args) > 2:
+ filter = args[2]
+ try:
+ target = re.match(r'\(.*=(.*)\)', filter).group(1)
+ except:
+ target = filter
+ return "%s not found" % str(target)
+ else:
+ return args[0]
diff --git a/ipaserver/install/krbinstance.py b/ipaserver/install/krbinstance.py
new file mode 100644
index 000000000..252844304
--- /dev/null
+++ b/ipaserver/install/krbinstance.py
@@ -0,0 +1,428 @@
+# Authors: Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import subprocess
+import string
+import tempfile
+import shutil
+import logging
+import fileinput
+import re
+import sys
+import os
+import pwd
+import socket
+import shutil
+
+import service
+import installutils
+from ipa import sysrestore
+from ipa import ipautil
+from ipa import ipaerror
+
+import ipaldap
+
+import ldap
+from ldap import LDAPError
+from ldap import ldapobject
+
+from pyasn1.type import univ, namedtype
+import pyasn1.codec.ber.encoder
+import pyasn1.codec.ber.decoder
+import struct
+import base64
+
+KRBMKEY_DENY_ACI = """
+(targetattr = "krbMKey")(version 3.0; acl "No external access"; deny (all) userdn != "ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";)
+"""
+
+def update_key_val_in_file(filename, key, val):
+ if os.path.exists(filename):
+ pattern = "^[\s#]*%s\s*=\s*%s\s*" % (re.escape(key), re.escape(val))
+ p = re.compile(pattern)
+ for line in fileinput.input(filename):
+ if p.search(line):
+ fileinput.close()
+ return
+ fileinput.close()
+
+ pattern = "^[\s#]*%s\s*=" % re.escape(key)
+ p = re.compile(pattern)
+ for line in fileinput.input(filename, inplace=1):
+ if not p.search(line):
+ sys.stdout.write(line)
+ fileinput.close()
+ f = open(filename, "a")
+ f.write("%s=%s\n" % (key, val))
+ f.close()
+
+class KpasswdInstance(service.SimpleServiceInstance):
+ def __init__(self):
+ service.SimpleServiceInstance.__init__(self, "ipa_kpasswd")
+
+class KrbInstance(service.Service):
+ def __init__(self, fstore=None):
+ service.Service.__init__(self, "krb5kdc")
+ self.ds_user = None
+ self.fqdn = None
+ self.realm = None
+ self.domain = None
+ self.host = None
+ self.admin_password = None
+ self.master_password = None
+ self.suffix = None
+ self.kdc_password = None
+ self.sub_dict = None
+
+ self.kpasswd = KpasswdInstance()
+
+ if fstore:
+ self.fstore = fstore
+ else:
+ self.fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore')
+
+ def __common_setup(self, ds_user, realm_name, host_name, domain_name, admin_password):
+ self.ds_user = ds_user
+ self.fqdn = host_name
+ self.realm = realm_name.upper()
+ self.host = host_name.split(".")[0]
+ self.ip = socket.gethostbyname(host_name)
+ self.domain = domain_name
+ self.suffix = ipautil.realm_to_suffix(self.realm)
+ self.kdc_password = ipautil.ipa_generate_password()
+ self.admin_password = admin_password
+
+ self.__setup_sub_dict()
+
+ # get a connection to the DS
+ try:
+ self.conn = ipaldap.IPAdmin(self.fqdn)
+ self.conn.do_simple_bind(bindpw=self.admin_password)
+ except Exception, e:
+ logging.critical("Could not connect to the Directory Server on %s" % self.fqdn)
+ raise e
+
+ self.backup_state("running", self.is_running())
+ try:
+ self.stop()
+ except:
+ # It could have been not running
+ pass
+
+ def __common_post_setup(self):
+ self.step("starting the KDC", self.__start_instance)
+ self.step("configuring KDC to start on boot", self.__enable)
+
+ def create_instance(self, ds_user, realm_name, host_name, domain_name, admin_password, master_password):
+ self.master_password = master_password
+
+ self.__common_setup(ds_user, realm_name, host_name, domain_name, admin_password)
+
+ self.step("setting KDC account password", self.__configure_kdc_account_password)
+ self.step("adding sasl mappings to the directory", self.__configure_sasl_mappings)
+ self.step("adding kerberos entries to the DS", self.__add_krb_entries)
+ self.step("adding default ACIs", self.__add_default_acis)
+ self.step("configuring KDC", self.__create_instance)
+ self.step("adding default keytypes", self.__add_default_keytypes)
+ self.step("creating a keytab for the directory", self.__create_ds_keytab)
+ self.step("creating a keytab for the machine", self.__create_host_keytab)
+ self.step("exporting the kadmin keytab", self.__export_kadmin_changepw_keytab)
+ self.step("adding the password extension to the directory", self.__add_pwd_extop_module)
+ self.step("adding the kerberos master key to the directory", self.__add_master_key)
+
+ self.__common_post_setup()
+
+ self.start_creation("Configuring Kerberos KDC")
+
+ self.kpasswd.create_instance()
+
+ def create_replica(self, ds_user, realm_name, host_name, domain_name, admin_password, ldap_passwd_filename, kpasswd_filename):
+ self.__copy_ldap_passwd(ldap_passwd_filename)
+ self.__copy_kpasswd_keytab(kpasswd_filename)
+
+ self.__common_setup(ds_user, realm_name, host_name, domain_name, admin_password)
+
+ self.step("adding sasl mappings to the directory", self.__configure_sasl_mappings)
+ self.step("writing stash file from DS", self.__write_stash_from_ds)
+ self.step("configuring KDC", self.__create_replica_instance)
+ self.step("creating a keytab for the directory", self.__create_ds_keytab)
+ self.step("creating a keytab for the machine", self.__create_host_keytab)
+ self.step("adding the password extension to the directory", self.__add_pwd_extop_module)
+
+ self.__common_post_setup()
+
+ self.start_creation("Configuring Kerberos KDC")
+
+ self.kpasswd.create_instance()
+
+ def __copy_ldap_passwd(self, filename):
+ self.fstore.backup_file("/var/kerberos/krb5kdc/ldappwd")
+ shutil.copy(filename, "/var/kerberos/krb5kdc/ldappwd")
+ os.chmod("/var/kerberos/krb5kdc/ldappwd", 0600)
+
+ def __copy_kpasswd_keytab(self, filename):
+ self.fstore.backup_file("/var/kerberos/krb5kdc/kpasswd.keytab")
+ shutil.copy(filename, "/var/kerberos/krb5kdc/kpasswd.keytab")
+ os.chmod("/var/kerberos/krb5kdc/kpasswd.keytab", 0600)
+
+
+ def __configure_kdc_account_password(self):
+ hexpwd = ''
+ for x in self.kdc_password:
+ hexpwd += (hex(ord(x))[2:])
+ self.fstore.backup_file("/var/kerberos/krb5kdc/ldappwd")
+ pwd_fd = open("/var/kerberos/krb5kdc/ldappwd", "w")
+ pwd_fd.write("uid=kdc,cn=sysaccounts,cn=etc,"+self.suffix+"#{HEX}"+hexpwd+"\n")
+ pwd_fd.close()
+ os.chmod("/var/kerberos/krb5kdc/ldappwd", 0600)
+
+ def __enable(self):
+ self.backup_state("enabled", self.is_enabled())
+ self.chkconfig_on()
+
+ def __start_instance(self):
+ try:
+ self.start()
+ except:
+ logging.critical("krb5kdc service failed to start")
+
+ def __setup_sub_dict(self):
+ self.sub_dict = dict(FQDN=self.fqdn,
+ IP=self.ip,
+ PASSWORD=self.kdc_password,
+ SUFFIX=self.suffix,
+ DOMAIN=self.domain,
+ HOST=self.host,
+ REALM=self.realm)
+
+ def __ldap_mod(self, ldif):
+ txt = ipautil.template_file(ipautil.SHARE_DIR + ldif, self.sub_dict)
+ fd = ipautil.write_tmp_file(txt)
+
+ [pw_fd, pw_name] = tempfile.mkstemp()
+ os.write(pw_fd, self.admin_password)
+ os.close(pw_fd)
+
+ args = ["/usr/bin/ldapmodify", "-h", "127.0.0.1", "-xv",
+ "-D", "cn=Directory Manager", "-y", pw_name, "-f", fd.name]
+
+ try:
+ try:
+ ipautil.run(args)
+ except ipautil.CalledProcessError, e:
+ logging.critical("Failed to load %s: %s" % (ldif, str(e)))
+ finally:
+ os.remove(pw_name)
+
+ fd.close()
+
+ def __configure_sasl_mappings(self):
+ # we need to remove any existing SASL mappings in the directory as otherwise they
+ # they may conflict. There is no way to define the order they are used in atm.
+
+ # FIXME: for some reason IPAdmin dies here, so we switch
+ # it out for a regular ldapobject.
+ conn = self.conn
+ self.conn = ldapobject.SimpleLDAPObject("ldap://127.0.0.1/")
+ self.conn.bind("cn=directory manager", self.admin_password)
+ try:
+ msgid = self.conn.search("cn=mapping,cn=sasl,cn=config", ldap.SCOPE_ONELEVEL, "(objectclass=nsSaslMapping)")
+ res = self.conn.result(msgid)
+ for r in res[1]:
+ mid = self.conn.delete_s(r[0])
+ #except LDAPError, e:
+ # logging.critical("Error during SASL mapping removal: %s" % str(e))
+ except Exception, e:
+ logging.critical("Could not connect to the Directory Server on %s" % self.fqdn)
+ raise e
+ print type(e)
+ print dir(e)
+ raise e
+
+ self.conn = conn
+
+ entry = ipaldap.Entry("cn=Full Principal,cn=mapping,cn=sasl,cn=config")
+ entry.setValues("objectclass", "top", "nsSaslMapping")
+ entry.setValues("cn", "Full Principal")
+ entry.setValues("nsSaslMapRegexString", '\(.*\)@\(.*\)')
+ entry.setValues("nsSaslMapBaseDNTemplate", self.suffix)
+ entry.setValues("nsSaslMapFilterTemplate", '(krbPrincipalName=\\1@\\2)')
+
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ logging.critical("failed to add Full Principal Sasl mapping")
+ raise e
+
+ entry = ipaldap.Entry("cn=Name Only,cn=mapping,cn=sasl,cn=config")
+ entry.setValues("objectclass", "top", "nsSaslMapping")
+ entry.setValues("cn", "Name Only")
+ entry.setValues("nsSaslMapRegexString", '\(.*\)')
+ entry.setValues("nsSaslMapBaseDNTemplate", self.suffix)
+ entry.setValues("nsSaslMapFilterTemplate", '(krbPrincipalName=\\1@%s)' % self.realm)
+
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ logging.critical("failed to add Name Only Sasl mapping")
+ raise e
+
+ def __add_krb_entries(self):
+ self.__ldap_mod("kerberos.ldif")
+
+ def __add_default_acis(self):
+ self.__ldap_mod("default-aci.ldif")
+
+ def __add_default_keytypes(self):
+ self.__ldap_mod("default-keytypes.ldif")
+
+ def __create_replica_instance(self):
+ self.__create_instance(replica=True)
+
+ def __template_file(self, path):
+ template = os.path.join(ipautil.SHARE_DIR, os.path.basename(path) + ".template")
+ conf = ipautil.template_file(template, self.sub_dict)
+ self.fstore.backup_file(path)
+ fd = open(path, "w+")
+ fd.write(conf)
+ fd.close()
+
+ def __create_instance(self, replica=False):
+ self.__template_file("/var/kerberos/krb5kdc/kdc.conf")
+ self.__template_file("/etc/krb5.conf")
+ self.__template_file("/usr/share/ipa/html/krb5.ini")
+ self.__template_file("/usr/share/ipa/html/krb.con")
+ self.__template_file("/usr/share/ipa/html/krbrealm.con")
+
+ if not replica:
+ #populate the directory with the realm structure
+ args = ["/usr/kerberos/sbin/kdb5_ldap_util", "-D", "uid=kdc,cn=sysaccounts,cn=etc,"+self.suffix, "-w", self.kdc_password, "create", "-s", "-P", self.master_password, "-r", self.realm, "-subtrees", self.suffix, "-sscope", "sub"]
+ try:
+ ipautil.run(args)
+ except ipautil.CalledProcessError, e:
+ print "Failed to populate the realm structure in kerberos", e
+
+ def __write_stash_from_ds(self):
+ try:
+ entry = self.conn.getEntry("cn=%s, cn=kerberos, %s" % (self.realm, self.suffix), ldap.SCOPE_SUBTREE)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), e:
+ logging.critical("Could not find master key in DS")
+ raise e
+
+ krbMKey = pyasn1.codec.ber.decoder.decode(entry.krbmkey)
+ keytype = int(krbMKey[0][1][0])
+ keydata = str(krbMKey[0][1][1])
+
+ format = '=hi%ss' % len(keydata)
+ s = struct.pack(format, keytype, len(keydata), keydata)
+ try:
+ fd = open("/var/kerberos/krb5kdc/.k5."+self.realm, "w")
+ fd.write(s)
+ fd.close()
+ except os.error, e:
+ logging.critical("failed to write stash file")
+ raise e
+
+ #add the password extop module
+ def __add_pwd_extop_module(self):
+ self.__ldap_mod("pwd-extop-conf.ldif")
+
+ def __add_master_key(self):
+ #get the Master Key from the stash file
+ try:
+ stash = open("/var/kerberos/krb5kdc/.k5."+self.realm, "r")
+ keytype = struct.unpack('h', stash.read(2))[0]
+ keylen = struct.unpack('i', stash.read(4))[0]
+ keydata = stash.read(keylen)
+ except os.error:
+ logging.critical("Failed to retrieve Master Key from Stash file: %s")
+ #encode it in the asn.1 attribute
+ MasterKey = univ.Sequence()
+ MasterKey.setComponentByPosition(0, univ.Integer(keytype))
+ MasterKey.setComponentByPosition(1, univ.OctetString(keydata))
+ krbMKey = univ.Sequence()
+ krbMKey.setComponentByPosition(0, univ.Integer(0)) #we have no kvno
+ krbMKey.setComponentByPosition(1, MasterKey)
+ asn1key = pyasn1.codec.ber.encoder.encode(krbMKey)
+
+ dn = "cn="+self.realm+",cn=kerberos,"+self.suffix
+ #protect the master key by adding an appropriate deny rule along with the key
+ mod = [(ldap.MOD_ADD, 'aci', ipautil.template_str(KRBMKEY_DENY_ACI, self.sub_dict)),
+ (ldap.MOD_ADD, 'krbMKey', str(asn1key))]
+ try:
+ self.conn.modify_s(dn, mod)
+ except ldap.TYPE_OR_VALUE_EXISTS, e:
+ logging.critical("failed to add master key to kerberos database\n")
+ raise e
+
+ def __create_ds_keytab(self):
+ ldap_principal = "ldap/" + self.fqdn + "@" + self.realm
+ installutils.kadmin_addprinc(ldap_principal)
+
+ self.fstore.backup_file("/etc/dirsrv/ds.keytab")
+ installutils.create_keytab("/etc/dirsrv/ds.keytab", ldap_principal)
+
+ self.fstore.backup_file("/etc/sysconfig/dirsrv")
+ update_key_val_in_file("/etc/sysconfig/dirsrv", "export KRB5_KTNAME", "/etc/dirsrv/ds.keytab")
+ pent = pwd.getpwnam(self.ds_user)
+ os.chown("/etc/dirsrv/ds.keytab", pent.pw_uid, pent.pw_gid)
+
+ def __create_host_keytab(self):
+ host_principal = "host/" + self.fqdn + "@" + self.realm
+ installutils.kadmin_addprinc(host_principal)
+
+ self.fstore.backup_file("/etc/krb5.keytab")
+ installutils.create_keytab("/etc/krb5.keytab", host_principal)
+
+ # Make sure access is strictly reserved to root only for now
+ os.chown("/etc/krb5.keytab", 0, 0)
+ os.chmod("/etc/krb5.keytab", 0600)
+
+ def __export_kadmin_changepw_keytab(self):
+ installutils.kadmin_modprinc("kadmin/changepw", "+requires_preauth")
+
+ self.fstore.backup_file("/var/kerberos/krb5kdc/kpasswd.keytab")
+ installutils.create_keytab("/var/kerberos/krb5kdc/kpasswd.keytab", "kadmin/changepw")
+
+ self.fstore.backup_file("/etc/sysconfig/ipa_kpasswd")
+ update_key_val_in_file("/etc/sysconfig/ipa_kpasswd", "export KRB5_KTNAME", "/var/kerberos/krb5kdc/kpasswd.keytab")
+
+ def uninstall(self):
+ self.kpasswd.uninstall()
+
+ running = self.restore_state("running")
+ enabled = self.restore_state("enabled")
+
+ try:
+ self.stop()
+ except:
+ pass
+
+ for f in ["/var/kerberos/krb5kdc/ldappwd", "/var/kerberos/krb5kdc/kdc.conf", "/etc/krb5.conf"]:
+ try:
+ self.fstore.restore_file(f)
+ except ValueError, error:
+ logging.debug(error)
+ pass
+
+ if not enabled is None and not enabled:
+ self.chkconfig_off()
+
+ if not running is None and running:
+ self.start()
diff --git a/ipaserver/install/ldapupdate.py b/ipaserver/install/ldapupdate.py
new file mode 100755
index 000000000..cdf23125a
--- /dev/null
+++ b/ipaserver/install/ldapupdate.py
@@ -0,0 +1,593 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2008 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+# Documentation can be found at http://freeipa.org/page/LdapUpdate
+
+# TODO
+# save undo files?
+
+UPDATES_DIR="/usr/share/ipa/updates/"
+
+import sys
+from ipaserver import ipaldap, installutils
+from ipa import entity, ipaerror, ipautil
+import ldap
+import logging
+import krbV
+import platform
+import shlex
+import time
+import random
+import os
+import fnmatch
+
+class BadSyntax(Exception):
+ def __init__(self, value):
+ self.value = value
+ def __str__(self):
+ return repr(self.value)
+
+class LDAPUpdate:
+ def __init__(self, dm_password, sub_dict={}, live_run=True):
+ """dm_password = Directory Manager password
+ sub_dict = substitution dictionary
+ live_run = Apply the changes or just test
+ """
+ self.sub_dict = sub_dict
+ self.live_run = live_run
+ self.dm_password = dm_password
+ self.conn = None
+ self.modified = False
+
+ krbctx = krbV.default_context()
+
+ fqdn = installutils.get_fqdn()
+ if fqdn is None:
+ raise RuntimeError("Unable to determine hostname")
+
+ domain = ipautil.get_domain_name()
+ libarch = self.__identify_arch()
+ suffix = ipautil.realm_to_suffix(krbctx.default_realm)
+
+ if not self.sub_dict.get("REALM"):
+ self.sub_dict["REALM"] = krbctx.default_realm
+ if not self.sub_dict.get("FQDN"):
+ self.sub_dict["FQDN"] = fqdn
+ if not self.sub_dict.get("DOMAIN"):
+ self.sub_dict["DOMAIN"] = domain
+ if not self.sub_dict.get("SUFFIX"):
+ self.sub_dict["SUFFIX"] = suffix
+ if not self.sub_dict.get("LIBARCH"):
+ self.sub_dict["LIBARCH"] = libarch
+ if not self.sub_dict.get("TIME"):
+ self.sub_dict["TIME"] = int(time.time())
+
+ # Try out the password
+ try:
+ conn = ipaldap.IPAdmin(fqdn)
+ conn.do_simple_bind(bindpw=self.dm_password)
+ conn.unbind()
+ except ldap.CONNECT_ERROR, e:
+ raise RuntimeError("Unable to connect to LDAP server %s" % fqdn)
+ except ldap.SERVER_DOWN, e:
+ raise RuntimeError("Unable to connect to LDAP server %s" % fqdn)
+ except ldap.INVALID_CREDENTIALS, e :
+ raise RuntimeError("The password provided is incorrect for LDAP server %s" % fqdn)
+
+ def __detail_error(self, detail):
+ """IPA returns two errors back. One a generic one indicating the broad
+ problem and a detailed message back as well which should have come
+ from LDAP. This function will parse that into a human-readable
+ string.
+ """
+ msg = ""
+ desc = detail[0].get('desc')
+ info = detail[0].get('info')
+
+ if desc:
+ msg = desc
+ if info:
+ msg = msg + " " + info
+
+ return msg
+
+ def __identify_arch(self):
+ """On multi-arch systems some libraries may be in /lib64, /usr/lib64,
+ etc. Determine if a suffix is needed based on the current
+ architecture.
+ """
+ bits = platform.architecture()[0]
+
+ if bits == "64bit":
+ return "64"
+ else:
+ return ""
+
+ def __template_str(self, s):
+ try:
+ return ipautil.template_str(s, self.sub_dict)
+ except KeyError, e:
+ raise BadSyntax("Unknown template keyword %s" % e)
+
+ def __remove_quotes(self, line):
+ """Remove leading and trailng double or single quotes"""
+ if line.startswith('"'):
+ line = line[1:]
+ if line.endswith('"'):
+ line = line[:-1]
+ if line.startswith("'"):
+ line = line[1:]
+ if line.endswith("'"):
+ line = line[:-1]
+
+ return line
+
+ def __parse_values(self, line):
+ """Parse a comma-separated string into separate values and convert them
+ into a list. This should handle quoted-strings with embedded commas
+ """
+ lexer = shlex.shlex(line)
+ lexer.wordchars = lexer.wordchars + ".()-"
+ l = []
+ v = ""
+ for token in lexer:
+ if token != ',':
+ if v:
+ v = v + " " + token
+ else:
+ v = token
+ else:
+ l.append(self.__remove_quotes(v))
+ v = ""
+
+ l.append(self.__remove_quotes(v))
+
+ return l
+
+ def read_file(self, filename):
+ if filename == '-':
+ fd = sys.stdin
+ else:
+ fd = open(filename)
+ text = fd.readlines()
+ if fd != sys.stdin: fd.close()
+ return text
+
+ def __entry_to_entity(self, ent):
+ """Tne Entry class is a bare LDAP entry. The Entity class has a lot more
+ helper functions that we need, so convert to dict and then to Entity.
+ """
+ entry = dict(ent.data)
+ entry['dn'] = ent.dn
+ for key,value in entry.iteritems():
+ if isinstance(value,list) or isinstance(value,tuple):
+ if len(value) == 0:
+ entry[key] = ''
+ elif len(value) == 1:
+ entry[key] = value[0]
+ return entity.Entity(entry)
+
+ def __combine_updates(self, dn_list, all_updates, update):
+ """Combine a new update with the list of total updates
+
+ Updates are stored in 2 lists:
+ dn_list: contains a unique list of DNs in the updates
+ all_updates: the actual updates that need to be applied
+
+ We want to apply the updates from the shortest to the longest
+ path so if new child and parent entries are in different updates
+ we can be sure the parent gets written first. This also lets
+ us apply any schema first since it is in the very short cn=schema.
+ """
+ dn = update.get('dn')
+ dns = ldap.explode_dn(dn.lower())
+ l = len(dns)
+ if dn_list.get(l):
+ if dn not in dn_list[l]:
+ dn_list[l].append(dn)
+ else:
+ dn_list[l] = [dn]
+ if not all_updates.get(dn):
+ all_updates[dn] = update
+ return all_updates
+
+ e = all_updates[dn]
+ e['updates'] = e['updates'] + update['updates']
+
+ all_updates[dn] = e
+
+ return all_updates
+
+ def parse_update_file(self, data, all_updates, dn_list):
+ """Parse the update file into a dictonary of lists and apply the update
+ for each DN in the file."""
+ valid_keywords = ["default", "add", "remove", "only"]
+ update = {}
+ d = ""
+ index = ""
+ dn = None
+ lcount = 0
+ for line in data:
+ # Strip out \n and extra white space
+ lcount = lcount + 1
+
+ # skip comments and empty lines
+ line = line.rstrip()
+ if line.startswith('#') or line == '': continue
+
+ if line.lower().startswith('dn:'):
+ if dn is not None:
+ all_updates = self.__combine_updates(dn_list, all_updates, update)
+
+ update = {}
+ dn = line[3:].strip()
+ update['dn'] = self.__template_str(dn)
+ else:
+ if dn is None:
+ raise BadSyntax, "dn is not defined in the update"
+
+ if line.startswith(' '):
+ v = d[len(d) - 1]
+ v = v + " " + line.strip()
+ d[len(d) - 1] = v
+ update[index] = d
+ continue
+ line = line.strip()
+ values = line.split(':', 2)
+ if len(values) != 3:
+ raise BadSyntax, "Bad formatting on line %d: %s" % (lcount,line)
+
+ index = values[0].strip().lower()
+
+ if index not in valid_keywords:
+ raise BadSyntax, "Unknown keyword %s" % index
+
+ attr = values[1].strip()
+ value = values[2].strip()
+ value = self.__template_str(value)
+
+ new_value = ""
+ if index == "default":
+ new_value = attr + ":" + value
+ else:
+ new_value = index + ":" + attr + ":" + value
+ index = "updates"
+
+ d = update.get(index, [])
+
+ d.append(new_value)
+
+ update[index] = d
+
+ if dn is not None:
+ all_updates = self.__combine_updates(dn_list, all_updates, update)
+
+ return (all_updates, dn_list)
+
+ def create_index_task(self, attribute):
+ """Create a task to update an index for an attribute"""
+
+ r = random.SystemRandom()
+
+ # Refresh the time to make uniqueness more probable. Add on some
+ # randomness for good measure.
+ self.sub_dict['TIME'] = int(time.time()) + r.randint(0,10000)
+
+ cn = self.__template_str("indextask_$TIME")
+ dn = "cn=%s, cn=index, cn=tasks, cn=config" % cn
+
+ e = ipaldap.Entry(dn)
+
+ e.setValues('objectClass', ['top', 'extensibleObject'])
+ e.setValue('cn', cn)
+ e.setValue('nsInstance', 'userRoot')
+ e.setValues('nsIndexAttribute', attribute)
+
+ logging.info("Creating task to index attribute: %s", attribute)
+ logging.debug("Task id: %s", dn)
+
+ if self.live_run:
+ self.conn.addEntry(e.dn, e.toTupleList())
+
+ return dn
+
+ def monitor_index_task(self, dn):
+ """Give a task DN monitor it and wait until it has completed (or failed)
+ """
+
+ if not self.live_run:
+ # If not doing this live there is nothing to monitor
+ return
+
+ # Pause for a moment to give the task time to be created
+ time.sleep(1)
+
+ attrlist = ['nstaskstatus', 'nstaskexitcode']
+ entry = None
+
+ while True:
+ try:
+ entry = self.conn.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ logging.error("Task not found: %s", dn)
+ return
+ except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e:
+ logging.error("Task lookup failure %s: %s", e, self.__detail_error(e.detail))
+ return
+
+ status = entry.getValue('nstaskstatus')
+ if status is None:
+ # task doesn't have a status yet
+ time.sleep(1)
+ continue
+
+ if status.lower().find("finished") > -1:
+ logging.info("Indexing finished")
+ break
+
+ logging.debug("Indexing in progress")
+ time.sleep(1)
+
+ return
+
+ def __create_default_entry(self, dn, default):
+ """Create the default entry from the values provided.
+
+ The return type is entity.Entity
+ """
+ entry = ipaldap.Entry(dn)
+
+ if not default:
+ # This means that the entire entry needs to be created with add
+ return self.__entry_to_entity(entry)
+
+ for line in default:
+ # We already do syntax-parsing so this is safe
+ (k, v) = line.split(':',1)
+ e = entry.getValues(k)
+ if e:
+ # multi-valued attribute
+ e = list(e)
+ e.append(v)
+ else:
+ e = v
+ entry.setValues(k, e)
+
+ return self.__entry_to_entity(entry)
+
+ def __get_entry(self, dn):
+ """Retrieve an object from LDAP.
+
+ The return type is ipaldap.Entry
+ """
+ searchfilter="objectclass=*"
+ sattrs = ["*"]
+ scope = ldap.SCOPE_BASE
+
+ return self.conn.getList(dn, scope, searchfilter, sattrs)
+
+ def __apply_updates(self, updates, entry):
+ """updates is a list of changes to apply
+ entry is the thing to apply them to
+
+ returns the modified entry
+ """
+ if not updates:
+ return entry
+
+ only = {}
+ for u in updates:
+ # We already do syntax-parsing so this is safe
+ (utype, k, values) = u.split(':',2)
+
+ values = self.__parse_values(values)
+
+ e = entry.getValues(k)
+ if not isinstance(e, list):
+ if e is None:
+ e = []
+ else:
+ e = [e]
+
+ for v in values:
+ if utype == 'remove':
+ logging.debug("remove: '%s' from %s, current value %s", v, k, e)
+ try:
+ e.remove(v)
+ except ValueError:
+ logging.warn("remove: '%s' not in %s", v, k)
+ pass
+ entry.setValues(k, e)
+ logging.debug('remove: updated value %s', e)
+ elif utype == 'add':
+ logging.debug("add: '%s' to %s, current value %s", v, k, e)
+ # Remove it, ignoring errors so we can blindly add it later
+ try:
+ e.remove(v)
+ except ValueError:
+ pass
+ e.append(v)
+ logging.debug('add: updated value %s', e)
+ entry.setValues(k, e)
+ elif utype == 'only':
+ logging.debug("only: set %s to '%s', current value %s", k, v, e)
+ if only.get(k):
+ e.append(v)
+ else:
+ e = [v]
+ only[k] = True
+ entry.setValues(k, e)
+ logging.debug('only: updated value %s', e)
+
+ self.print_entity(entry)
+
+ return entry
+
+ def print_entity(self, e, message=None):
+ """The entity object currently lacks a str() method"""
+ logging.debug("---------------------------------------------")
+ if message:
+ logging.debug("%s", message)
+ logging.debug("dn: " + e.dn)
+ attr = e.attrList()
+ for a in attr:
+ value = e.getValues(a)
+ if isinstance(value,str):
+ logging.debug(a + ": " + value)
+ else:
+ logging.debug(a + ": ")
+ for l in value:
+ logging.debug("\t" + l)
+ def is_schema_updated(self, s):
+ """Compare the schema in 's' with the current schema in the DS to
+ see if anything has changed. This should account for syntax
+ differences (like added parens that make no difference but are
+ detected as a change by generateModList()).
+
+ This doesn't handle re-ordering of attributes. They are still
+ detected as changes, so foo $ bar != bar $ foo.
+
+ return True if the schema has changed
+ return False if it has not
+ """
+ s = ldap.schema.SubSchema(s)
+ s = s.ldap_entry()
+
+ # Get a fresh copy and convert into a SubSchema
+ n = self.__get_entry("cn=schema")[0]
+ n = dict(n.data)
+ n = ldap.schema.SubSchema(n)
+ n = n.ldap_entry()
+
+ if s == n:
+ return False
+ else:
+ return True
+
+ def __update_record(self, update):
+ found = False
+
+ new_entry = self.__create_default_entry(update.get('dn'),
+ update.get('default'))
+
+ try:
+ e = self.__get_entry(new_entry.dn)
+ if len(e) > 1:
+ # we should only ever get back one entry
+ raise BadSyntax, "More than 1 entry returned on a dn search!? %s" % new_entry.dn
+ entry = self.__entry_to_entity(e[0])
+ found = True
+ logging.info("Updating existing entry: %s", entry.dn)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # Doesn't exist, start with the default entry
+ entry = new_entry
+ logging.info("New entry: %s", entry.dn)
+ except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR):
+ # Doesn't exist, start with the default entry
+ entry = new_entry
+ logging.info("New entry, using default value: %s", entry.dn)
+
+ self.print_entity(entry)
+
+ # Bring this entry up to date
+ entry = self.__apply_updates(update.get('updates'), entry)
+
+ self.print_entity(entry, "Final value")
+
+ if not found:
+ # New entries get their orig_data set to the entry itself. We want to
+ # empty that so that everything appears new when generating the
+ # modlist
+ # entry.orig_data = {}
+ try:
+ if self.live_run:
+ self.conn.addEntry(entry.dn, entry.toTupleList())
+ except Exception, e:
+ logging.error("Add failure %s: %s", e, self.__detail_error(e.detail))
+ else:
+ # Update LDAP
+ try:
+ updated = False
+ changes = self.conn.generateModList(entry.origDataDict(), entry.toDict())
+ if (entry.dn == "cn=schema"):
+ updated = self.is_schema_updated(entry.toDict())
+ else:
+ if len(changes) > 1:
+ updated = True
+ logging.debug("%s" % changes)
+ if self.live_run and updated:
+ self.conn.updateEntry(entry.dn, entry.origDataDict(), entry.toDict())
+ logging.info("Done")
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST), e:
+ logging.info("Entry already up-to-date")
+ updated = False
+ except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e:
+ logging.error("Update failed: %s: %s", e, self.__detail_error(e.detail))
+ updated = False
+
+ if ("cn=index" in entry.dn and
+ "cn=userRoot" in entry.dn):
+ taskid = self.create_index_task(entry.cn)
+ self.monitor_index_task(taskid)
+
+ if updated:
+ self.modified = True
+ return
+
+ def get_all_files(self, root, recursive=False):
+ """Get all update files"""
+ f = []
+ for path, subdirs, files in os.walk(root):
+ for name in files:
+ if fnmatch.fnmatch(name, "*.update"):
+ f.append(os.path.join(path, name))
+ if not recursive:
+ break
+ return f
+
+ def update(self, files):
+ """Execute the update. files is a list of the update files to use.
+
+ returns True if anything was changed, otherwise False
+ """
+
+ try:
+ self.conn = ipaldap.IPAdmin(self.sub_dict['FQDN'])
+ self.conn.do_simple_bind(bindpw=self.dm_password)
+ all_updates = {}
+ dn_list = {}
+ for f in files:
+ try:
+ logging.info("Parsing file %s" % f)
+ data = self.read_file(f)
+ except Exception, e:
+ print e
+ sys.exit(1)
+
+ (all_updates, dn_list) = self.parse_update_file(data, all_updates, dn_list)
+
+ sortedkeys = dn_list.keys()
+ sortedkeys.sort()
+ for k in sortedkeys:
+ for dn in dn_list[k]:
+ self.__update_record(all_updates[dn])
+ finally:
+ if self.conn: self.conn.unbind()
+
+ return self.modified
diff --git a/ipaserver/install/ntpinstance.py b/ipaserver/install/ntpinstance.py
new file mode 100644
index 000000000..e2ec60650
--- /dev/null
+++ b/ipaserver/install/ntpinstance.py
@@ -0,0 +1,107 @@
+# Authors: Karl MacMillan <kmacmillan@redhat.com>
+#
+# Copyright (C) 2007 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import shutil
+import logging
+
+import service
+from ipa import sysrestore
+from ipa import ipautil
+
+class NTPInstance(service.Service):
+ def __init__(self, fstore=None):
+ service.Service.__init__(self, "ntpd")
+
+ if fstore:
+ self.fstore = fstore
+ else:
+ self.fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore')
+
+ def __write_config(self):
+ # The template sets the config to point towards ntp.pool.org, but
+ # they request that software not point towards the default pool.
+ # We use the OS variable to point it towards either the rhel
+ # or fedora pools. Other distros should be added in the future
+ # or we can get our own pool.
+ os = ""
+ if ipautil.file_exists("/etc/fedora-release"):
+ os = "fedora"
+ elif ipautil.file_exists("/etc/redhat-release"):
+ os = "rhel"
+
+ sub_dict = { }
+ sub_dict["SERVERA"] = "0.%s.pool.ntp.org" % os
+ sub_dict["SERVERB"] = "1.%s.pool.ntp.org" % os
+ sub_dict["SERVERC"] = "2.%s.pool.ntp.org" % os
+
+ ntp_conf = ipautil.template_file(ipautil.SHARE_DIR + "ntp.conf.server.template", sub_dict)
+ ntp_sysconf = ipautil.template_file(ipautil.SHARE_DIR + "ntpd.sysconfig.template", {})
+
+ self.fstore.backup_file("/etc/ntp.conf")
+ self.fstore.backup_file("/etc/sysconfig/ntpd")
+
+ fd = open("/etc/ntp.conf", "w")
+ fd.write(ntp_conf)
+ fd.close()
+
+ fd = open("/etc/sysconfig/ntpd", "w")
+ fd.write(ntp_sysconf)
+ fd.close()
+
+ def __stop(self):
+ self.backup_state("running", self.is_running())
+ self.stop()
+
+ def __start(self):
+ self.start()
+
+ def __enable(self):
+ self.backup_state("enabled", self.is_enabled())
+ self.chkconfig_on()
+
+ def create_instance(self):
+
+ # we might consider setting the date manually using ntpd -qg in case
+ # the current time is very far off.
+
+ self.step("stopping ntpd", self.__stop)
+ self.step("writing configuration", self.__write_config)
+ self.step("configuring ntpd to start on boot", self.__enable)
+ self.step("starting ntpd", self.__start)
+
+ self.start_creation("Configuring ntpd")
+
+ def uninstall(self):
+ running = self.restore_state("running")
+ enabled = self.restore_state("enabled")
+
+ if not running is None:
+ self.stop()
+
+ try:
+ self.fstore.restore_file("/etc/ntp.conf")
+ except ValueError, error:
+ logging.debug(error)
+ pass
+
+ if not enabled is None and not enabled:
+ self.chkconfig_off()
+
+ if not running is None and running:
+ self.start()
diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py
new file mode 100644
index 000000000..8477bd18a
--- /dev/null
+++ b/ipaserver/install/replication.py
@@ -0,0 +1,532 @@
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import time, logging
+
+import ipaldap, ldap, dsinstance
+from ldap import modlist
+from ipa import ipaerror
+
+DIRMAN_CN = "cn=directory manager"
+CACERT="/usr/share/ipa/html/ca.crt"
+# the default container used by AD for user entries
+WIN_USER_CONTAINER="cn=Users"
+# the default container used by IPA for user entries
+IPA_USER_CONTAINER="cn=users,cn=accounts"
+PORT = 636
+TIMEOUT = 120
+
+IPA_REPLICA = 1
+WINSYNC = 2
+
+class ReplicationManager:
+ """Manage replication agreements between DS servers, and sync
+ agreements with Windows servers"""
+ def __init__(self, hostname, dirman_passwd):
+ self.hostname = hostname
+ self.dirman_passwd = dirman_passwd
+
+ self.conn = ipaldap.IPAdmin(hostname, port=PORT, cacert=CACERT)
+ self.conn.do_simple_bind(bindpw=dirman_passwd)
+
+ self.repl_man_passwd = dirman_passwd
+
+ # these are likely constant, but you could change them
+ # at runtime if you really want
+ self.repl_man_dn = "cn=replication manager,cn=config"
+ self.repl_man_cn = "replication manager"
+ self.suffix = ""
+
+ def _get_replica_id(self, conn, master_conn):
+ """
+ Returns the replica ID which is unique for each backend.
+
+ conn is the connection we are trying to get the replica ID for.
+ master_conn is the master we are going to replicate with.
+ """
+ # First see if there is already one set
+ dn = self.replica_dn()
+ try:
+ replica = conn.search_s(dn, ldap.SCOPE_BASE, "objectclass=*")[0]
+ if replica.getValue('nsDS5ReplicaId'):
+ return int(replica.getValue('nsDS5ReplicaId'))
+ except ldap.NO_SUCH_OBJECT:
+ pass
+
+ # Ok, either the entry doesn't exist or the attribute isn't set
+ # so get it from the other master
+ retval = -1
+ dn = "cn=replication, cn=etc, %s" % self.suffix
+ try:
+ replica = master_conn.search_s(dn, ldap.SCOPE_BASE, "objectclass=*")[0]
+ if not replica.getValue('nsDS5ReplicaId'):
+ logging.debug("Unable to retrieve nsDS5ReplicaId from remote server")
+ raise RuntimeError("Unable to retrieve nsDS5ReplicaId from remote server")
+ except ldap.NO_SUCH_OBJECT:
+ logging.debug("Unable to retrieve nsDS5ReplicaId from remote server")
+ raise
+
+ # Now update the value on the master
+ retval = int(replica.getValue('nsDS5ReplicaId'))
+ mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaId', str(retval + 1))]
+
+ try:
+ master_conn.modify_s(dn, mod)
+ except Exception, e:
+ logging.debug("Problem updating nsDS5ReplicaID %s" % e)
+ raise
+
+ return retval
+
+ def find_replication_dns(self, conn):
+ filt = "(|(objectclass=nsDSWindowsReplicationAgreement)(objectclass=nsds5ReplicationAgreement))"
+ try:
+ ents = conn.search_s("cn=mapping tree,cn=config", ldap.SCOPE_SUBTREE, filt)
+ except ldap.NO_SUCH_OBJECT:
+ return []
+ return [ent.dn for ent in ents]
+
+ def add_replication_manager(self, conn, passwd=None):
+ """
+ Create a pseudo user to use for replication. If no password
+ is provided the directory manager password will be used.
+ """
+
+ if passwd:
+ self.repl_man_passwd = passwd
+
+ ent = ipaldap.Entry(self.repl_man_dn)
+ ent.setValues("objectclass", "top", "person")
+ ent.setValues("cn", self.repl_man_cn)
+ ent.setValues("userpassword", self.repl_man_passwd)
+ ent.setValues("sn", "replication manager pseudo user")
+
+ try:
+ conn.add_s(ent)
+ except ldap.ALREADY_EXISTS:
+ # should we set the password here?
+ pass
+
+ def delete_replication_manager(self, conn, dn="cn=replication manager,cn=config"):
+ try:
+ conn.delete_s(dn)
+ except ldap.NO_SUCH_OBJECT:
+ pass
+
+ def get_replica_type(self, master=True):
+ if master:
+ return "3"
+ else:
+ return "2"
+
+ def replica_dn(self):
+ return 'cn=replica, cn="%s", cn=mapping tree, cn=config' % self.suffix
+
+ def local_replica_config(self, conn, replica_id):
+ dn = self.replica_dn()
+
+ try:
+ conn.getEntry(dn, ldap.SCOPE_BASE)
+ # replication is already configured
+ return
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ pass
+
+ replica_type = self.get_replica_type()
+
+ entry = ipaldap.Entry(dn)
+ entry.setValues('objectclass', "top", "nsds5replica", "extensibleobject")
+ entry.setValues('cn', "replica")
+ entry.setValues('nsds5replicaroot', self.suffix)
+ entry.setValues('nsds5replicaid', str(replica_id))
+ entry.setValues('nsds5replicatype', replica_type)
+ entry.setValues('nsds5flags', "1")
+ entry.setValues('nsds5replicabinddn', [self.repl_man_dn])
+ entry.setValues('nsds5replicalegacyconsumer', "off")
+
+ conn.add_s(entry)
+
+ def setup_changelog(self, conn):
+ dn = "cn=changelog5, cn=config"
+ dirpath = conn.dbdir + "/cldb"
+ entry = ipaldap.Entry(dn)
+ entry.setValues('objectclass', "top", "extensibleobject")
+ entry.setValues('cn', "changelog5")
+ entry.setValues('nsslapd-changelogdir', dirpath)
+ try:
+ conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ return
+
+ def setup_chaining_backend(self, conn):
+ chaindn = "cn=chaining database, cn=plugins, cn=config"
+ benamebase = "chaindb"
+ urls = [self.to_ldap_url(conn)]
+ cn = ""
+ benum = 1
+ done = False
+ while not done:
+ try:
+ cn = benamebase + str(benum) # e.g. localdb1
+ dn = "cn=" + cn + ", " + chaindn
+ entry = ipaldap.Entry(dn)
+ entry.setValues('objectclass', 'top', 'extensibleObject', 'nsBackendInstance')
+ entry.setValues('cn', cn)
+ entry.setValues('nsslapd-suffix', self.suffix)
+ entry.setValues('nsfarmserverurl', urls)
+ entry.setValues('nsmultiplexorbinddn', self.repl_man_dn)
+ entry.setValues('nsmultiplexorcredentials', self.repl_man_passwd)
+
+ self.conn.add_s(entry)
+ done = True
+ except ldap.ALREADY_EXISTS:
+ benum += 1
+ except ldap.LDAPError, e:
+ print "Could not add backend entry " + dn, e
+ raise
+
+ return cn
+
+ def to_ldap_url(self, conn):
+ return "ldap://%s:%d/" % (conn.host, conn.port)
+
+ def setup_chaining_farm(self, conn):
+ try:
+ conn.modify_s(self.suffix, [(ldap.MOD_ADD, 'aci',
+ [ "(targetattr = \"*\")(version 3.0; acl \"Proxied authorization for database links\"; allow (proxy) userdn = \"ldap:///%s\";)" % self.repl_man_dn ])])
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ logging.debug("proxy aci already exists in suffix %s on %s" % (self.suffix, conn.host))
+
+ def get_mapping_tree_entry(self):
+ try:
+ entry = self.conn.getEntry("cn=mapping tree,cn=config", ldap.SCOPE_ONELEVEL,
+ "(cn=\"%s\")" % (self.suffix))
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), e:
+ logging.debug("failed to find mappting tree entry for %s" % self.suffix)
+ raise e
+
+ return entry
+
+
+ def enable_chain_on_update(self, bename):
+ mtent = self.get_mapping_tree_entry()
+ dn = mtent.dn
+
+ plgent = self.conn.getEntry("cn=Multimaster Replication Plugin,cn=plugins,cn=config",
+ ldap.SCOPE_BASE, "(objectclass=*)", ['nsslapd-pluginPath'])
+ path = plgent.getValue('nsslapd-pluginPath')
+
+ mod = [(ldap.MOD_REPLACE, 'nsslapd-state', 'backend'),
+ (ldap.MOD_ADD, 'nsslapd-backend', bename),
+ (ldap.MOD_ADD, 'nsslapd-distribution-plugin', path),
+ (ldap.MOD_ADD, 'nsslapd-distribution-funct', 'repl_chain_on_update')]
+
+ try:
+ self.conn.modify_s(dn, mod)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ logging.debug("chainOnUpdate already enabled for %s" % self.suffix)
+
+ def setup_chain_on_update(self, other_conn):
+ chainbe = self.setup_chaining_backend(other_conn)
+ self.enable_chain_on_update(chainbe)
+
+ def add_passsync_user(self, conn, password):
+ pass_dn = "uid=passsync,cn=sysaccounts,cn=etc,%s" % self.suffix
+ print "The user for the Windows PassSync service is %s" % pass_dn
+ try:
+ conn.getEntry(pass_dn, ldap.SCOPE_BASE)
+ print "Windows PassSync entry exists, not resetting password"
+ return
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ pass
+
+ # The user doesn't exist, add it
+ entry = ipaldap.Entry(pass_dn)
+ entry.setValues("objectclass", ["account", "simplesecurityobject"])
+ entry.setValues("uid", "passsync")
+ entry.setValues("userPassword", password)
+ conn.add_s(entry)
+
+ # Add it to the list of users allowed to bypass password policy
+ extop_dn = "cn=ipa_pwd_extop,cn=plugins,cn=config"
+ entry = conn.getEntry(extop_dn, ldap.SCOPE_BASE)
+ pass_mgrs = entry.getValues('passSyncManagersDNs')
+ if not pass_mgrs:
+ pass_mgrs = []
+ if not isinstance(pass_mgrs, list):
+ pass_mgrs = [pass_mgrs]
+ pass_mgrs.append(pass_dn)
+ mod = [(ldap.MOD_REPLACE, 'passSyncManagersDNs', pass_mgrs)]
+ conn.modify_s(extop_dn, mod)
+
+ # And finally grant it permission to write passwords
+ mod = [(ldap.MOD_ADD, 'aci',
+ ['(targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Windows PassSync service can write passwords"; allow (write) userdn="ldap:///%s";)' % pass_dn])]
+ try:
+ conn.modify_s(self.suffix, mod)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ logging.debug("passsync aci already exists in suffix %s on %s" % (self.suffix, conn.host))
+
+ def setup_winsync_agmt(self, entry, **kargs):
+ entry.setValues("objectclass", "nsDSWindowsReplicationAgreement")
+ entry.setValues("nsds7WindowsReplicaSubtree",
+ kargs.get("win_subtree",
+ WIN_USER_CONTAINER + "," + self.suffix))
+ entry.setValues("nsds7DirectoryReplicaSubtree",
+ kargs.get("ds_subtree",
+ IPA_USER_CONTAINER + "," + self.suffix))
+ # for now, just sync users and ignore groups
+ entry.setValues("nsds7NewWinUserSyncEnabled", kargs.get('newwinusers', 'true'))
+ entry.setValues("nsds7NewWinGroupSyncEnabled", kargs.get('newwingroups', 'false'))
+ windomain = ''
+ if kargs.has_key('windomain'):
+ windomain = kargs['windomain']
+ else:
+ windomain = '.'.join(ldap.explode_dn(self.suffix, 1))
+ entry.setValues("nsds7WindowsDomain", windomain)
+
+ def agreement_dn(self, hostname, port=PORT):
+ cn = "meTo%s%d" % (hostname, port)
+ dn = "cn=%s, %s" % (cn, self.replica_dn())
+
+ return (cn, dn)
+
+ def setup_agreement(self, a, b, **kargs):
+ cn, dn = self.agreement_dn(b.host)
+ try:
+ a.getEntry(dn, ldap.SCOPE_BASE)
+ return
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ pass
+
+ iswinsync = kargs.get("winsync", False)
+ repl_man_dn = kargs.get("binddn", self.repl_man_dn)
+ repl_man_passwd = kargs.get("bindpw", self.repl_man_passwd)
+ port = kargs.get("port", PORT)
+
+ entry = ipaldap.Entry(dn)
+ entry.setValues('objectclass', "nsds5replicationagreement")
+ entry.setValues('cn', cn)
+ entry.setValues('nsds5replicahost', b.host)
+ entry.setValues('nsds5replicaport', str(port))
+ entry.setValues('nsds5replicatimeout', str(TIMEOUT))
+ entry.setValues('nsds5replicabinddn', repl_man_dn)
+ entry.setValues('nsds5replicacredentials', repl_man_passwd)
+ entry.setValues('nsds5replicabindmethod', 'simple')
+ entry.setValues('nsds5replicaroot', self.suffix)
+ entry.setValues('nsds5replicaupdateschedule', '0000-2359 0123456')
+ entry.setValues('nsds5replicatransportinfo', 'SSL')
+ entry.setValues('nsDS5ReplicatedAttributeList', '(objectclass=*) $ EXCLUDE memberOf')
+ entry.setValues('description', "me to %s%d" % (b.host, port))
+ if iswinsync:
+ self.setup_winsync_agmt(entry, **kargs)
+
+ a.add_s(entry)
+
+ entry = a.waitForEntry(entry)
+
+ def delete_agreement(self, hostname):
+ cn, dn = self.agreement_dn(hostname)
+ return self.conn.deleteEntry(dn)
+
+ def check_repl_init(self, conn, agmtdn):
+ done = False
+ hasError = 0
+ attrlist = ['cn', 'nsds5BeginReplicaRefresh', 'nsds5replicaUpdateInProgress',
+ 'nsds5ReplicaLastInitStatus', 'nsds5ReplicaLastInitStart',
+ 'nsds5ReplicaLastInitEnd']
+ entry = conn.getEntry(agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
+ if not entry:
+ print "Error reading status from agreement", agmtdn
+ hasError = 1
+ else:
+ refresh = entry.nsds5BeginReplicaRefresh
+ inprogress = entry.nsds5replicaUpdateInProgress
+ status = entry.nsds5ReplicaLastInitStatus
+ if not refresh: # done - check status
+ if not status:
+ print "No status yet"
+ elif status.find("replica busy") > -1:
+ print "[%s] reports: Replica Busy! Status: [%s]" % (conn.host, status)
+ done = True
+ hasError = 2
+ elif status.find("Total update succeeded") > -1:
+ print "Update succeeded"
+ done = True
+ elif inprogress.lower() == 'true':
+ print "Update in progress yet not in progress"
+ else:
+ print "[%s] reports: Update failed! Status: [%s]" % (conn.host, status)
+ hasError = 1
+ done = True
+ else:
+ print "Update in progress"
+
+ return done, hasError
+
+ def check_repl_update(self, conn, agmtdn):
+ done = False
+ hasError = 0
+ attrlist = ['cn', 'nsds5replicaUpdateInProgress',
+ 'nsds5ReplicaLastUpdateStatus', 'nsds5ReplicaLastUpdateStart',
+ 'nsds5ReplicaLastUpdateEnd']
+ entry = conn.getEntry(agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
+ if not entry:
+ print "Error reading status from agreement", agmtdn
+ hasError = 1
+ else:
+ inprogress = entry.nsds5replicaUpdateInProgress
+ status = entry.nsds5ReplicaLastUpdateStatus
+ start = entry.nsds5ReplicaLastUpdateStart
+ end = entry.nsds5ReplicaLastUpdateEnd
+ # incremental update is done if inprogress is false and end >= start
+ done = inprogress and inprogress.lower() == 'false' and start and end and (start <= end)
+ logging.info("Replication Update in progress: %s: status: %s: start: %s: end: %s" %
+ (inprogress, status, start, end))
+ if not done and status: # check for errors
+ # status will usually be a number followed by a string
+ # number != 0 means error
+ rc, msg = status.split(' ', 1)
+ if rc != '0':
+ hasError = 1
+ done = True
+
+ return done, hasError
+
+ def wait_for_repl_init(self, conn, agmtdn):
+ done = False
+ haserror = 0
+ 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)
+ return haserror
+
+ def wait_for_repl_update(self, conn, agmtdn, maxtries=600):
+ done = False
+ haserror = 0
+ while not done and not haserror and maxtries > 0:
+ time.sleep(1) # give it a few seconds to get going
+ done, haserror = self.check_repl_update(conn, agmtdn)
+ maxtries -= 1
+ if maxtries == 0: # too many tries
+ print "Error: timeout: could not determine agreement status: please check your directory server logs for possible errors"
+ haserror = 1
+ return haserror
+
+ def start_replication(self, other_conn, conn=None):
+ print "Starting replication, please wait until this has completed."
+ if conn == None:
+ conn = self.conn
+ cn, dn = self.agreement_dn(conn.host)
+
+ mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')]
+ other_conn.modify_s(dn, mod)
+
+ return self.wait_for_repl_init(other_conn, dn)
+
+ def basic_replication_setup(self, conn, replica_id):
+ self.add_replication_manager(conn)
+ self.local_replica_config(conn, replica_id)
+ self.setup_changelog(conn)
+
+ def setup_replication(self, other_hostname, realm_name, **kargs):
+ """
+ NOTES:
+ - the directory manager password needs to be the same on
+ both directories. Or use the optional binddn and bindpw
+ """
+ iswinsync = kargs.get("winsync", False)
+ oth_port = kargs.get("port", PORT)
+ oth_cacert = kargs.get("cacert", CACERT)
+ oth_binddn = kargs.get("binddn", DIRMAN_CN)
+ oth_bindpw = kargs.get("bindpw", self.dirman_passwd)
+ # note - there appears to be a bug in python-ldap - it does not
+ # allow connections using two different CA certs
+ other_conn = ipaldap.IPAdmin(other_hostname, port=oth_port, cacert=oth_cacert)
+ try:
+ other_conn.do_simple_bind(binddn=oth_binddn, bindpw=oth_bindpw)
+ except Exception, e:
+ if iswinsync:
+ logging.info("Could not validate connection to remote server %s:%d - continuing" %
+ (other_hostname, oth_port))
+ logging.info("The error was: %s" % e)
+ else:
+ raise e
+
+ self.suffix = ipaldap.IPAdmin.normalizeDN(dsinstance.realm_to_suffix(realm_name))
+
+ if not iswinsync:
+ local_id = self._get_replica_id(self.conn, other_conn)
+ else:
+ # there is no other side to get a replica ID from
+ local_id = self._get_replica_id(self.conn, self.conn)
+ self.basic_replication_setup(self.conn, local_id)
+
+ if not iswinsync:
+ other_id = self._get_replica_id(other_conn, other_conn)
+ self.basic_replication_setup(other_conn, other_id)
+ self.setup_agreement(other_conn, self.conn)
+ self.setup_agreement(self.conn, other_conn)
+ return self.start_replication(other_conn)
+ else:
+ self.add_passsync_user(self.conn, kargs.get("passsync"))
+ self.setup_agreement(self.conn, other_conn, **kargs)
+ logging.info("Added new sync agreement, waiting for it to become ready . . .")
+ cn, dn = self.agreement_dn(other_hostname)
+ self.wait_for_repl_update(self.conn, dn, 30)
+ logging.info("Agreement is ready, starting replication . . .")
+ return self.start_replication(self.conn, other_conn)
+
+ def initialize_replication(self, dn, conn):
+ mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')]
+ try:
+ conn.modify_s(dn, mod)
+ except ldap.ALREADY_EXISTS:
+ return
+
+ def force_synch(self, dn, schedule, conn):
+ newschedule = '2358-2359 0'
+
+ # On the remote chance of a match. We force a synch to happen right
+ # now by changing the schedule to something else and quickly changing
+ # it back.
+ if newschedule == schedule:
+ newschedule = '2358-2359 1'
+ logging.info("Changing agreement %s schedule to %s to force synch" %
+ (dn, newschedule))
+ mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaUpdateSchedule', [ newschedule ])]
+ conn.modify_s(dn, mod)
+ time.sleep(1)
+ logging.info("Changing agreement %s to restore original schedule %s" %
+ (dn, schedule))
+ mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaUpdateSchedule', [ schedule ])]
+ conn.modify_s(dn, mod)
+
+ def get_agreement_type(self, hostname):
+ cn, dn = self.agreement_dn(hostname)
+
+ entry = self.conn.getEntry(dn, ldap.SCOPE_BASE)
+
+ objectclass = entry.getValues("objectclass")
+
+ for o in objectclass:
+ if o.lower() == "nsdswindowsreplicationagreement":
+ return WINSYNC
+
+ return IPA_REPLICA
diff --git a/ipaserver/install/service.py b/ipaserver/install/service.py
new file mode 100644
index 000000000..b9f6c505d
--- /dev/null
+++ b/ipaserver/install/service.py
@@ -0,0 +1,169 @@
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import logging, sys
+from ipa import sysrestore
+from ipa import ipautil
+
+
+def stop(service_name):
+ ipautil.run(["/sbin/service", service_name, "stop"])
+
+def start(service_name):
+ ipautil.run(["/sbin/service", service_name, "start"])
+
+def restart(service_name):
+ ipautil.run(["/sbin/service", service_name, "restart"])
+
+def is_running(service_name):
+ ret = True
+ try:
+ ipautil.run(["/sbin/service", service_name, "status"])
+ except ipautil.CalledProcessError:
+ ret = False
+ return ret
+
+def chkconfig_on(service_name):
+ ipautil.run(["/sbin/chkconfig", service_name, "on"])
+
+def chkconfig_off(service_name):
+ ipautil.run(["/sbin/chkconfig", service_name, "off"])
+
+def chkconfig_add(service_name):
+ ipautil.run(["/sbin/chkconfig", "--add", service_name])
+
+def chkconfig_del(service_name):
+ ipautil.run(["/sbin/chkconfig", "--del", service_name])
+
+def is_enabled(service_name):
+ (stdout, stderr) = ipautil.run(["/sbin/chkconfig", "--list", service_name])
+
+ runlevels = {}
+ for runlevel in range(0, 7):
+ runlevels[runlevel] = False
+
+ for line in stdout.split("\n"):
+ parts = line.split()
+ if parts[0] == service_name:
+ for s in parts[1:]:
+ (runlevel, status) = s.split(":")[0:2]
+ try:
+ runlevels[int(runlevel)] = status == "on"
+ except ValueError:
+ pass
+ break
+
+ return (runlevels[3] and runlevels[4] and runlevels[5])
+
+def print_msg(message, output_fd=sys.stdout):
+ logging.debug(message)
+ output_fd.write(message)
+ output_fd.write("\n")
+
+
+class Service:
+ def __init__(self, service_name, sstore=None):
+ self.service_name = service_name
+ self.steps = []
+ self.output_fd = sys.stdout
+
+ if sstore:
+ self.sstore = sstore
+ else:
+ self.sstore = sysrestore.StateFile('/var/lib/ipa/sysrestore')
+
+ def set_output(self, fd):
+ self.output_fd = fd
+
+ def stop(self):
+ stop(self.service_name)
+
+ def start(self):
+ start(self.service_name)
+
+ def restart(self):
+ restart(self.service_name)
+
+ def is_running(self):
+ return is_running(self.service_name)
+
+ def chkconfig_add(self):
+ chkconfig_add(self.service_name)
+
+ def chkconfig_del(self):
+ chkconfig_del(self.service_name)
+
+ def chkconfig_on(self):
+ chkconfig_on(self.service_name)
+
+ def chkconfig_off(self):
+ chkconfig_off(self.service_name)
+
+ def is_enabled(self):
+ return is_enabled(self.service_name)
+
+ def backup_state(self, key, value):
+ self.sstore.backup_state(self.service_name, key, value)
+
+ def restore_state(self, key):
+ return self.sstore.restore_state(self.service_name, key)
+
+ def print_msg(self, message):
+ print_msg(message, self.output_fd)
+
+ def step(self, message, method):
+ self.steps.append((message, method))
+
+ def start_creation(self, message):
+ self.print_msg(message)
+
+ step = 0
+ for (message, method) in self.steps:
+ self.print_msg(" [%d/%d]: %s" % (step+1, len(self.steps), message))
+ method()
+ step += 1
+
+ self.print_msg("done configuring %s." % self.service_name)
+
+ self.steps = []
+
+class SimpleServiceInstance(Service):
+ def create_instance(self):
+ self.step("starting %s " % self.service_name, self.__start)
+ self.step("configuring %s to start on boot" % self.service_name, self.__enable)
+ self.start_creation("Configuring %s" % self.service_name)
+
+ def __start(self):
+ self.backup_state("running", self.is_running())
+ self.restart()
+
+ def __enable(self):
+ self.chkconfig_add()
+ self.backup_state("enabled", self.is_enabled())
+ self.chkconfig_on()
+
+ def uninstall(self):
+ running = self.restore_state("running")
+ enabled = not self.restore_state("enabled")
+
+ if not running is None and not running:
+ self.stop()
+ if not enabled is None and not enabled:
+ self.chkconfig_off()
+ self.chkconfig_del()