The attached patches fix following tickets:
    https://fedorahosted.org/freeipa/ticket/4949
    https://fedorahosted.org/freeipa/ticket/4048
    https://fedorahosted.org/freeipa/ticket/1930

With these patches, an administrator can specify LDIF file that contains modifications to be applied to dse.ldif right after creation of DS instance.
From 3d0b62913a2611004c52804ae9cf34d7f4b4b55a Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Mon, 5 Oct 2015 14:37:05 +0200
Subject: [PATCH 1/3] Make offline LDIF modify more robust

* move code to installutils
* add replace_value method
* use lists instead of single values for add_value, remove_value methods

https://fedorahosted.org/freeipa/ticket/4949

Also fixes:
https://fedorahosted.org/freeipa/ticket/4048
https://fedorahosted.org/freeipa/ticket/1930
---
 ipaserver/install/installutils.py    |  86 +++++++++++++++++++++++++++
 ipaserver/install/upgradeinstance.py | 112 ++++-------------------------------
 2 files changed, 97 insertions(+), 101 deletions(-)

diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index 58be9f23384f0c1734d1ba7a14182f60817a32a8..325856e29166fe25df5405cb3f010c3a4f2a0cc8 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -22,6 +22,7 @@ from __future__ import print_function
 
 import socket
 import getpass
+import ldif
 import os
 import re
 import fileinput
@@ -1107,3 +1108,88 @@ def enable_and_start_oddjobd(sstore):
         oddjobd.start()
     except Exception as e:
         root_logger.critical("Unable to start oddjobd: {0}".format(str(e)))
+
+
+class ModifyLDIF(ldif.LDIFParser):
+    """
+    Allows to modify LDIF file.
+
+    Operations keep the order in whihc were specified per DN.
+    Warning: only modifications of existing DNs are supported
+    """
+    def __init__(self, input_file, output_file):
+        """
+        :param input_file: an LDIF
+        :param output_file: an LDIF file
+        """
+        ldif.LDIFParser.__init__(self, input_file)
+        self.writer = ldif.LDIFWriter(output_file)
+
+        self.modifications = {}  # keep modify operations in original order
+
+    def add_value(self, dn, attr, values):
+        """
+        Add value to LDIF.
+        :param dn: DN of entry (must exists)
+        :param attr: attribute name
+        :param value: value to be added
+        """
+        assert isinstance(values, list)
+        self.modifications.setdefault(dn, []).append(
+            dict(
+                op="add",
+                attr=attr,
+                values=values,
+            )
+        )
+
+    def remove_value(self, dn, attr, values=None):
+        """
+        Remove value from LDIF.
+        :param dn: DN of entry
+        :param attr: attribute name
+        :param value: value to be removed, if value is None, attribute will
+        be removed
+        """
+        assert values is None or isinstance(values, list)
+        self.modifications.setdefault(dn, []).append(
+            dict(
+                op="del",
+                attr=attr,
+                values=values,
+            )
+        )
+
+    def replace_value(self, dn, attr, values):
+        """
+        Replace values in LDIF with new value.
+        :param dn: DN of entry
+        :param attr: attribute name
+        :param value: new value for atribute
+        """
+        assert isinstance(values, list)
+        self.remove_value(dn, attr)
+        self.add_value(dn, attr, values)
+
+    def handle(self, dn, entry):
+        for mod in self.modifications.get(dn, []):
+            attr_name = mod["attr"]
+            values = mod["values"]
+
+            if mod["op"] == "del":
+                # delete
+                attribute = entry.setdefault(attr_name, [])
+                if values is None:
+                    attribute = []
+                else:
+                    attribute = [v for v in attribute if v not in values]
+                if not attribute:  # empty
+                    del entry[attr_name]
+            elif mod["op"] == "add":
+                # add
+                attribute = entry.setdefault(attr_name, [])
+                attribute.extend([v for v in values if v not in attribute])
+            else:
+                assert False, "Unknown operation: %r" % mod["op"]
+
+        self.writer.unparse(dn, entry)
diff --git a/ipaserver/install/upgradeinstance.py b/ipaserver/install/upgradeinstance.py
index 684a3dd99e2215c86b92dcb7ba9d00ee9e17b8fb..602e6ec4930cd9d2b9e686a5ec2ed3de10cb082f 100644
--- a/ipaserver/install/upgradeinstance.py
+++ b/ipaserver/install/upgradeinstance.py
@@ -66,85 +66,6 @@ class GetEntryFromLDIF(ldif.LDIFParser):
         self.results[dn] = entry
 
 
