diff options
Diffstat (limited to 'ipaserver/install')
-rw-r--r-- | ipaserver/install/Makefile.am | 24 | ||||
-rw-r--r-- | ipaserver/install/__init__.py | 21 | ||||
-rw-r--r-- | ipaserver/install/bindinstance.py | 156 | ||||
-rw-r--r-- | ipaserver/install/certs.py | 424 | ||||
-rw-r--r-- | ipaserver/install/dsinstance.py | 479 | ||||
-rw-r--r-- | ipaserver/install/httpinstance.py | 231 | ||||
-rw-r--r-- | ipaserver/install/installutils.py | 248 | ||||
-rw-r--r-- | ipaserver/install/ipaldap.py | 701 | ||||
-rw-r--r-- | ipaserver/install/krbinstance.py | 428 | ||||
-rwxr-xr-x | ipaserver/install/ldapupdate.py | 593 | ||||
-rw-r--r-- | ipaserver/install/ntpinstance.py | 107 | ||||
-rw-r--r-- | ipaserver/install/replication.py | 532 | ||||
-rw-r--r-- | ipaserver/install/service.py | 169 |
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() |