544061
From abea98a9b918c0771ad10b314238b32c570f0372 Mon Sep 17 00:00:00 2001
544061
From: François Cami <fcami@redhat.com>
544061
Date: Aug 29 2019 06:45:12 +0000
544061
Subject: ipatests: check that ipa-client-automount restores nsswitch.conf at uninstall time
544061
544061
544061
Check that using ipa-client-install, ipa-client-automount --no-ssd, then uninstalling
544061
both properly restores nsswitch.conf sequentially.
544061
544061
Related-to:: https://pagure.io/freeipa/issue/8038
544061
Signed-off-by: François Cami <fcami@redhat.com>
544061
Reviewed-By: Francois Cami <fcami@redhat.com>
544061
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
544061
Reviewed-By: Rob Critenden <rcritten@redhat.com>
544061
Reviewed-By: François Cami <fcami@redhat.com>
544061
544061
---
544061
544061
#diff --git a/ipatests/prci_definitions/nightly_ipa-4-8.yaml b/ipatests/prci_definitions/nightly_ipa-4-8.yaml
544061
#index ef5d2c6..f39e4b4 100644
544061
#--- a/ipatests/prci_definitions/nightly_ipa-4-8.yaml
544061
#+++ b/ipatests/prci_definitions/nightly_ipa-4-8.yaml
544061
#@@ -1257,6 +1257,18 @@ jobs:
544061
#         timeout: 9000
544061
#         topology: *master_3client
544061
# 
544061
#+  fedora-30/nfs_nsswitch_restore:
544061
#+    requires: [fedora-30/build]
544061
#+    priority: 50
544061
#+    job:
544061
#+      class: RunPytest
544061
#+      args:
544061
#+        build_url: '{fedora-30/build_url}'
544061
#+        test_suite: test_integration/test_nfs.py::TestIpaClientAutomountFileRestore
544061
#+        template: *ci-master-f30
544061
#+        timeout: 3600
544061
#+        topology: *master_3client
544061
#+
544061
#   fedora-30/mask:
544061
#     requires: [fedora-30/build]
544061
#     priority: 50
544061
diff --git a/ipatests/test_integration/test_nfs.py b/ipatests/test_integration/test_nfs.py
544061
index adfc19f..0e1ef6a 100644
544061
--- a/ipatests/test_integration/test_nfs.py
544061
+++ b/ipatests/test_integration/test_nfs.py
544061
@@ -15,6 +15,7 @@
544061
 
544061
 from __future__ import absolute_import
544061
 
544061
+import pytest
544061
 import os
544061
 import re
544061
 import time
544061
@@ -258,3 +259,74 @@ class TestNFS(IntegrationTest):
544061
         time.sleep(WAIT_AFTER_UNINSTALL)
544061
 
544061
         self.cleanup()
544061
+
544061
+
544061
+class TestIpaClientAutomountFileRestore(IntegrationTest):
544061
+
544061
+    num_clients = 1
544061
+    topology = 'line'
544061
+
544061
+    @classmethod
544061
+    def install(cls, mh):
544061
+        tasks.install_master(cls.master, setup_dns=True)
544061
+
544061
+    def teardown_method(self, method):
544061
+        tasks.uninstall_client(self.clients[0])
544061
+
544061
+    def nsswitch_backup_restore(
544061
+        self,
544061
+        no_sssd=False,
544061
+    ):
544061
+
544061
+        # In order to get a more pure sum, one that ignores the Generated
544061
+        # header and any white space we have to do a bit of work...
544061
+        sha256nsswitch_cmd = \
544061
+            'egrep -v "Generated|^$" /etc/nsswitch.conf | sed "s/\\s//g" ' \
544061
+            '| sort | sha256sum'
544061
+
544061
+        cmd = self.clients[0].run_command(sha256nsswitch_cmd)
544061
+        orig_sha256 = cmd.stdout_text
544061
+
544061
+        grep_automount_command = \
544061
+            "grep automount /etc/nsswitch.conf | cut -d: -f2"
544061
+
544061
+        tasks.install_client(self.master, self.clients[0])
544061
+        cmd = self.clients[0].run_command(grep_automount_command)
544061
+        after_ipa_client_install = cmd.stdout_text.split()
544061
+
544061
+        if no_sssd:
544061
+            ipa_client_automount_command = [
544061
+                "ipa-client-automount", "--no-sssd", "-U"
544061
+            ]
544061
+        else:
544061
+            ipa_client_automount_command = [
544061
+                "ipa-client-automount", "-U"
544061
+            ]
544061
+        self.clients[0].run_command(ipa_client_automount_command)
544061
+        cmd = self.clients[0].run_command(grep_automount_command)
544061
+        after_ipa_client_automount = cmd.stdout_text.split()
544061
+        if no_sssd:
544061
+            assert after_ipa_client_automount == ['files', 'ldap']
544061
+        else:
544061
+            assert after_ipa_client_automount == ['sss', 'files']
544061
+
544061
+        cmd = self.clients[0].run_command(grep_automount_command)
544061
+        assert cmd.stdout_text.split() == after_ipa_client_automount
544061
+
544061
+        self.clients[0].run_command([
544061
+            "ipa-client-automount", "--uninstall", "-U"
544061
+        ])
544061
+
544061
+        cmd = self.clients[0].run_command(grep_automount_command)
544061
+        assert cmd.stdout_text.split() == after_ipa_client_install
544061
+
544061
+        tasks.uninstall_client(self.clients[0])
544061
+        cmd = self.clients[0].run_command(sha256nsswitch_cmd)
544061
+        assert cmd.stdout_text == orig_sha256
544061
+
544061
+    @pytest.mark.xfail(reason='freeipa ticket 8054', strict=True)
544061
+    def test_nsswitch_backup_restore_sssd(self):
544061
+        self.nsswitch_backup_restore()
544061
+
544061
+    def test_nsswitch_backup_restore_no_sssd(self):
544061
+        self.nsswitch_backup_restore(no_sssd=True)
544061
544061
From 2f0afeda6e66fcca5c184a4036112fcd315f2f6e Mon Sep 17 00:00:00 2001
544061
From: François Cami <fcami@redhat.com>
544061
Date: Aug 29 2019 06:45:12 +0000
544061
Subject: ipa-client-automount: always restore nsswitch.conf at uninstall time
544061
544061
544061
ipa-client-automount used to only restore nsswitch.conf when sssd was not
544061
used. However authselect's default profile is now sssd so always restore
544061
nsswitch.conf's automount configuration to 'files sssd'.
544061
Note that the behavior seen before commit:
544061
a0e846f56c8de3b549d1d284087131da13135e34
544061
would always restore nsswitch.conf to the previous state which in some cases
544061
was wrong.
544061
544061
Fixes: https://pagure.io/freeipa/issue/8038
544061
Signed-off-by: François Cami <fcami@redhat.com>
544061
Reviewed-By: Francois Cami <fcami@redhat.com>
544061
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
544061
Reviewed-By: Rob Critenden <rcritten@redhat.com>
544061
Reviewed-By: François Cami <fcami@redhat.com>
544061
544061
---
544061
544061
diff --git a/ipaclient/install/ipa_client_automount.py b/ipaclient/install/ipa_client_automount.py
544061
index fa07598..a1dc2a1 100644
544061
--- a/ipaclient/install/ipa_client_automount.py
544061
+++ b/ipaclient/install/ipa_client_automount.py
544061
@@ -177,18 +177,30 @@ def configure_xml(fstore):
544061
         print("Configured %s" % authconf)
544061
 
544061
 
544061
-def configure_nsswitch(fstore, options):
544061
+def configure_nsswitch(statestore, options):
544061
     """
544061
-    Point automount to ldap in nsswitch.conf. This function is for non-SSSD
544061
-    setups only
544061
+    Point automount to ldap in nsswitch.conf.
544061
+    This function is for non-SSSD setups only.
544061
     """
544061
-    fstore.backup_file(paths.NSSWITCH_CONF)
544061
-
544061
     conf = ipachangeconf.IPAChangeConf("IPA Installer")
544061
     conf.setOptionAssignment(':')
544061
 
544061
-    nss_value = ' files ldap'
544061
+    with open(paths.NSSWITCH_CONF, 'r') as f:
544061
+        current_opts = conf.parse(f)
544061
+        current_nss_value = conf.findOpts(
544061
+            current_opts, name='automount', type='option'
544061
+        )[1]
544061
+        if current_nss_value is None:
544061
+            # no automount database present
544061
+            current_nss_value = False  # None cannot be backed up
544061
+        else:
544061
+            current_nss_value = current_nss_value['value']
544061
+        statestore.backup_state(
544061
+            'ipa-client-automount-nsswitch', 'previous-automount',
544061
+            current_nss_value
544061
+        )
544061
 
544061
+    nss_value = ' files ldap'
544061
     opts = [
544061
         {
544061
             'name': 'automount',
544061
@@ -198,7 +210,6 @@ def configure_nsswitch(fstore, options):
544061
         },
544061
         {'name': 'empty', 'type': 'empty'},
544061
     ]
544061
-
544061
     conf.changeConf(paths.NSSWITCH_CONF, opts)
544061
 
544061
     print("Configured %s" % paths.NSSWITCH_CONF)
544061
@@ -322,19 +333,47 @@ def configure_autofs_common(fstore, statestore, options):
544061
 def uninstall(fstore, statestore):
544061
     RESTORE_FILES = [
544061
         paths.SYSCONFIG_AUTOFS,
544061
-        paths.NSSWITCH_CONF,
544061
         paths.AUTOFS_LDAP_AUTH_CONF,
544061
         paths.SYSCONFIG_NFS,
544061
         paths.IDMAPD_CONF,
544061
     ]
544061
     STATES = ['autofs', 'rpcidmapd', 'rpcgssd']
544061
 