-class ModifyLDIF(ldif.LDIFParser):
-    """
-    Allows to modify LDIF file.
-
-    Remove operations are executed before add operations
-    """
-    def __init__(self, input_file, writer):
-        """
-        :param input_file: an LDIF
-        :param writer: ldif.LDIFWriter instance where modified LDIF will
-        be written
-        """
-        ldif.LDIFParser.__init__(self, input_file)
-        self.writer = writer
-
-        self.add_dict = {}
-        self.remove_dict = {}
-
-    def add_value(self, dn, attr, value):
-        """
-        Add value to LDIF.
-        :param dn: DN of entry (must exists)
-        :param attr: attribute name
-        :param value: value to be added
-        """
-        attr = attr.lower()
-        entry = self.add_dict.setdefault(dn, {})
-        attribute = entry.setdefault(attr, [])
-        if value not in attribute:
-            attribute.append(value)
-
-    def remove_value(self, dn, attr, value=None):
-        """
-        Remove value from LDIF.
-        :param dn: DN of entry
-        :param attr: attribute name
-        :param value: value to be removed, if value is None, attribute will
-        be removed
-        """
-        attr = attr.lower()
-        entry = self.remove_dict.setdefault(dn, {})
-
-        if entry is None:
-            return
-        attribute = entry.setdefault(attr, [])
-        if value is None:
-            # remove all values
-            entry[attr] = None
-            return
-        elif attribute is None:
-            # already marked to remove all values
-            return
-        if value not in attribute:
-            attribute.append(value)
-
-    def handle(self, dn, entry):
-        if dn in self.remove_dict:
-            for name, value in self.remove_dict[dn].items():
-                if value is None:
-                    attribute = []
-                else:
-                    attribute = entry.setdefault(name, [])
-                    attribute = [v for v in attribute if v not in value]
-                entry[name] = attribute
-
-                if not attribute:  # empty
-                    del entry[name]
-
-        if dn in self.add_dict:
-            for name, value in self.add_dict[dn].items():
-                attribute = entry.setdefault(name, [])
-                attribute.extend([v for v in value if v not in attribute])
-
-        if not entry:  # empty
-            return
-
-        self.writer.unparse(dn, entry)
-
-
 class IPAUpgrade(service.Service):
     """
     Update the LDAP data in an instance by turning off all network
@@ -235,13 +156,11 @@ class IPAUpgrade(service.Service):
     def __enable_ds_global_write_lock(self):
         ldif_outfile = "%s.modified.out" % self.filename
         with open(ldif_outfile, "wb") as out_file:
-            ldif_writer = ldif.LDIFWriter(out_file)
             with open(self.filename, "rb") as in_file:
-                parser = ModifyLDIF(in_file, ldif_writer)
+                parser = installutils.ModifyLDIF(in_file, out_file)
 
-                parser.remove_value("cn=config", "nsslapd-global-backend-lock")
-                parser.add_value("cn=config", "nsslapd-global-backend-lock",
-                                 "on")
+                parser.replace_value(
+                    "cn=config", "nsslapd-global-backend-lock", ["on"])
                 parser.parse()
 
         shutil.copy2(ldif_outfile, self.filename)
@@ -253,22 +172,20 @@ class IPAUpgrade(service.Service):
 
         ldif_outfile = "%s.modified.out" % self.filename
         with open(ldif_outfile, "wb") as out_file:
-            ldif_writer = ldif.LDIFWriter(out_file)
             with open(self.filename, "rb") as in_file:
-                parser = ModifyLDIF(in_file, ldif_writer)
+                parser = installutils.ModifyLDIF(in_file, out_file)
 
                 if port is not None:
-                    parser.remove_value("cn=config", "nsslapd-port")
-                    parser.add_value("cn=config", "nsslapd-port", port)
+                    parser.replace_value("cn=config", "nsslapd-port", [port])
                 if security is not None:
-                    parser.remove_value("cn=config", "nsslapd-security")
-                    parser.add_value("cn=config", "nsslapd-security", security)
+                    parser.replace_value("cn=config", "nsslapd-security",
+                                         [security])
 
                 # disable global lock by default
                 parser.remove_value("cn=config", "nsslapd-global-backend-lock")
                 if global_lock is not None:
                     parser.add_value("cn=config", "nsslapd-global-backend-lock",
-                                     global_lock)
+                                     [global_lock])
 
                 parser.parse()
 
@@ -277,18 +194,11 @@ class IPAUpgrade(service.Service):
     def __disable_listeners(self):
         ldif_outfile = "%s.modified.out" % self.filename
         with open(ldif_outfile, "wb") as out_file:
-            ldif_writer = ldif.LDIFWriter(out_file)
             with open(self.filename, "rb") as in_file:
-                parser = ModifyLDIF(in_file, ldif_writer)
-
-                parser.remove_value("cn=config", "nsslapd-port")
-                parser.add_value("cn=config", "nsslapd-port", "0")
-
-                parser.remove_value("cn=config", "nsslapd-security")
-                parser.add_value("cn=config", "nsslapd-security", "off")
-
+                parser = installutils.ModifyLDIF(in_file, out_file)
+                parser.replace_value("cn=config", "nsslapd-port", ["0"])
+                parser.replace_value("cn=config", "nsslapd-security", ["off"])
                 parser.remove_value("cn=config", "nsslapd-ldapientrysearchbase")
-
                 parser.parse()
 
         shutil.copy2(ldif_outfile, self.filename)
-- 
2.4.3

From 2b158f19bf7cd8726096a509ee283c82783e38a7 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Wed, 7 Oct 2015 17:15:34 +0200
Subject: [PATCH 2/3] Add method to read changes from LDIF

modifications_from_ldif will read LDIF file and changes in LDIF will
be cached until parse() is called. After calling parse() method changes
will be applied into destination LDIF.

Only changetype modify is supported, the default operation is add.

https://fedorahosted.org/freeipa/ticket/4949

Also fixes:
https://fedorahosted.org/freeipa/ticket/4048
https://fedorahosted.org/freeipa/ticket/1930
---
 ipaserver/install/installutils.py | 40 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index 325856e29166fe25df5405cb3f010c3a4f2a0cc8..c4cfef2042d29532283e9a2d75312853f06bd628 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -1171,6 +1171,46 @@ class ModifyLDIF(ldif.LDIFParser):
         self.remove_value(dn, attr)
         self.add_value(dn, attr, values)
 
+    def modifications_from_ldif(self, ldif_file):
+        """
+        Parse ldif file. Default operation is add, only changetypes "add"
+        and "modify" are supported.
+        :param ldif_file: an opened file for read
+        :raises: ValueError
+        """
+        parser = ldif.LDIFRecordList(ldif_file)
+        parser.parse()
+
+        last_dn = None
+        for dn, entry in parser.all_records:
+            if dn is None:
+                # ldif parser return None, if record belong to previous DN
+                dn = last_dn
+            else:
+                last_dn = dn
+
+            if "replace" in entry:
+                for attr in entry["replace"]:
+                    try:
+                        self.replace_value(dn, attr, entry[attr])
+                    except KeyError:
+                        raise ValueError("replace: {dn}, {attr}: values are "
+                                         "missing".format(dn=dn, attr=attr))
+            elif "delete" in entry:
+                for attr in entry["delete"]:
+                    self.remove_value(dn, attr, entry.get(attr, None))
+            else:
+                if "add" in entry:
+                    for attr in entry["add"]:
+                        try:
+                            self.replace_value(dn, attr, entry[attr])
+                        except KeyError:
+                            raise ValueError("add: {dn}, {attr}: values are "
+                                             "missing".format(dn=dn, attr=attr))
+                else:
+                    for attr, values in six.iteritems(entry):
+                        self.add_value(dn, attr, values)
+
     def handle(self, dn, entry):
         for mod in self.modifications.get(dn, []):
             attr_name = mod["attr"]
-- 
2.4.3

From 0a3568316cd138a320e7213179f9cbcf7a0ae613 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Thu, 8 Oct 2015 10:38:44 +0200
Subject: [PATCH 3/3] Add option to specify LDIF file that contains DS
 configuration chages

This allows to user modify configuration changes of the directory server
instance during installation of DS

https://fedorahosted.org/freeipa/ticket/4949

Also fixes:
https://fedorahosted.org/freeipa/ticket/4048
https://fedorahosted.org/freeipa/ticket/1930
---
 ipaserver/install/dsinstance.py            | 43 ++++++++++++++++++++++++------
 ipaserver/install/server/common.py         | 13 +++++++++
 ipaserver/install/server/install.py        |  6 +++--
 ipaserver/install/server/replicainstall.py |  7 ++---
 4 files changed, 56 insertions(+), 13 deletions(-)

diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index 312188273bbc2ddd6a0d4ff4e776cc6ad08a6f5e..fbd771024da75d6faf59ff9680e0e2aedf672ed6 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -192,7 +192,7 @@ info: IPA V2.0
 
 class DsInstance(service.Service):
     def __init__(self, realm_name=None, domain_name=None, dm_password=None,
-                 fstore=None, domainlevel=None):
+                 fstore=None, domainlevel=None, config_ldif=None):
         service.Service.__init__(self, "dirsrv",
             service_desc="directory server",
             dm_password=dm_password,
@@ -215,6 +215,7 @@ class DsInstance(service.Service):
         self.subject_base = None
         self.open_ports = []
         self.run_init_memberof = True
+        self.config_ldif = config_ldif  # updates for dse.ldif
         self.domainlevel = domainlevel
         if realm_name:
             self.suffix = ipautil.realm_to_suffix(self.realm)
@@ -234,6 +235,9 @@ class DsInstance(service.Service):
 
         self.step("creating directory server user", create_ds_user)
         self.step("creating directory server instance", self.__create_instance)
+        if self.config_ldif:
+            self.step("updating configuration in dse.ldif", self.__update_dse_ldif)
+        self.step("restarting directory server", self.__restart_instance)
         self.step("adding default schema", self.__add_default_schemas)
         self.step("enabling memberof plugin", self.__add_memberof_module)
         self.step("enabling winsync plugin", self.__add_winsync_module)
@@ -503,16 +507,39 @@ class DsInstance(service.Service):
         # check for open port 389 from now on
         self.open_ports.append(389)
 
-        root_logger.debug("restarting ds instance")
-        try:
-            self.__restart_instance()
-            root_logger.debug("done restarting ds instance")
-        except ipautil.CalledProcessError as e:
-            print("failed to restart ds instance", e)
-            root_logger.debug("failed to restart ds instance %s" % e)
         inf_fd.close()
         os.remove(paths.DIRSRV_BOOT_LDIF)
 
+    def __update_dse_ldif(self):
+        """
+        This method updates dse.ldif right after instance creation. This is
+        supposed to allow admin modify configuration of the DS which has to be
+        done before IPA is fully installed (for example: settings for
+        replication on replicas)
+        DS must be turned off.
+        """
+        self.stop()
+
+        dse_filename = '%s/%s' % (
+            paths.ETC_DIRSRV_SLAPD_INSTANCE_TEMPLATE % self.serverid,
+            'dse.ldif',
+        )
+
+        with tempfile.NamedTemporaryFile(delete=False) as new_dse_ldif:
+            temp_filename = new_dse_ldif.name
+            with open(dse_filename, "r") as input_file:
+                parser = installutils.ModifyLDIF(input_file, new_dse_ldif)
+                # parse modification from config ldif
+                with open(self.config_ldif, "r") as config_ldif:
+                    parser.modifications_from_ldif(config_ldif)
+                parser.parse()
+            new_dse_ldif.flush()
+        shutil.copy2(temp_filename, dse_filename)
+        try:
+            os.remove(temp_filename)
+        except OSError as e:
+            root_logger.debug("Failed to clean temporary file: %s" % e)
+
     def __add_default_schemas(self):
         pent = pwd.getpwnam(DS_USER)
         for schema_fname in IPA_SCHEMA_FILES:
diff --git a/ipaserver/install/server/common.py b/ipaserver/install/server/common.py
index 3eb7279d200ffd6ab33d8d914c8d4f13e567a171..df5664d6f14ee6da36dd11c9a8e464f04c73ddee 100644
--- a/ipaserver/install/server/common.py
+++ b/ipaserver/install/server/common.py
@@ -343,6 +343,19 @@ class BaseServer(common.Installable, common.Interactive, core.Composite):
         description="Do not automatically create DNS SSHFP records",
     )
 
+    config_ldif_file = Knob(
+        str, None,
+        description="path to LDIF file that will be used to modify dse.ldif"
+                    "during installation of the directory server instance",
+        cli_metavar='FILE',
+    )
+
+    @config_ldif_file.validator
+    def config_ldif_file(self, value):
+        if not os.path.exists(value):
+            raise ValueError("File %s does not exist." % value)
+
+
     def __init__(self, **kwargs):
         super(BaseServer, self).__init__(**kwargs)
 
diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py
index 72f6e4d8d7476d73b2d92919ae929501f05f8ece..81abde5914dd0dade6f8d154da8789e0da93a5a2 100644
--- a/ipaserver/install/server/install.py
+++ b/ipaserver/install/server/install.py
@@ -734,7 +734,8 @@ def install(installer):
 
         if options.dirsrv_cert_files:
             ds = dsinstance.DsInstance(fstore=fstore,
-                                       domainlevel=options.domainlevel)
+                                       domainlevel=options.domainlevel,
+                                       config_ldif=options.config_ldif_file)
             installer._ds = ds
             ds.create_instance(realm_name, host_name, domain_name,
                                dm_password, dirsrv_pkcs12_info,
@@ -743,7 +744,8 @@ def install(installer):
                                hbac_allow=not options.no_hbac_allow)
         else:
             ds = dsinstance.DsInstance(fstore=fstore,
-                                       domainlevel=options.domainlevel)
+                                       domainlevel=options.domainlevel,
+                                       config_ldif=options.config_ldif_file)
             installer._ds = ds
             ds.create_instance(realm_name, host_name, domain_name,
                                dm_password,
diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py
index 3087091e4bebdee6f3aec560acb7916c40615d1e..3218cd4ca972e1e3575d41145759e2c6d1bc80cd 100644
--- a/ipaserver/install/server/replicainstall.py
+++ b/ipaserver/install/server/replicainstall.py
@@ -60,7 +60,7 @@ def make_pkcs12_info(directory, cert_name, password_name):
         return None
 
 
-def install_replica_ds(config):
+def install_replica_ds(config, options):
     dsinstance.check_ports()
 
     # if we have a pkcs12 file, create the cert db from
@@ -68,7 +68,8 @@ def install_replica_ds(config):
     # cert
     pkcs12_info = make_pkcs12_info(config.dir, "dscert.p12", "dirsrv_pin.txt")
 
-    ds = dsinstance.DsInstance()
+    ds = dsinstance.DsInstance(
+        config_ldif=options.config_ldif_file)
     ds.create_replica(
         realm_name=config.realm_name,
         master_fqdn=config.master_host_name,
@@ -562,7 +563,7 @@ def install(installer):
             ntp.create_instance()
 
         # Configure dirsrv
-        ds = install_replica_ds(config)
+        ds = install_replica_ds(config, options)
 
         # Always try to install DNS records
         install_dns_records(config, options, remote_api)
-- 
2.4.3

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to