544061
-    # automount only touches /etc/nsswitch.conf if LDAP is
544061
-    # used. Don't restore it otherwise.
544061
-    if statestore.get_state('authconfig', 'sssd') or (
544061
-        statestore.get_state('authselect', 'profile') == 'sssd'
544061
-    ):
544061
-        RESTORE_FILES.remove(paths.NSSWITCH_CONF)
544061
+    if statestore.get_state(
544061
+        'ipa-client-automount-nsswitch', 'previous-automount'
544061
+    ) is False:
544061
+        # Previous nsswitch.conf had no automount database configured
544061
+        # so remove it.
544061
+        conf = ipachangeconf.IPAChangeConf("IPA automount installer")
544061
+        conf.setOptionAssignment(':')
544061
+        changes = [conf.rmOption('automount')]
544061
+        conf.changeConf(paths.NSSWITCH_CONF, changes)
544061
+        tasks.restore_context(paths.NSSWITCH_CONF)
544061
+        statestore.delete_state(
544061
+            'ipa-client-automount-nsswitch', 'previous-automount'
544061
+        )
544061
+    elif statestore.get_state(
544061
+        'ipa-client-automount-nsswitch', 'previous-automount'
544061
+    ) is not None:
544061
+        nss_value = statestore.get_state(
544061
+            'ipa-client-automount-nsswitch', 'previous-automount'
544061
+        )
544061
+        opts = [
544061
+            {
544061
+                'name': 'automount',
544061
+                'type': 'option',
544061
+                'action': 'set',
544061
+                'value': nss_value,
544061
+            },
544061
+            {'name': 'empty', 'type': 'empty'},
544061
+        ]
544061
+        conf = ipachangeconf.IPAChangeConf("IPA automount installer")
544061
+        conf.setOptionAssignment(':')
544061
+        conf.changeConf(paths.NSSWITCH_CONF, opts)
544061
+        tasks.restore_context(paths.NSSWITCH_CONF)
544061
+        statestore.delete_state(
544061
+            'ipa-client-automount-nsswitch', 'previous-automount'
544061
+        )
544061
 
544061
     if not any(fstore.has_file(f) for f in RESTORE_FILES) or not any(
544061
         statestore.has_state(s) for s in STATES
544061
@@ -588,7 +627,7 @@ def configure_automount():
544061
 
544061
     try:
544061
         if not options.sssd:
544061
-            configure_nsswitch(fstore, options)
544061
+            configure_nsswitch(statestore, options)
544061
         configure_nfs(fstore, statestore, options)
544061
         if options.sssd:
544061
             configure_autofs_sssd(fstore, statestore, autodiscover, options)
544061
544061
From 6e92776bfc199e9ca92e11ef3315dcecad3c9307 Mon Sep 17 00:00:00 2001
544061
From: Rob Critenden <rcritten@redhat.com>
544061
Date: Aug 29 2019 06:45:12 +0000
544061
Subject: Move ipachangeconf from ipaclient.install to ipapython
544061
544061
544061
This will let us call it from ipaplatform.
544061
544061
Mark the original location as deprecated.
544061
544061
Reviewed-By: Francois Cami <fcami@redhat.com>
544061
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
544061
Reviewed-By: Rob Critenden <rcritten@redhat.com>
544061
Reviewed-By: François Cami <fcami@redhat.com>
544061
544061
---
544061
544061
diff --git a/install/tools/ipa-replica-conncheck.in b/install/tools/ipa-replica-conncheck.in
544061
index 9208076..b22db11 100644
544061
--- a/install/tools/ipa-replica-conncheck.in
544061
+++ b/install/tools/ipa-replica-conncheck.in
544061
@@ -22,7 +22,7 @@ from __future__ import print_function
544061
 
544061
 import logging
544061
 
544061
-import ipaclient.install.ipachangeconf
544061
+from ipapython import ipachangeconf
544061
 from ipapython.config import IPAOptionParser
544061
 from ipapython.dn import DN
544061
 from ipapython import version
544061
@@ -229,7 +229,7 @@ def sigterm_handler(signum, frame):
544061
 
544061
 def configure_krb5_conf(realm, kdc, filename):
544061
 
544061
-    krbconf = ipaclient.install.ipachangeconf.IPAChangeConf("IPA Installer")
544061
+    krbconf = ipachangeconf.IPAChangeConf("IPA Installer")
544061
     krbconf.setOptionAssignment((" = ", " "))
544061
     krbconf.setSectionNameDelimiters(("[","]"))
544061
     krbconf.setSubSectionDelimiters(("{","}"))
544061
diff --git a/ipaclient/install/ipachangeconf.py b/ipaclient/install/ipachangeconf.py
544061
index a13e0ea..c51e42e 100644
544061
--- a/ipaclient/install/ipachangeconf.py
544061
+++ b/ipaclient/install/ipachangeconf.py
544061
@@ -18,566 +18,18 @@
544061
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
544061
 #
544061
 
544061
-import fcntl
544061
-import logging
544061
-import os
544061
-import shutil
544061
+import warnings
544061
+from ipapython.ipachangeconf import IPAChangeConf as realIPAChangeConf
544061
 
544061
-import six
544061
 
544061
-if six.PY3:
544061
-    unicode = str
544061
+class IPAChangeConf(realIPAChangeConf):
544061
+    """Advertise the old name"""
544061
 
544061
-logger = logging.getLogger(__name__)
544061
-
544061
-def openLocked(filename, perms):
544061
-    fd = -1
544061
-    try:
544061
-        fd = os.open(filename, os.O_RDWR | os.O_CREAT, perms)
544061
-
544061
-        fcntl.lockf(fd, fcntl.LOCK_EX)
544061
-    except OSError as e:
544061
-        if fd != -1:
544061
-            try:
544061
-                os.close(fd)
544061
-            except OSError:
544061
-                pass
544061
-        raise IOError(e.errno, e.strerror)
544061
-    return os.fdopen(fd, "r+")
544061
-
544061
-
544061
-    #TODO: add subsection as a concept
544061
-    #      (ex. REALM.NAME = { foo = x bar = y } )
544061
-    #TODO: put section delimiters as separating element of the list
544061
-    #      so that we can process multiple sections in one go
544061
-    #TODO: add a comment all but provided options as a section option
544061
-class IPAChangeConf:
544061
     def __init__(self, name):
544061
-        self.progname = name
544061
-        self.indent = ("", "", "")
544061
-        self.assign = (" = ", "=")
544061
-        self.dassign = self.assign[0]
544061
-        self.comment = ("#",)
544061
-        self.dcomment = self.comment[0]
544061
-        self.eol = ("\n",)
544061
-        self.deol = self.eol[0]
544061
-        self.sectnamdel = ("[", "]")
544061
-        self.subsectdel = ("{", "}")
544061
-        self.case_insensitive_sections = True
544061
-
544061
-    def setProgName(self, name):
544061
-        self.progname = name
544061
-
544061
-    def setIndent(self, indent):
544061
-        if type(indent) is tuple:
544061
-            self.indent = indent
544061
-        elif type(indent) is str:
544061
-            self.indent = (indent, )
544061
-        else:
544061
-            raise ValueError('Indent must be a list of strings')
544061
-
544061
-    def setOptionAssignment(self, assign):
544061
-        if type(assign) is tuple:
544061
-            self.assign = assign
544061
-        else:
544061
-            self.assign = (assign, )
544061
-        self.dassign = self.assign[0]
544061
-
544061
-    def setCommentPrefix(self, comment):
544061
-        if type(comment) is tuple:
544061
-            self.comment = comment
544061
-        else:
544061
-            self.comment = (comment, )
544061
-        self.dcomment = self.comment[0]
544061
-
544061
-    def setEndLine(self, eol):
544061
-        if type(eol) is tuple:
544061
-            self.eol = eol
544061
-        else:
544061
-            self.eol = (eol, )
544061
-        self.deol = self.eol[0]
544061
-
544061
-    def setSectionNameDelimiters(self, delims):
544061
-        self.sectnamdel = delims
544061
-
544061
-    def setSubSectionDelimiters(self, delims):
544061
-        self.subsectdel = delims
544061
-
544061
-    def matchComment(self, line):
544061
-        for v in self.comment:
544061
-            if line.lstrip().startswith(v):
544061
-                return line.lstrip()[len(v):]
544061
-        return False
544061
-
544061
-    def matchEmpty(self, line):
544061
-        if line.strip() == "":
544061
-            return True
544061
-        return False
544061
-
544061
-    def matchSection(self, line):
544061
-        cl = "".join(line.strip().split())
544061
-        cl = cl.lower() if self.case_insensitive_sections else cl
544061
-
544061
-        if len(self.sectnamdel) != 2:
544061
-            return False
544061
-        if not cl.startswith(self.sectnamdel[0]):
544061
-            return False
544061
-        if not cl.endswith(self.sectnamdel[1]):
544061
-            return False
544061
-        return cl[len(self.sectnamdel[0]):-len(self.sectnamdel[1])]
544061
-
544061
-    def matchSubSection(self, line):
544061
-        if self.matchComment(line):
544061
-            return False
544061
-
544061
-        parts = line.split(self.dassign, 1)
544061
-        if len(parts) < 2:
544061
-            return False
544061
-
544061
-        if parts[1].strip() == self.subsectdel[0]:
544061
-            return parts[0].strip()
544061
-
544061
-        return False
544061
-
544061
-    def matchSubSectionEnd(self, line):
544061
-        if self.matchComment(line):
544061
-            return False
544061
-
544061
-        if line.strip() == self.subsectdel[1]:
544061
-            return True
544061
-
544061
-        return False
544061
-
544061
-    def getSectionLine(self, section):
544061
-        if len(self.sectnamdel) != 2:
544061
-            return section
544061
-        return self._dump_line(self.sectnamdel[0],
544061
-                               section,
544061
-                               self.sectnamdel[1],
544061
-                               self.deol)
544061
-
544061
-    def _dump_line(self, *args):
544061
-        return u"".join(unicode(x) for x in args)
544061
-
544061
-    def dump(self, options, level=0):
544061
-        output = []
544061
-        if level >= len(self.indent):
544061
-            level = len(self.indent) - 1
544061
-
544061
-        for o in options:
544061
-            if o['type'] == "section":
544061
-                output.append(self._dump_line(self.sectnamdel[0],
544061
-                                              o['name'],
544061
-                                              self.sectnamdel[1]))
544061
-                output.append(self.dump(o['value'], (level + 1)))
544061
-                continue
544061
-            if o['type'] == "subsection":
544061
-                output.append(self._dump_line(self.indent[level],
544061
-                                              o['name'],
544061
-                                              self.dassign,
544061
-                                              self.subsectdel[0]))
544061
-                output.append(self.dump(o['value'], (level + 1)))
544061
-                output.append(self._dump_line(self.indent[level],
544061
-                                              self.subsectdel[1]))
544061
-                continue
544061
-            if o['type'] == "option":
544061
-                delim = o.get('delim', self.dassign)
544061
-                if delim not in self.assign:
544061
-                    raise ValueError('Unknown delim "%s" must be one of "%s"' % (delim, " ".join([d for d in self.assign])))
544061
-                output.append(self._dump_line(self.indent[level],
544061
-                                              o['name'],
544061
-                                              delim,
544061
-                                              o['value']))
544061
-                continue
544061
-            if o['type'] == "comment":
544061
-                output.append(self._dump_line(self.dcomment, o['value']))
544061
-                continue
544061
-            if o['type'] == "empty":
544061
-                output.append('')
544061
-                continue
544061
-            raise SyntaxError('Unknown type: [%s]' % o['type'])
544061
-
544061
-        # append an empty string to the output so that we add eol to the end
544061
-        # of the file contents in a single join()
544061
-        output.append('')
544061
-        return self.deol.join(output)
544061
-
544061
-    def parseLine(self, line):
544061
-
544061
-        if self.matchEmpty(line):
544061
-            return {'name': 'empty', 'type': 'empty'}
544061
-
544061
-        value = self.matchComment(line)
544061
-        if value:
544061
-            return {'name': 'comment',
544061
-                    'type': 'comment',
544061
-                    'value': value.rstrip()}  # pylint: disable=E1103
544061
-
544061
-        o = dict()
544061
-        parts = line.split(self.dassign, 1)
544061
-        if len(parts) < 2:
544061
-            # The default assign didn't match, try the non-default
544061
-            for d in self.assign[1:]:
544061
-                parts = line.split(d, 1)
544061
-                if len(parts) >= 2:
544061
-                    o['delim'] = d
544061
-                    break
544061
-
544061
-            if 'delim' not in o:
544061
-                raise SyntaxError('Syntax Error: Unknown line format')
544061
-
544061
-        o.update({'name':parts[0].strip(), 'type':'option', 'value':parts[1].rstrip()})
544061
-        return o
544061
-
544061
-    def findOpts(self, opts, type, name, exclude_sections=False):
544061
-
544061
-        num = 0
544061
-        for o in opts:
544061
-            if o['type'] == type and o['name'] == name:
544061
-                return (num, o)
544061
-            if exclude_sections and (o['type'] == "section" or
544061
-                                     o['type'] == "subsection"):
544061
-                return (num, None)
544061
-            num += 1
544061
-        return (num, None)
544061
-
544061
-    def commentOpts(self, inopts, level=0):
544061
-
544061
-        opts = []
544061
-
544061
-        if level >= len(self.indent):
544061
-            level = len(self.indent) - 1
544061
-
544061
-        for o in inopts:
544061
-            if o['type'] == 'section':
544061
-                no = self.commentOpts(o['value'], (level + 1))
544061
-                val = self._dump_line(self.dcomment,
544061
-                                      self.sectnamdel[0],
544061
-                                      o['name'],
544061
-                                      self.sectnamdel[1])
544061
-                opts.append({'name': 'comment',
544061
-                             'type': 'comment',
544061
-                             'value': val})
544061
-                for n in no:
544061
-                    opts.append(n)
544061
-                continue
544061
-            if o['type'] == 'subsection':
544061
-                no = self.commentOpts(o['value'], (level + 1))
544061
-                val = self._dump_line(self.indent[level],
544061
-                                      o['name'],
544061
-                                      self.dassign,
544061
-                                      self.subsectdel[0])
544061
-                opts.append({'name': 'comment',
544061
-                             'type': 'comment',
544061
-                             'value': val})
544061
-                opts.extend(no)
544061
-                val = self._dump_line(self.indent[level], self.subsectdel[1])
544061
-                opts.append({'name': 'comment',
544061
-                             'type': 'comment',
544061
-                             'value': val})
544061
-                continue
544061
-            if o['type'] == 'option':
544061
-                delim = o.get('delim', self.dassign)
544061
-                if delim not in self.assign:
544061
-                    val = self._dump_line(self.indent[level],
544061
-                                          o['name'],
544061
-                                          delim,
544061
-                                          o['value'])
544061
-                opts.append({'name':'comment', 'type':'comment', 'value':val})
544061
-                continue
544061
-            if o['type'] == 'comment':
544061
-                opts.append(o)
544061
-                continue
544061
-            if o['type'] == 'empty':
544061
-                opts.append({'name': 'comment',
544061
-                             'type': 'comment',
544061
-                             'value': ''})
544061
-                continue
544061
-            raise SyntaxError('Unknown type: [%s]' % o['type'])
544061
-
544061
-        return opts
544061
-
544061
-    def mergeOld(self, oldopts, newopts):
544061
-
544061
-        opts = []
544061
-
544061
-        for o in oldopts:
544061
-            if o['type'] == "section" or o['type'] == "subsection":
544061
-                _num, no = self.findOpts(newopts, o['type'], o['name'])
544061
-                if not no:
544061
-                    opts.append(o)
544061
-                    continue
544061
-                if no['action'] == "set":
544061
-                    mo = self.mergeOld(o['value'], no['value'])
544061
-                    opts.append({'name': o['name'],
544061
-                                 'type': o['type'],
544061
-                                 'value': mo})
544061
-                    continue
544061
-                if no['action'] == "comment":
544061
-                    co = self.commentOpts(o['value'])
544061
-                    for c in co:
544061
-                        opts.append(c)
544061
-                    continue
544061
-                if no['action'] == "remove":
544061
-                    continue
544061
-                raise SyntaxError('Unknown action: [%s]' % no['action'])
544061
-
544061
-            if o['type'] == "comment" or o['type'] == "empty":
544061
-                opts.append(o)
544061
-                continue
544061
-
544061
-            if o['type'] == "option":
544061
-                _num, no = self.findOpts(newopts, 'option', o['name'], True)
544061
-                if not no:
544061
-                    opts.append(o)
544061
-                    continue
544061
-                if no['action'] == 'comment' or no['action'] == 'remove':
544061
-                    if (no['value'] is not None and
544061
-                            o['value'] is not no['value']):
544061
-                        opts.append(o)
544061
-                        continue
544061
-                    if no['action'] == 'comment':
544061
-                        value = self._dump_line(self.dcomment,
544061
-                                                o['name'],
544061
-                                                self.dassign,
544061
-                                                o['value'])
544061
-                        opts.append({'name': 'comment',
544061
-                                     'type': 'comment',
544061
-                                     'value': value})
544061
-                    continue
544061
-                if no['action'] == 'set':
544061
-                    opts.append(no)
544061
-                    continue
544061
-                if no['action'] == 'addifnotset':
544061
-                    opts.append({
544061
-                        'name': 'comment',
544061
-                        'type': 'comment',
544061
-                        'value': self._dump_line(
544061
-                            ' ', no['name'], ' modified by IPA'
544061
-                        ),
544061
-                    })
544061
-                    opts.append({'name': 'comment', 'type': 'comment',
544061
-                                'value': self._dump_line(no['name'],
544061
-                                                         self.dassign,
544061
-                                                         no['value'],
544061
-                                                         )})
544061
-                    opts.append(o)
544061
-                    continue
544061
-                raise SyntaxError('Unknown action: [%s]' % no['action'])
544061
-
544061
-            raise SyntaxError('Unknown type: [%s]' % o['type'])
544061
-
544061
-        return opts
544061
-
544061
-    def mergeNew(self, opts, newopts):
544061
-
544061
-        cline = 0
544061
-
544061
-        for no in newopts:
544061
-
544061
-            if no['type'] == "section" or no['type'] == "subsection":
544061
-                (num, o) = self.findOpts(opts, no['type'], no['name'])
544061
-                if not o:
544061
-                    if no['action'] == 'set':
544061
-                        opts.append(no)
544061
-                    continue
544061
-                if no['action'] == "set":
544061
-                    self.mergeNew(o['value'], no['value'])
544061
-                    continue
544061
-                cline = num + 1
544061
-                continue
544061
-
544061
-            if no['type'] == "option":
544061
-                (num, o) = self.findOpts(opts, no['type'], no['name'], True)
544061
-                if not o:
544061
-                    if no['action'] == 'set' or no['action'] == 'addifnotset':
544061
-                        opts.append(no)
544061
-                    continue
544061
-                cline = num + 1
544061
-                continue
544061
-
544061
-            if no['type'] == "comment" or no['type'] == "empty":
544061
-                opts.insert(cline, no)
544061
-                cline += 1
544061
-                continue
544061
-
544061
-            raise SyntaxError('Unknown type: [%s]' % no['type'])
544061
-
544061
-    def merge(self, oldopts, newopts):
544061
-        """
544061
-        Uses a two pass strategy:
544061
-        First we create a new opts tree from oldopts removing/commenting
544061
-          the options as indicated by the contents of newopts
544061
-        Second we fill in the new opts tree with options as indicated
544061
-          in the newopts tree (this is becaus eentire (sub)sections may
544061
-          in the newopts tree (this is becaus entire (sub)sections may
544061
-          exist in the newopts that do not exist in oldopts)
544061
-        """
544061
-        opts = self.mergeOld(oldopts, newopts)
544061
-        self.mergeNew(opts, newopts)
544061
-        return opts
544061
-
544061
-    #TODO: Make parse() recursive?
544061
-    def parse(self, f):
544061
-
544061
-        opts = []
544061
-        sectopts = []
544061
-        section = None
544061
-        subsectopts = []
544061
-        subsection = None
544061
-        curopts = opts
544061
-        fatheropts = opts
544061
-
544061
-        # Read in the old file.
544061
-        for line in f:
544061
-
544061
-            # It's a section start.
544061
-            value = self.matchSection(line)
544061
-            if value:
544061
-                if section is not None:
544061
-                    opts.append({'name': section,
544061
-                                 'type': 'section',
544061
-                                 'value': sectopts})
544061
-                sectopts = []
544061
-                curopts = sectopts
544061
-                fatheropts = sectopts
544061
-                section = value
544061
-                continue
544061
-
544061
-            # It's a subsection start.
544061
-            value = self.matchSubSection(line)
544061
-            if value:
544061
-                if subsection is not None:
544061
-                    raise SyntaxError('nested subsections are not '
544061
-                                      'supported yet')
544061
-                subsectopts = []
544061
-                curopts = subsectopts
544061
-                subsection = value
544061
-                continue
544061
-
544061
-            value = self.matchSubSectionEnd(line)
544061
-            if value:
544061
-                if subsection is None:
544061
-                    raise SyntaxError('Unmatched end subsection terminator '
544061
-                                      'found')
544061
-                fatheropts.append({'name': subsection,
544061
-                                   'type': 'subsection',
544061
-                                   'value': subsectopts})
544061
-                subsection = None
544061
-                curopts = fatheropts
544061
-                continue
544061
-
544061
-            # Copy anything else as is.
544061
-            try:
544061
-                curopts.append(self.parseLine(line))
544061
-            except SyntaxError as e:
544061
-                raise SyntaxError('{error} in file {fname}: [{line}]'.format(
544061
-                    error=e, fname=f.name, line=line.rstrip()))
544061
-
544061
-        #Add last section if any
544061
-        if len(sectopts) is not 0:
544061
-            opts.append({'name': section,
544061
-                         'type': 'section',
544061
-                         'value': sectopts})
544061
-
544061
-        return opts
544061
-
544061
-    def changeConf(self, file, newopts):
544061
-        """
544061
-        Write settings to configuration file
544061
-        :param file: path to the file
544061
-        :param options: set of dictionaries in the form:
544061
-             {'name': 'foo', 'value': 'bar', 'action': 'set/comment'}
544061
-        :param section: section name like 'global'
544061
-        """
544061
-        output = ""
544061
-        f = None
544061
-        try:
544061
-            # Do not catch an unexisting file error
544061
-            # we want to fail in that case
544061
-            shutil.copy2(file, (file + ".ipabkp"))
544061
-
544061
-            f = openLocked(file, 0o644)
544061
-
544061
-            oldopts = self.parse(f)
544061
-
544061
-            options = self.merge(oldopts, newopts)
544061
-
544061
-            output = self.dump(options)
544061
-
544061
-            # Write it out and close it.
544061
-            f.seek(0)
544061
-            f.truncate(0)
544061
-            f.write(output)
544061
-        finally:
544061
-            try:
544061
-                if f:
544061
-                    f.close()
544061
-            except IOError:
544061
-                pass
544061
-        logger.debug("Updating configuration file %s", file)
544061
-        logger.debug(output)
544061
-        return True
544061
-
544061
-    def newConf(self, file, options, file_perms=0o644):
544061
-        """"
544061
-        Write settings to a new file, backup the old
544061
-        :param file: path to the file
544061
-        :param options: a set of dictionaries in the form:
544061
-             {'name': 'foo', 'value': 'bar', 'action': 'set/comment'}
544061
-        :param file_perms: number defining the new file's permissions
544061
-        """
544061
-        output = ""
544061
-        f = None
544061
-        try:
544061
-            try:
544061
-                shutil.copy2(file, (file + ".ipabkp"))
544061
-            except IOError as err:
544061
-                if err.errno == 2:
544061
-                    # The orign file did not exist
544061
-                    pass
544061
-
544061
-            f = openLocked(file, file_perms)
544061
-
544061
-            # Trunkate
544061
-            f.seek(0)
544061
-            f.truncate(0)
544061
-
544061
-            output = self.dump(options)
544061
-
544061
-            f.write(output)
544061
-        finally:
544061
-            try:
544061
-                if f:
544061
-                    f.close()
544061
-            except IOError:
544061
-                pass
544061
-        logger.debug("Writing configuration file %s", file)
544061
-        logger.debug(output)
544061
-        return True
544061
-
544061
-    @staticmethod
544061
-    def setOption(name, value):
544061
-        return {'name': name,
544061
-                'type': 'option',
544061
-                'action': 'set',
544061
-                'value': value}
544061
-
544061
-    @staticmethod
544061
-    def rmOption(name):
544061
-        return {'name': name,
544061
-                'type': 'option',
544061
-                'action': 'remove',
544061
-                'value': None}
544061
-
544061
-    @staticmethod
544061
-    def setSection(name, options):
544061
-        return {'name': name,
544061
-                'type': 'section',
544061
-                'action': 'set',
544061
-                'value': options}
544061
-
544061
-    @staticmethod
544061
-    def emptyLine():
544061
-        return {'name': 'empty',
544061
-                'type': 'empty'}
544061
+        """something"""
544061
+        warnings.warn(
544061
+            "Use 'ipapython.ipachangeconf.IPAChangeConfg'",
544061
+            DeprecationWarning,
544061
+            stacklevel=2
544061
+        )
544061
+        super(IPAChangeConf, self).__init__(name)
544061
diff --git a/ipapython/ipachangeconf.py b/ipapython/ipachangeconf.py
544061
new file mode 100644
544061
index 0000000..cfb4a6e
544061
--- /dev/null
544061
+++ b/ipapython/ipachangeconf.py
544061
@@ -0,0 +1,590 @@
544061
+#
544061
+# ipachangeconf - configuration file manipulation classes and functions
544061
+# partially based on authconfig code
544061
+# Copyright (c) 1999-2007 Red Hat, Inc.
544061
+# Author: Simo Sorce <ssorce@redhat.com>
544061
+#
544061
+# This program is free software; you can redistribute it and/or modify
544061
+# it under the terms of the GNU General Public License as published by
544061
+# the Free Software Foundation, either version 3 of the License, or
544061
+# (at your option) any later version.
544061
+#
544061
+# This program is distributed in the hope that it will be useful,
544061
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
544061
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
544061
+# GNU General Public License for more details.
544061
+#
544061
+# You should have received a copy of the GNU General Public License
544061
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
544061
+#
544061
+
544061
+import fcntl
544061
+import logging
544061
+import os
544061
+import shutil
544061
+
544061
+import six
544061
+
544061
+if six.PY3:
544061
+    unicode = str
544061
+
544061
+logger = logging.getLogger(__name__)
544061
+
544061
+
544061
+def openLocked(filename, perms):
544061
+    fd = -1
544061
+    try:
544061
+        fd = os.open(filename, os.O_RDWR | os.O_CREAT, perms)
544061
+
544061
+        fcntl.lockf(fd, fcntl.LOCK_EX)
544061
+    except OSError as e:
544061
+        if fd != -1:
544061
+            try:
544061
+                os.close(fd)
544061
+            except OSError:
544061
+                pass
544061
+        raise IOError(e.errno, e.strerror)
544061
+    return os.fdopen(fd, "r+")
544061
+
544061
+    # TODO: add subsection as a concept
544061
+    #       (ex. REALM.NAME = { foo = x bar = y } )
544061
+    # TODO: put section delimiters as separating element of the list
544061
+    #       so that we can process multiple sections in one go
544061
+    # TODO: add a comment all but provided options as a section option
544061
+
544061
+
544061
+class IPAChangeConf:
544061
+    def __init__(self, name):
544061
+        self.progname = name
544061
+        self.indent = ("", "", "")
544061
+        self.assign = (" = ", "=")
544061
+        self.dassign = self.assign[0]
544061
+        self.comment = ("#",)
544061
+        self.dcomment = self.comment[0]
544061
+        self.eol = ("\n",)
544061
+        self.deol = self.eol[0]
544061
+        self.sectnamdel = ("[", "]")
544061
+        self.subsectdel = ("{", "}")
544061
+        self.case_insensitive_sections = True
544061
+
544061
+    def setProgName(self, name):
544061
+        self.progname = name
544061
+
544061
+    def setIndent(self, indent):
544061
+        if type(indent) is tuple:
544061
+            self.indent = indent
544061
+        elif type(indent) is str:
544061
+            self.indent = (indent, )
544061
+        else:
544061
+            raise ValueError('Indent must be a list of strings')
544061
+
544061
+    def setOptionAssignment(self, assign):
544061
+        if type(assign) is tuple:
544061
+            self.assign = assign
544061
+        else:
544061
+            self.assign = (assign, )
544061
+        self.dassign = self.assign[0]
544061
+
544061
+    def setCommentPrefix(self, comment):
544061
+        if type(comment) is tuple:
544061
+            self.comment = comment
544061
+        else:
544061
+            self.comment = (comment, )
544061
+        self.dcomment = self.comment[0]
544061
+
544061
+    def setEndLine(self, eol):
544061
+        if type(eol) is tuple:
544061
+            self.eol = eol
544061
+        else:
544061
+            self.eol = (eol, )
544061
+        self.deol = self.eol[0]
544061
+
544061
+    def setSectionNameDelimiters(self, delims):
544061
+        self.sectnamdel = delims
544061
+
544061
+    def setSubSectionDelimiters(self, delims):
544061
+        self.subsectdel = delims
544061
+
544061
+    def matchComment(self, line):
544061
+        for v in self.comment:
544061
+            if line.lstrip().startswith(v):
544061
+                return line.lstrip()[len(v):]
544061
+        return False
544061
+
544061
+    def matchEmpty(self, line):
544061
+        if line.strip() == "":
544061
+            return True
544061
+        return False
544061
+
544061
+    def matchSection(self, line):
544061
+        cl = "".join(line.strip().split())
544061
+        cl = cl.lower() if self.case_insensitive_sections else cl
544061
+
544061
+        if len(self.sectnamdel) != 2:
544061
+            return False
544061
+        if not cl.startswith(self.sectnamdel[0]):
544061
+            return False
544061
+        if not cl.endswith(self.sectnamdel[1]):
544061
+            return False
544061
+        return cl[len(self.sectnamdel[0]):-len(self.sectnamdel[1])]
544061
+
544061
+    def matchSubSection(self, line):
544061
+        if self.matchComment(line):
544061
+            return False
544061
+
544061
+        parts = line.split(self.dassign, 1)
544061
+        if len(parts) < 2:
544061
+            return False
544061
+
544061
+        if parts[1].strip() == self.subsectdel[0]:
544061
+            return parts[0].strip()
544061
+
544061
+        return False
544061
+
544061
+    def matchSubSectionEnd(self, line):
544061
+        if self.matchComment(line):
544061
+            return False
544061
+
544061
+        if line.strip() == self.subsectdel[1]:
544061
+            return True
544061
+
544061
+        return False
544061
+
544061
+    def getSectionLine(self, section):
544061
+        if len(self.sectnamdel) != 2:
544061
+            return section
544061
+        return self._dump_line(self.sectnamdel[0],
544061
+                               section,
544061
+                               self.sectnamdel[1],
544061
+                               self.deol)
544061
+
544061
+    def _dump_line(self, *args):
544061
+        return u"".join(unicode(x) for x in args)
544061
+
544061
+    def dump(self, options, level=0):
544061
+        output = []
544061
+        if level >= len(self.indent):
544061
+            level = len(self.indent) - 1
544061
+
544061
+        for o in options:
544061
+            if o['type'] == "section":
544061
+                output.append(self._dump_line(self.sectnamdel[0],
544061
+                                              o['name'],
544061
+                                              self.sectnamdel[1]))
544061
+                output.append(self.dump(o['value'], (level + 1)))
544061
+                continue
544061
+            if o['type'] == "subsection":
544061
+                output.append(self._dump_line(self.indent[level],
544061
+                                              o['name'],
544061
+                                              self.dassign,
544061
+                                              self.subsectdel[0]))
544061
+                output.append(self.dump(o['value'], (level + 1)))
544061
+                output.append(self._dump_line(self.indent[level],
544061
+                                              self.subsectdel[1]))
544061
+                continue
544061
+            if o['type'] == "option":
544061
+                delim = o.get('delim', self.dassign)
544061
+                if delim not in self.assign:
544061
+                    raise ValueError(
544061
+                        'Unknown delim "%s" must be one of "%s"' %
544061
+                        (delim, " ".join([d for d in self.assign]))
544061
+                    )
544061
+                output.append(self._dump_line(self.indent[level],
544061
+                                              o['name'],
544061
+                                              delim,
544061
+                                              o['value']))
544061
+                continue
544061
+            if o['type'] == "comment":
544061
+                output.append(self._dump_line(self.dcomment, o['value']))
544061
+                continue
544061
+            if o['type'] == "empty":
544061
+                output.append('')
544061
+                continue
544061
+            raise SyntaxError('Unknown type: [%s]' % o['type'])
544061
+
544061
+        # append an empty string to the output so that we add eol to the end
544061
+        # of the file contents in a single join()
544061
+        output.append('')
544061
+        return self.deol.join(output)
544061
+
544061
+    def parseLine(self, line):
544061
+
544061
+        if self.matchEmpty(line):
544061
+            return {'name': 'empty', 'type': 'empty'}
544061
+
544061
+        value = self.matchComment(line)
544061
+        if value:
544061
+            return {'name': 'comment',
544061
+                    'type': 'comment',
544061
+                    'value': value.rstrip()}  # pylint: disable=E1103
544061
+
544061
+        o = dict()
544061
+        parts = line.split(self.dassign, 1)
544061
+        if len(parts) < 2:
544061
+            # The default assign didn't match, try the non-default
544061
+            for d in self.assign[1:]:
544061
+                parts = line.split(d, 1)
544061
+                if len(parts) >= 2:
544061
+                    o['delim'] = d
544061
+                    break
544061
+
544061
+            if 'delim' not in o:
544061
+                raise SyntaxError('Syntax Error: Unknown line format')
544061
+
544061
+        o.update({'name': parts[0].strip(), 'type': 'option',
544061
+                  'value': parts[1].rstrip()})
544061
+        return o
544061
+
544061
+    def findOpts(self, opts, type, name, exclude_sections=False):
544061
+
544061
+        num = 0
544061
+        for o in opts:
544061
+            if o['type'] == type and o['name'] == name:
544061
+                return (num, o)
544061
+            if exclude_sections and (o['type'] == "section" or
544061
+                                     o['type'] == "subsection"):
544061
+                return (num, None)
544061
+            num += 1
544061
+        return (num, None)
544061
+
544061
+    def commentOpts(self, inopts, level=0):
544061
+
544061
+        opts = []
544061
+
544061
+        if level >= len(self.indent):
544061
+            level = len(self.indent) - 1
544061
+
544061
+        for o in inopts:
544061
+            if o['type'] == 'section':
544061
+                no = self.commentOpts(o['value'], (level + 1))
544061
+                val = self._dump_line(self.dcomment,
544061
+                                      self.sectnamdel[0],
544061
+                                      o['name'],
544061
+                                      self.sectnamdel[1])
544061
+                opts.append({'name': 'comment',
544061
+                             'type': 'comment',
544061
+                             'value': val})
544061
+                for n in no:
544061
+                    opts.append(n)
544061
+                continue
544061
+            if o['type'] == 'subsection':
544061
+                no = self.commentOpts(o['value'], (level + 1))
544061
+                val = self._dump_line(self.indent[level],
544061
+                                      o['name'],
544061
+                                      self.dassign,
544061
+                                      self.subsectdel[0])
544061
+                opts.append({'name': 'comment',
544061
+                             'type': 'comment',
544061
+                             'value': val})
544061
+                opts.extend(no)
544061
+                val = self._dump_line(self.indent[level], self.subsectdel[1])
544061
+                opts.append({'name': 'comment',
544061
+                             'type': 'comment',
544061
+                             'value': val})
544061
+                continue
544061
+            if o['type'] == 'option':
544061
+                delim = o.get('delim', self.dassign)
544061
+                if delim not in self.assign:
544061
+                    val = self._dump_line(self.indent[level],
544061
+                                          o['name'],
544061
+                                          delim,
544061
+                                          o['value'])
544061
+                opts.append({'name': 'comment', 'type': 'comment',
544061
+                             'value': val})
544061
+                continue
544061
+            if o['type'] == 'comment':
544061
+                opts.append(o)
544061
+                continue
544061
+            if o['type'] == 'empty':
544061
+                opts.append({'name': 'comment',
544061
+                             'type': 'comment',
544061
+                             'value': ''})
544061
+                continue
544061
+            raise SyntaxError('Unknown type: [%s]' % o['type'])
544061
+
544061
+        return opts
544061
+
544061
+    def mergeOld(self, oldopts, newopts):
544061
+
544061
+        opts = []
544061
+
544061
+        for o in oldopts:
544061
+            if o['type'] == "section" or o['type'] == "subsection":
544061
+                _num, no = self.findOpts(newopts, o['type'], o['name'])
544061
+                if not no:
544061
+                    opts.append(o)
544061
+                    continue
544061
+                if no['action'] == "set":
544061
+                    mo = self.mergeOld(o['value'], no['value'])
544061
+                    opts.append({'name': o['name'],
544061
+                                 'type': o['type'],
544061
+                                 'value': mo})
544061
+                    continue
544061
+                if no['action'] == "comment":
544061
+                    co = self.commentOpts(o['value'])
544061
+                    for c in co:
544061
+                        opts.append(c)
544061
+                    continue
544061
+                if no['action'] == "remove":
544061
+                    continue
544061
+                raise SyntaxError('Unknown action: [%s]' % no['action'])
544061
+
544061
+            if o['type'] == "comment" or o['type'] == "empty":
544061
+                opts.append(o)
544061
+                continue
544061
+
544061
+            if o['type'] == "option":
544061
+                _num, no = self.findOpts(newopts, 'option', o['name'], True)
544061
+                if not no:
544061
+                    opts.append(o)
544061
+                    continue
544061
+                if no['action'] == 'comment' or no['action'] == 'remove':
544061
+                    if (no['value'] is not None and
544061
+                            o['value'] is not no['value']):
544061
+                        opts.append(o)
544061
+                        continue
544061
+                    if no['action'] == 'comment':
544061
+                        value = self._dump_line(self.dcomment,
544061
+                                                o['name'],
544061
+                                                self.dassign,
544061
+                                                o['value'])
544061
+                        opts.append({'name': 'comment',
544061
+                                     'type': 'comment',
544061
+                                     'value': value})
544061
+                    continue
544061
+                if no['action'] == 'set':
544061
+                    opts.append(no)
544061
+                    continue
544061
+                if no['action'] == 'addifnotset':
544061
+                    opts.append({
544061
+                        'name': 'comment',
544061
+                        'type': 'comment',
544061
+                        'value': self._dump_line(
544061
+                            ' ', no['name'], ' modified by IPA'
544061
+                        ),
544061
+                    })
544061
+                    opts.append({'name': 'comment', 'type': 'comment',
544061
+                                'value': self._dump_line(no['name'],
544061
+                                                         self.dassign,
544061
+                                                         no['value'],
544061
+                                                         )})
544061
+                    opts.append(o)
544061
+                    continue
544061
+                raise SyntaxError('Unknown action: [%s]' % no['action'])
544061
+
544061
+            raise SyntaxError('Unknown type: [%s]' % o['type'])
544061
+
544061
+        return opts
544061
+
544061
+    def mergeNew(self, opts, newopts):
544061
+
544061
+        cline = 0
544061
+
544061
+        for no in newopts:
544061
+
544061
+            if no['type'] == "section" or no['type'] == "subsection":
544061
+                (num, o) = self.findOpts(opts, no['type'], no['name'])
544061
+                if not o:
544061
+                    if no['action'] == 'set':
544061
+                        opts.append(no)
544061
+                    continue
544061
+                if no['action'] == "set":
544061
+                    self.mergeNew(o['value'], no['value'])
544061
+                    continue
544061
+                cline = num + 1
544061
+                continue
544061
+
544061
+            if no['type'] == "option":
544061
+                (num, o) = self.findOpts(opts, no['type'], no['name'], True)
544061
+                if not o:
544061
+                    if no['action'] == 'set' or no['action'] == 'addifnotset':
544061
+                        opts.append(no)
544061
+                    continue
544061
+                cline = num + 1
544061
+                continue
544061
+
544061
+            if no['type'] == "comment" or no['type'] == "empty":
544061
+                opts.insert(cline, no)
544061
+                cline += 1
544061
+                continue
544061
+
544061
+            raise SyntaxError('Unknown type: [%s]' % no['type'])
544061
+
544061
+    def merge(self, oldopts, newopts):
544061
+        """
544061
+        Uses a two pass strategy:
544061
+        First we create a new opts tree from oldopts removing/commenting
544061
+          the options as indicated by the contents of newopts
544061
+        Second we fill in the new opts tree with options as indicated
544061
+          in the newopts tree (this is becaus eentire (sub)sections may
544061
+          in the newopts tree (this is becaus entire (sub)sections may
544061
+          exist in the newopts that do not exist in oldopts)
544061
+        """
544061
+        opts = self.mergeOld(oldopts, newopts)
544061
+        self.mergeNew(opts, newopts)
544061
+        return opts
544061
+
544061
+    # TODO: Make parse() recursive?
544061
+    def parse(self, f):
544061
+
544061
+        opts = []
544061
+        sectopts = []
544061
+        section = None
544061
+        subsectopts = []
544061
+        subsection = None
544061
+        curopts = opts
544061
+        fatheropts = opts
544061
+
544061
+        # Read in the old file.
544061
+        for line in f:
544061
+
544061
+            # It's a section start.
544061
+            value = self.matchSection(line)
544061
+            if value:
544061
+                if section is not None:
544061
+                    opts.append({'name': section,
544061
+                                 'type': 'section',
544061
+                                 'value': sectopts})
544061
+                sectopts = []
544061
+                curopts = sectopts
544061
+                fatheropts = sectopts
544061
+                section = value
544061
+                continue
544061
+
544061
+            # It's a subsection start.
544061
+            value = self.matchSubSection(line)
544061
+            if value:
544061
+                if subsection is not None:
544061
+                    raise SyntaxError('nested subsections are not '
544061
+                                      'supported yet')
544061
+                subsectopts = []
544061
+                curopts = subsectopts
544061
+                subsection = value
544061
+                continue
544061
+
544061
+            value = self.matchSubSectionEnd(line)
544061
+            if value:
544061
+                if subsection is None:
544061
+                    raise SyntaxError('Unmatched end subsection terminator '
544061
+                                      'found')
544061
+                fatheropts.append({'name': subsection,
544061
+                                   'type': 'subsection',
544061
+                                   'value': subsectopts})
544061
+                subsection = None
544061
+                curopts = fatheropts
544061
+                continue
544061
+
544061
+            # Copy anything else as is.
544061
+            try:
544061
+                curopts.append(self.parseLine(line))
544061
+            except SyntaxError as e:
544061
+                raise SyntaxError('{error} in file {fname}: [{line}]'.format(
544061
+                    error=e, fname=f.name, line=line.rstrip()))
544061
+
544061
+        # Add last section if any
544061
+        if len(sectopts) is not 0:
544061
+            opts.append({'name': section,
544061
+                         'type': 'section',
544061
+                         'value': sectopts})
544061
+
544061
+        return opts
544061
+
544061
+    def changeConf(self, file, newopts):
544061
+        """
544061
+        Write settings to configuration file
544061
+        :param file: path to the file
544061
+        :param options: set of dictionaries in the form:
544061
+             {'name': 'foo', 'value': 'bar', 'action': 'set/comment'}
544061
+        :param section: section name like 'global'
544061
+        """
544061
+        output = ""
544061
+        f = None
544061
+        try:
544061
+            # Do not catch an unexisting file error
544061
+            # we want to fail in that case
544061
+            shutil.copy2(file, (file + ".ipabkp"))
544061
+
544061
+            f = openLocked(file, 0o644)
544061
+
544061
+            oldopts = self.parse(f)
544061
+
544061
+            options = self.merge(oldopts, newopts)
544061
+
544061
+            output = self.dump(options)
544061
+
544061
+            # Write it out and close it.
544061
+            f.seek(0)
544061
+            f.truncate(0)
544061
+            f.write(output)
544061
+        finally:
544061
+            try:
544061
+                if f:
544061
+                    f.close()
544061
+            except IOError:
544061
+                pass
544061
+        logger.debug("Updating configuration file %s", file)
544061
+        logger.debug(output)
544061
+        return True
544061
+
544061
+    def newConf(self, file, options, file_perms=0o644):
544061
+        """"
544061
+        Write settings to a new file, backup the old
544061
+        :param file: path to the file
544061
+        :param options: a set of dictionaries in the form:
544061
+             {'name': 'foo', 'value': 'bar', 'action': 'set/comment'}
544061
+        :param file_perms: number defining the new file's permissions
544061
+        """
544061
+        output = ""
544061
+        f = None
544061
+        try:
544061
+            try:
544061
+                shutil.copy2(file, (file + ".ipabkp"))
544061
+            except IOError as err:
544061
+                if err.errno == 2:
544061
+                    # The orign file did not exist
544061
+                    pass
544061
+
544061
+            f = openLocked(file, file_perms)
544061
+
544061
+            # Trunkate
544061
+            f.seek(0)
544061
+            f.truncate(0)
544061
+
544061
+            output = self.dump(options)
544061
+
544061
+            f.write(output)
544061
+        finally:
544061
+            try:
544061
+                if f:
544061
+                    f.close()
544061
+            except IOError:
544061
+                pass
544061
+        logger.debug("Writing configuration file %s", file)
544061
+        logger.debug(output)
544061
+        return True
544061
+
544061
+    @staticmethod
544061
+    def setOption(name, value):
544061
+        return {'name': name,
544061
+                'type': 'option',
544061
+                'action': 'set',
544061
+                'value': value}
544061
+
544061
+    @staticmethod
544061
+    def rmOption(name):
544061
+        return {'name': name,
544061
+                'type': 'option',
544061
+                'action': 'remove',
544061
+                'value': None}
544061
+
544061
+    @staticmethod
544061
+    def setSection(name, options):
544061
+        return {'name': name,
544061
+                'type': 'section',
544061
+                'action': 'set',
544061
+                'value': options}
544061
+
544061
+    @staticmethod
544061
+    def emptyLine():
544061
+        return {'name': 'empty',
544061
+                'type': 'empty'}
544061
diff --git a/ipaserver/install/adtrustinstance.py b/ipaserver/install/adtrustinstance.py
544061
index 7bb9431..47a5a92 100644
544061
--- a/ipaserver/install/adtrustinstance.py
544061
+++ b/ipaserver/install/adtrustinstance.py
544061
@@ -40,11 +40,11 @@ from ipaserver.install.replication import wait_for_task
544061
 from ipalib import errors, api
544061
 from ipalib.util import normalize_zone
544061
 from ipapython.dn import DN
544061
+from ipapython import ipachangeconf
544061
 from ipapython import ipaldap
544061
 from ipapython import ipautil
544061
 import ipapython.errors
544061
 
544061
-import ipaclient.install.ipachangeconf
544061
 from ipaplatform import services
544061
 from ipaplatform.constants import constants
544061
 from ipaplatform.paths import paths
544061
@@ -639,7 +639,7 @@ class ADTRUSTInstance(service.Service):
544061
             self.print_msg("Cannot modify /etc/krb5.conf")
544061
 
544061
         krbconf = (
544061
-            ipaclient.install.ipachangeconf.IPAChangeConf("IPA Installer"))
544061
+            ipachangeconf.IPAChangeConf("IPA Installer"))
544061
         krbconf.setOptionAssignment((" = ", " "))
544061
         krbconf.setSectionNameDelimiters(("[", "]"))
544061
         krbconf.setSubSectionDelimiters(("{", "}"))
544061
diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py
544061
index 02c8f4d..6a81d57 100644
544061
--- a/ipaserver/install/server/install.py
544061
+++ b/ipaserver/install/server/install.py
544061
@@ -19,7 +19,7 @@ import six
544061
 from ipaclient.install import timeconf
544061
 from ipaclient.install.client import (
544061
     check_ldap_conf, sync_time, restore_time_sync)
544061
-from ipaclient.install.ipachangeconf import IPAChangeConf
544061
+from ipapython.ipachangeconf import IPAChangeConf
544061
 from ipalib.install import certmonger, sysrestore
544061
 from ipapython import ipautil, version
544061
 from ipapython.ipautil import (
544061
diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py
544061
index 6da6804..7272640 100644
544061
--- a/ipaserver/install/server/replicainstall.py
544061
+++ b/ipaserver/install/server/replicainstall.py
544061
@@ -23,13 +23,13 @@ from pkg_resources import parse_version
544061
 import six
544061
 
544061
 from ipaclient.install.client import check_ldap_conf
544061
-from ipaclient.install.ipachangeconf import IPAChangeConf
544061
 import ipaclient.install.timeconf
544061
 from ipalib.install import certstore, sysrestore
544061
 from ipalib.install.kinit import kinit_keytab
544061
 from ipapython import ipaldap, ipautil
544061
 from ipapython.dn import DN
544061
 from ipapython.admintool import ScriptError
544061
+from ipapython.ipachangeconf import IPAChangeConf
544061
 from ipaplatform import services
544061
 from ipaplatform.tasks import tasks
544061
 from ipaplatform.paths import paths
544061
diff --git a/ipatests/test_install/test_changeconf.py b/ipatests/test_install/test_changeconf.py
544061
index 2dc2b7d..40c8a1d 100644
544061
--- a/ipatests/test_install/test_changeconf.py
544061
+++ b/ipatests/test_install/test_changeconf.py
544061
@@ -3,7 +3,7 @@
544061
 from __future__ import absolute_import
544061
 
544061
 import pytest
544061
-from ipaclient.install.ipachangeconf import IPAChangeConf
544061
+from ipapython.ipachangeconf import IPAChangeConf
544061
 
544061
 
544061
 @pytest.fixture(scope='function')
544061
544061
From 2da90887632c764a73866c9ad3824ebb53c0aa73 Mon Sep 17 00:00:00 2001
544061
From: Rob Critenden <rcritten@redhat.com>
544061
Date: Aug 29 2019 06:45:12 +0000
544061
Subject: Use tasks to configure automount nsswitch settings
544061
544061
544061
authselect doesn't allow one to directly write to
544061
/etc/nsswitch.conf. It will complain bitterly if it
544061
detects it and will refuse to work until reset.
544061
544061
Instead it wants the user to write to
544061
/etc/authselect/user-nsswitch.conf and then it will handle
544061
merging in any differences.
544061
544061
To complicate matters some databases are not user configurable
544061
like passwd, group and of course, automount. There are some
544061
undocumented options to allow one to override these though so
544061
we utilize that.
544061
544061
tasks are used so that authselect-based installations can still
544061
write directly to /etc/nsswitch.conf and operate as it used to.
544061
544061
Reviewed-By: Francois Cami <fcami@redhat.com>
544061
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
544061
Reviewed-By: Rob Critenden <rcritten@redhat.com>
544061
Reviewed-By: François Cami <fcami@redhat.com>
544061
544061
---
544061
544061
diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py
544061
index 9492ca4..1e88ba1 100644
544061
--- a/ipaclient/install/client.py
544061
+++ b/ipaclient/install/client.py
544061
@@ -66,7 +66,7 @@ from ipapython import version
544061
 
544061
 from . import automount, timeconf, sssd
544061
 from ipaclient import discovery
544061
-from .ipachangeconf import IPAChangeConf
544061
+from ipapython.ipachangeconf import IPAChangeConf
544061
 
544061
 NoneType = type(None)
544061
 
544061
@@ -281,72 +281,6 @@ def is_ipa_client_installed(fstore, on_master=False):
544061
     return installed
544061
 
544061
 
544061
-def configure_nsswitch_database(fstore, database, services, preserve=True,
544061
-                                append=True, default_value=()):
544061
-    """
544061
-    Edits the specified nsswitch.conf database (e.g. passwd, group, sudoers)
544061
-    to use the specified service(s).
544061
-
544061
-    Arguments:
544061
-        fstore - FileStore to backup the nsswitch.conf
544061
-        database - database configuration that should be ammended,
544061
-                   e.g. 'sudoers'
544061
-        service - list of services that should be added, e.g. ['sss']
544061
-        preserve - if True, the already configured services will be preserved
544061
-
544061
-    The next arguments modify the behaviour if preserve=True:
544061
-        append - if True, the services will be appended, if False, prepended
544061
-        default_value - list of services that are considered as default (if
544061
-                        the database is not mentioned in nsswitch.conf), e.g.
544061
-                        ['files']
544061
-    """
544061
-
544061
-    # Backup the original version of nsswitch.conf, we're going to edit it now
544061
-    if not fstore.has_file(paths.NSSWITCH_CONF):
544061
-        fstore.backup_file(paths.NSSWITCH_CONF)
544061
-
544061
-    conf = IPAChangeConf("IPA Installer")
544061
-    conf.setOptionAssignment(':')
544061
-
544061
-    if preserve:
544061
-        # Read the existing configuration
544061
-        with open(paths.NSSWITCH_CONF, 'r') as f:
544061
-            opts = conf.parse(f)
544061
-            raw_database_entry = conf.findOpts(opts, 'option', database)[1]
544061
-
544061
-        # Detect the list of already configured services
544061
-        if not raw_database_entry:
544061
-            # If there is no database entry, database is not present in
544061
-            # the nsswitch.conf. Set the list of services to the
544061
-            # default list, if passed.
544061
-            configured_services = list(default_value)
544061
-        else:
544061
-            configured_services = raw_database_entry['value'].strip().split()
544061
-
544061
-        # Make sure no service is added if already mentioned in the list
544061
-        added_services = [s for s in services
544061
-                          if s not in configured_services]
544061
-
544061
-        # Prepend / append the list of new services
544061
-        if append:
544061
-            new_value = ' ' + ' '.join(configured_services + added_services)
544061
-        else:
544061
-            new_value = ' ' + ' '.join(added_services + configured_services)
544061
-
544061
-    else:
544061
-        # Preserve not set, let's rewrite existing configuration
544061
-        new_value = ' ' + ' '.join(services)
544061
-
544061
-    # Set new services as sources for database
544061
-    opts = [
544061
-        conf.setOption(database, new_value),
544061
-        conf.emptyLine(),
544061
-    ]
544061
-
544061
-    conf.changeConf(paths.NSSWITCH_CONF, opts)
544061
-    logger.info("Configured %s in %s", database, paths.NSSWITCH_CONF)
544061
-
544061
-
544061
 def configure_ipa_conf(
544061
         fstore, cli_basedn, cli_realm, cli_domain, cli_server, hostname):
544061
     ipaconf = IPAChangeConf("IPA Installer")
544061
@@ -948,9 +882,7 @@ def configure_sssd_conf(
544061
                 "Unable to activate the SUDO service in SSSD config.")
544061
 
544061
         sssdconfig.activate_service('sudo')
544061
-        configure_nsswitch_database(
544061
-            fstore, 'sudoers', ['sss'],
544061
-            default_value=['files'])
544061
+        tasks.enable_sssd_sudo(fstore)
544061
 
544061
     domain.add_provider('ipa', 'id')
544061
 
544061
diff --git a/ipaclient/install/ipa_client_automount.py b/ipaclient/install/ipa_client_automount.py
544061
index a1dc2a1..3a0896b 100644
544061
--- a/ipaclient/install/ipa_client_automount.py
544061
+++ b/ipaclient/install/ipa_client_automount.py
544061
@@ -41,7 +41,8 @@ from six.moves.urllib.parse import urlsplit
544061
 
544061
 # pylint: enable=import-error
544061
 from optparse import OptionParser  # pylint: disable=deprecated-module
544061
-from ipaclient.install import ipachangeconf, ipadiscovery
544061
+from ipapython import ipachangeconf
544061
+from ipaclient.install import ipadiscovery
544061
 from ipaclient.install.client import (
544061
     CLIENT_NOT_CONFIGURED,
544061
     CLIENT_ALREADY_CONFIGURED,
544061
@@ -177,44 +178,6 @@ def configure_xml(fstore):
544061
         print("Configured %s" % authconf)
544061
 
544061
 
544061
-def configure_nsswitch(statestore, options):
544061
-    """
544061
-    Point automount to ldap in nsswitch.conf.
544061
-    This function is for non-SSSD setups only.
544061
-    """
544061
-    conf = ipachangeconf.IPAChangeConf("IPA Installer")
544061
-    conf.setOptionAssignment(':')
544061
-
544061
-    with open(paths.NSSWITCH_CONF, 'r') as f:
544061
-        current_opts = conf.parse(f)
544061
-        current_nss_value = conf.findOpts(
544061
-            current_opts, name='automount', type='option'
544061
-        )[1]
544061
-        if current_nss_value is None:
544061
-            # no automount database present
544061
-            current_nss_value = False  # None cannot be backed up
544061
-        else:
544061
-            current_nss_value = current_nss_value['value']
544061
-        statestore.backup_state(
544061
-            'ipa-client-automount-nsswitch', 'previous-automount',
544061
-            current_nss_value
544061
-        )
544061
-
544061
-    nss_value = ' files ldap'
544061
-    opts = [
544061
-        {
544061
-            'name': 'automount',
544061
-            'type': 'option',
544061
-            'action': 'set',
544061
-            'value': nss_value,
544061
-        },
544061
-        {'name': 'empty', 'type': 'empty'},
544061
-    ]
544061
-    conf.changeConf(paths.NSSWITCH_CONF, opts)
544061
-
544061
-    print("Configured %s" % paths.NSSWITCH_CONF)
544061
-
544061
-
544061
 def configure_autofs_sssd(fstore, statestore, autodiscover, options):
544061
     try:
544061
         sssdconfig = SSSDConfig.SSSDConfig()
544061
@@ -339,41 +302,8 @@ def uninstall(fstore, statestore):
544061
     ]
544061
     STATES = ['autofs', 'rpcidmapd', 'rpcgssd']
544061
 
544061
-    if statestore.get_state(
544061
-        'ipa-client-automount-nsswitch', 'previous-automount'
544061
-    ) is False:
544061
-        # Previous nsswitch.conf had no automount database configured
544061
-        # so remove it.
544061
-        conf = ipachangeconf.IPAChangeConf("IPA automount installer")
544061
-        conf.setOptionAssignment(':')
544061
-        changes = [conf.rmOption('automount')]
544061
-        conf.changeConf(paths.NSSWITCH_CONF, changes)
544061
-        tasks.restore_context(paths.NSSWITCH_CONF)
544061
-        statestore.delete_state(
544061
-            'ipa-client-automount-nsswitch', 'previous-automount'
544061
-        )
544061
-    elif statestore.get_state(
544061
-        'ipa-client-automount-nsswitch', 'previous-automount'
544061
-    ) is not None:
544061
-        nss_value = statestore.get_state(
544061
-            'ipa-client-automount-nsswitch', 'previous-automount'
544061
-        )
544061
-        opts = [
544061
-            {
544061
-                'name': 'automount',
544061
-                'type': 'option',
544061
-                'action': 'set',
544061
-                'value': nss_value,
544061
-            },
544061
-            {'name': 'empty', 'type': 'empty'},
544061
-        ]
544061
-        conf = ipachangeconf.IPAChangeConf("IPA automount installer")
544061
-        conf.setOptionAssignment(':')
544061
-        conf.changeConf(paths.NSSWITCH_CONF, opts)
544061
-        tasks.restore_context(paths.NSSWITCH_CONF)
544061
-        statestore.delete_state(
544061
-            'ipa-client-automount-nsswitch', 'previous-automount'
544061
-        )
544061
+    if not statestore.get_state('autofs', 'sssd'):
544061
+        tasks.disable_ldap_automount(statestore)
544061
 
544061
     if not any(fstore.has_file(f) for f in RESTORE_FILES) or not any(
544061
         statestore.has_state(s) for s in STATES
544061
@@ -627,7 +557,7 @@ def configure_automount():
544061
 
544061
     try:
544061
         if not options.sssd:
544061
-            configure_nsswitch(statestore, options)
544061
+            tasks.enable_ldap_automount(statestore)
544061
         configure_nfs(fstore, statestore, options)
544061
         if options.sssd:
544061
             configure_autofs_sssd(fstore, statestore, autodiscover, options)
544061
diff --git a/ipaplatform/base/tasks.py b/ipaplatform/base/tasks.py
544061
index 8aa9c5c..7fd7d57 100644
544061
--- a/ipaplatform/base/tasks.py
544061
+++ b/ipaplatform/base/tasks.py
544061
@@ -32,6 +32,7 @@ from pkg_resources import parse_version
544061
 from ipaplatform.constants import constants
544061
 from ipaplatform.paths import paths
544061
 from ipapython import ipautil
544061
+from ipapython.ipachangeconf import IPAChangeConf
544061
 
544061
 logger = logging.getLogger(__name__)
544061
 
544061
@@ -337,5 +338,157 @@ class BaseTaskNamespace:
544061
         """
544061
         raise NotImplementedError
544061
 
544061
+    def configure_nsswitch_database(self, fstore, database, services,
544061
+                                    preserve=True, append=True,
544061
+                                    default_value=()):
544061
+        """
544061
+        Edits the specified nsswitch.conf database (e.g. passwd, group,
544061
+        sudoers) to use the specified service(s).
544061
+
544061
+        Arguments:
544061
+            fstore - FileStore to backup the nsswitch.conf
544061
+            database - database configuration that should be ammended,
544061
+                       e.g. 'sudoers'
544061
+            service - list of services that should be added, e.g. ['sss']
544061
+            preserve - if True, the already configured services will be
544061
+                       preserved
544061
+
544061
+        The next arguments modify the behaviour if preserve=True:
544061
+            append - if True, the services will be appended, if False,
544061
+                     prepended
544061
+            default_value - list of services that are considered as default (if
544061
+                            the database is not mentioned in nsswitch.conf),
544061
+                            e.g. ['files']
544061
+        """
544061
+
544061
+        # Backup the original version of nsswitch.conf, we're going to edit it
544061
+        # now
544061
+        if not fstore.has_file(paths.NSSWITCH_CONF):
544061
+            fstore.backup_file(paths.NSSWITCH_CONF)
544061
+
544061
+        conf = IPAChangeConf("IPA Installer")
544061
+        conf.setOptionAssignment(':')
544061
+
544061
+        if preserve:
544061
+            # Read the existing configuration
544061
+            with open(paths.NSSWITCH_CONF, 'r') as f:
544061
+                opts = conf.parse(f)
544061
+                raw_database_entry = conf.findOpts(opts, 'option', database)[1]
544061
+
544061
+            # Detect the list of already configured services
544061
+            if not raw_database_entry:
544061
+                # If there is no database entry, database is not present in
544061
+                # the nsswitch.conf. Set the list of services to the
544061
+                # default list, if passed.
544061
+                configured_services = list(default_value)
544061
+            else:
544061
+                configured_services = raw_database_entry[
544061
+                    'value'].strip().split()
544061
+
544061
+            # Make sure no service is added if already mentioned in the list
544061
+            added_services = [s for s in services
544061
+                              if s not in configured_services]
544061
+
544061
+            # Prepend / append the list of new services
544061
+            if append:
544061
+                new_value = ' ' + ' '.join(configured_services +
544061
+                                           added_services)
544061
+            else:
544061
+                new_value = ' ' + ' '.join(added_services +
544061
+                                           configured_services)
544061
+
544061
+        else:
544061
+            # Preserve not set, let's rewrite existing configuration
544061
+            new_value = ' ' + ' '.join(services)
544061
+
544061
+        # Set new services as sources for database
544061
+        opts = [
544061
+            conf.setOption(database, new_value),
544061
+            conf.emptyLine(),
544061
+        ]
544061
+
544061
+        conf.changeConf(paths.NSSWITCH_CONF, opts)
544061
+        logger.info("Configured %s in %s", database, paths.NSSWITCH_CONF)
544061
+
544061
+    def enable_sssd_sudo(self, fstore):
544061
+        """Configure nsswitch.conf to use sssd for sudo"""
544061
+        self.configure_nsswitch_database(
544061
+            fstore, 'sudoers', ['sss'],
544061
+            default_value=['files'])
544061
+
544061
+    def enable_ldap_automount(self, statestore):
544061
+        """
544061
+        Point automount to ldap in nsswitch.conf.
544061
+        This function is for non-SSSD setups only.
544061
+        """
544061
+        conf = IPAChangeConf("IPA Installer")
544061
+        conf.setOptionAssignment(':')
544061
+
544061
+        with open(paths.NSSWITCH_CONF, 'r') as f:
544061
+            current_opts = conf.parse(f)
544061
+            current_nss_value = conf.findOpts(
544061
+                current_opts, name='automount', type='option'
544061
+            )[1]
544061
+            if current_nss_value is None:
544061
+                # no automount database present
544061
+                current_nss_value = False  # None cannot be backed up
544061
+            else:
544061
+                current_nss_value = current_nss_value['value']
544061
+            statestore.backup_state(
544061
+                'ipa-client-automount-nsswitch', 'previous-automount',
544061
+                current_nss_value
544061
+            )
544061
+
544061
+        nss_value = ' files ldap'
544061
+        opts = [
544061
+            {
544061
+                'name': 'automount',
544061
+                'type': 'option',
544061
+                'action': 'set',
544061
+                'value': nss_value,
544061
+            },
544061
+            {'name': 'empty', 'type': 'empty'},
544061
+        ]
544061
+        conf.changeConf(paths.NSSWITCH_CONF, opts)
544061
+
544061
+        logger.info("Configured %s", paths.NSSWITCH_CONF)
544061
+
544061
+    def disable_ldap_automount(self, statestore):
544061
+        """Disable automount using LDAP"""
544061
+        if statestore.get_state(
544061
+            'ipa-client-automount-nsswitch', 'previous-automount'
544061
+        ) is False:
544061
+            # Previous nsswitch.conf had no automount database configured
544061
+            # so remove it.
544061
+            conf = IPAChangeConf("IPA automount installer")
544061
+            conf.setOptionAssignment(':')
544061
+            changes = [conf.rmOption('automount')]
544061
+            conf.changeConf(paths.NSSWITCH_CONF, changes)
544061
+            self.restore_context(paths.NSSWITCH_CONF)
544061
+            statestore.delete_state(
544061
+                'ipa-client-automount-nsswitch', 'previous-automount'
544061
+            )
544061
+        elif statestore.get_state(
544061
+            'ipa-client-automount-nsswitch', 'previous-automount'
544061
+        ) is not None:
544061
+            nss_value = statestore.get_state(
544061
+                'ipa-client-automount-nsswitch', 'previous-automount'
544061
+            )
544061
+            opts = [
544061
+                {
544061
+                    'name': 'automount',
544061
+                    'type': 'option',
544061
+                    'action': 'set',
544061
+                    'value': nss_value,
544061
+                },
544061
+                {'name': 'empty', 'type': 'empty'},
544061
+            ]
544061
+            conf = IPAChangeConf("IPA automount installer")
544061
+            conf.setOptionAssignment(':')
544061
+            conf.changeConf(paths.NSSWITCH_CONF, opts)
544061
+            self.restore_context(paths.NSSWITCH_CONF)
544061
+            statestore.delete_state(
544061
+                'ipa-client-automount-nsswitch', 'previous-automount'
544061
+            )
544061
 
544061
 tasks = BaseTaskNamespace()
544061
diff --git a/ipaplatform/redhat/paths.py b/ipaplatform/redhat/paths.py
544061
index 8ccd04b..15bdef6 100644
544061
--- a/ipaplatform/redhat/paths.py
544061
+++ b/ipaplatform/redhat/paths.py
544061
@@ -39,6 +39,7 @@ class RedHatPathNamespace(BasePathNamespace):
544061
     AUTHCONFIG = '/usr/sbin/authconfig'
544061
     AUTHSELECT = '/usr/bin/authselect'
544061
     SYSCONF_NETWORK = '/etc/sysconfig/network'
544061
+    NSSWITCH_CONF = '/etc/authselect/user-nsswitch.conf'
544061
 
544061
 
544061
 paths = RedHatPathNamespace()
544061
diff --git a/ipaplatform/redhat/tasks.py b/ipaplatform/redhat/tasks.py
544061
index be0b641..e18f6fa 100644
544061
--- a/ipaplatform/redhat/tasks.py
544061
+++ b/ipaplatform/redhat/tasks.py
544061
@@ -744,4 +744,23 @@ class RedHatTaskNamespace(BaseTaskNamespace):
544061
 
544061
         return filenames
544061
 
544061
+    def enable_ldap_automount(self, statestore):
544061
+        """
544061
+        Point automount to ldap in nsswitch.conf.
544061
+        This function is for non-SSSD setups only.
544061
+        """
544061
+        super(RedHatTaskNamespace, self).enable_ldap_automount(statestore)
544061
+
544061
+        authselect_cmd = [paths.AUTHSELECT, "enable-feature",
544061
+                          "with-custom-automount"]
544061
+        ipautil.run(authselect_cmd)
544061
+
544061
+    def disable_ldap_automount(self, statestore):
544061
+        """Disable ldap-based automount"""
544061
+        super(RedHatTaskNamespace, self).disable_ldap_automount(statestore)
544061
+
544061
+        authselect_cmd = [paths.AUTHSELECT, "disable-feature",
544061
+                          "with-custom-automount"]
544061
+        ipautil.run(authselect_cmd)
544061
+
544061
 tasks = RedHatTaskNamespace()
544061