Blob Blame History Raw
From abea98a9b918c0771ad10b314238b32c570f0372 Mon Sep 17 00:00:00 2001
From: François Cami <fcami@redhat.com>
Date: Aug 29 2019 06:45:12 +0000
Subject: ipatests: check that ipa-client-automount restores nsswitch.conf at uninstall time


Check that using ipa-client-install, ipa-client-automount --no-ssd, then uninstalling
both properly restores nsswitch.conf sequentially.

Related-to:: https://pagure.io/freeipa/issue/8038
Signed-off-by: François Cami <fcami@redhat.com>
Reviewed-By: Francois Cami <fcami@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Rob Critenden <rcritten@redhat.com>
Reviewed-By: François Cami <fcami@redhat.com>

---

#diff --git a/ipatests/prci_definitions/nightly_ipa-4-8.yaml b/ipatests/prci_definitions/nightly_ipa-4-8.yaml
#index ef5d2c6..f39e4b4 100644
#--- a/ipatests/prci_definitions/nightly_ipa-4-8.yaml
#+++ b/ipatests/prci_definitions/nightly_ipa-4-8.yaml
#@@ -1257,6 +1257,18 @@ jobs:
#         timeout: 9000
#         topology: *master_3client
# 
#+  fedora-30/nfs_nsswitch_restore:
#+    requires: [fedora-30/build]
#+    priority: 50
#+    job:
#+      class: RunPytest
#+      args:
#+        build_url: '{fedora-30/build_url}'
#+        test_suite: test_integration/test_nfs.py::TestIpaClientAutomountFileRestore
#+        template: *ci-master-f30
#+        timeout: 3600
#+        topology: *master_3client
#+
#   fedora-30/mask:
#     requires: [fedora-30/build]
#     priority: 50
diff --git a/ipatests/test_integration/test_nfs.py b/ipatests/test_integration/test_nfs.py
index adfc19f..0e1ef6a 100644
--- a/ipatests/test_integration/test_nfs.py
+++ b/ipatests/test_integration/test_nfs.py
@@ -15,6 +15,7 @@
 
 from __future__ import absolute_import
 
+import pytest
 import os
 import re
 import time
@@ -258,3 +259,74 @@ class TestNFS(IntegrationTest):
         time.sleep(WAIT_AFTER_UNINSTALL)
 
         self.cleanup()
+
+
+class TestIpaClientAutomountFileRestore(IntegrationTest):
+
+    num_clients = 1
+    topology = 'line'
+
+    @classmethod
+    def install(cls, mh):
+        tasks.install_master(cls.master, setup_dns=True)
+
+    def teardown_method(self, method):
+        tasks.uninstall_client(self.clients[0])
+
+    def nsswitch_backup_restore(
+        self,
+        no_sssd=False,
+    ):
+
+        # In order to get a more pure sum, one that ignores the Generated
+        # header and any white space we have to do a bit of work...
+        sha256nsswitch_cmd = \
+            'egrep -v "Generated|^$" /etc/nsswitch.conf | sed "s/\\s//g" ' \
+            '| sort | sha256sum'
+
+        cmd = self.clients[0].run_command(sha256nsswitch_cmd)
+        orig_sha256 = cmd.stdout_text
+
+        grep_automount_command = \
+            "grep automount /etc/nsswitch.conf | cut -d: -f2"
+
+        tasks.install_client(self.master, self.clients[0])
+        cmd = self.clients[0].run_command(grep_automount_command)
+        after_ipa_client_install = cmd.stdout_text.split()
+
+        if no_sssd:
+            ipa_client_automount_command = [
+                "ipa-client-automount", "--no-sssd", "-U"
+            ]
+        else:
+            ipa_client_automount_command = [
+                "ipa-client-automount", "-U"
+            ]
+        self.clients[0].run_command(ipa_client_automount_command)
+        cmd = self.clients[0].run_command(grep_automount_command)
+        after_ipa_client_automount = cmd.stdout_text.split()
+        if no_sssd:
+            assert after_ipa_client_automount == ['files', 'ldap']
+        else:
+            assert after_ipa_client_automount == ['sss', 'files']
+
+        cmd = self.clients[0].run_command(grep_automount_command)
+        assert cmd.stdout_text.split() == after_ipa_client_automount
+
+        self.clients[0].run_command([
+            "ipa-client-automount", "--uninstall", "-U"
+        ])
+
+        cmd = self.clients[0].run_command(grep_automount_command)
+        assert cmd.stdout_text.split() == after_ipa_client_install
+
+        tasks.uninstall_client(self.clients[0])
+        cmd = self.clients[0].run_command(sha256nsswitch_cmd)
+        assert cmd.stdout_text == orig_sha256
+
+    @pytest.mark.xfail(reason='freeipa ticket 8054', strict=True)
+    def test_nsswitch_backup_restore_sssd(self):
+        self.nsswitch_backup_restore()
+
+    def test_nsswitch_backup_restore_no_sssd(self):
+        self.nsswitch_backup_restore(no_sssd=True)

From 2f0afeda6e66fcca5c184a4036112fcd315f2f6e Mon Sep 17 00:00:00 2001
From: François Cami <fcami@redhat.com>
Date: Aug 29 2019 06:45:12 +0000
Subject: ipa-client-automount: always restore nsswitch.conf at uninstall time


ipa-client-automount used to only restore nsswitch.conf when sssd was not
used. However authselect's default profile is now sssd so always restore
nsswitch.conf's automount configuration to 'files sssd'.
Note that the behavior seen before commit:
a0e846f56c8de3b549d1d284087131da13135e34
would always restore nsswitch.conf to the previous state which in some cases
was wrong.

Fixes: https://pagure.io/freeipa/issue/8038
Signed-off-by: François Cami <fcami@redhat.com>
Reviewed-By: Francois Cami <fcami@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Rob Critenden <rcritten@redhat.com>
Reviewed-By: François Cami <fcami@redhat.com>

---

diff --git a/ipaclient/install/ipa_client_automount.py b/ipaclient/install/ipa_client_automount.py
index fa07598..a1dc2a1 100644
--- a/ipaclient/install/ipa_client_automount.py
+++ b/ipaclient/install/ipa_client_automount.py
@@ -177,18 +177,30 @@ def configure_xml(fstore):
         print("Configured %s" % authconf)
 
 
-def configure_nsswitch(fstore, options):
+def configure_nsswitch(statestore, options):
     """
-    Point automount to ldap in nsswitch.conf. This function is for non-SSSD
-    setups only
+    Point automount to ldap in nsswitch.conf.
+    This function is for non-SSSD setups only.
     """
-    fstore.backup_file(paths.NSSWITCH_CONF)
-
     conf = ipachangeconf.IPAChangeConf("IPA Installer")
     conf.setOptionAssignment(':')
 
-    nss_value = ' files ldap'
+    with open(paths.NSSWITCH_CONF, 'r') as f:
+        current_opts = conf.parse(f)
+        current_nss_value = conf.findOpts(
+            current_opts, name='automount', type='option'
+        )[1]
+        if current_nss_value is None:
+            # no automount database present
+            current_nss_value = False  # None cannot be backed up
+        else:
+            current_nss_value = current_nss_value['value']
+        statestore.backup_state(
+            'ipa-client-automount-nsswitch', 'previous-automount',
+            current_nss_value
+        )
 
+    nss_value = ' files ldap'
     opts = [
         {
             'name': 'automount',
@@ -198,7 +210,6 @@ def configure_nsswitch(fstore, options):
         },
         {'name': 'empty', 'type': 'empty'},
     ]
-
     conf.changeConf(paths.NSSWITCH_CONF, opts)
 
     print("Configured %s" % paths.NSSWITCH_CONF)
@@ -322,19 +333,47 @@ def configure_autofs_common(fstore, statestore, options):
 def uninstall(fstore, statestore):
     RESTORE_FILES = [
         paths.SYSCONFIG_AUTOFS,
-        paths.NSSWITCH_CONF,
         paths.AUTOFS_LDAP_AUTH_CONF,
         paths.SYSCONFIG_NFS,
         paths.IDMAPD_CONF,
     ]
     STATES = ['autofs', 'rpcidmapd', 'rpcgssd']
 
-    # automount only touches /etc/nsswitch.conf if LDAP is
-    # used. Don't restore it otherwise.
-    if statestore.get_state('authconfig', 'sssd') or (
-        statestore.get_state('authselect', 'profile') == 'sssd'
-    ):
-        RESTORE_FILES.remove(paths.NSSWITCH_CONF)
+    if statestore.get_state(
+        'ipa-client-automount-nsswitch', 'previous-automount'
+    ) is False:
+        # Previous nsswitch.conf had no automount database configured
+        # so remove it.
+        conf = ipachangeconf.IPAChangeConf("IPA automount installer")
+        conf.setOptionAssignment(':')
+        changes = [conf.rmOption('automount')]
+        conf.changeConf(paths.NSSWITCH_CONF, changes)
+        tasks.restore_context(paths.NSSWITCH_CONF)
+        statestore.delete_state(
+            'ipa-client-automount-nsswitch', 'previous-automount'
+        )
+    elif statestore.get_state(
+        'ipa-client-automount-nsswitch', 'previous-automount'
+    ) is not None:
+        nss_value = statestore.get_state(
+            'ipa-client-automount-nsswitch', 'previous-automount'
+        )
+        opts = [
+            {
+                'name': 'automount',
+                'type': 'option',
+                'action': 'set',
+                'value': nss_value,
+            },
+            {'name': 'empty', 'type': 'empty'},
+        ]
+        conf = ipachangeconf.IPAChangeConf("IPA automount installer")
+        conf.setOptionAssignment(':')
+        conf.changeConf(paths.NSSWITCH_CONF, opts)
+        tasks.restore_context(paths.NSSWITCH_CONF)
+        statestore.delete_state(
+            'ipa-client-automount-nsswitch', 'previous-automount'
+        )
 
     if not any(fstore.has_file(f) for f in RESTORE_FILES) or not any(
         statestore.has_state(s) for s in STATES
@@ -588,7 +627,7 @@ def configure_automount():
 
     try:
         if not options.sssd:
-            configure_nsswitch(fstore, options)
+            configure_nsswitch(statestore, options)
         configure_nfs(fstore, statestore, options)
         if options.sssd:
             configure_autofs_sssd(fstore, statestore, autodiscover, options)

From 6e92776bfc199e9ca92e11ef3315dcecad3c9307 Mon Sep 17 00:00:00 2001
From: Rob Critenden <rcritten@redhat.com>
Date: Aug 29 2019 06:45:12 +0000
Subject: Move ipachangeconf from ipaclient.install to ipapython


This will let us call it from ipaplatform.

Mark the original location as deprecated.

Reviewed-By: Francois Cami <fcami@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Rob Critenden <rcritten@redhat.com>
Reviewed-By: François Cami <fcami@redhat.com>

---

diff --git a/install/tools/ipa-replica-conncheck.in b/install/tools/ipa-replica-conncheck.in
index 9208076..b22db11 100644
--- a/install/tools/ipa-replica-conncheck.in
+++ b/install/tools/ipa-replica-conncheck.in
@@ -22,7 +22,7 @@ from __future__ import print_function
 
 import logging
 
-import ipaclient.install.ipachangeconf
+from ipapython import ipachangeconf
 from ipapython.config import IPAOptionParser
 from ipapython.dn import DN
 from ipapython import version
@@ -229,7 +229,7 @@ def sigterm_handler(signum, frame):
 
 def configure_krb5_conf(realm, kdc, filename):
 
-    krbconf = ipaclient.install.ipachangeconf.IPAChangeConf("IPA Installer")
+    krbconf = ipachangeconf.IPAChangeConf("IPA Installer")
     krbconf.setOptionAssignment((" = ", " "))
     krbconf.setSectionNameDelimiters(("[","]"))
     krbconf.setSubSectionDelimiters(("{","}"))
diff --git a/ipaclient/install/ipachangeconf.py b/ipaclient/install/ipachangeconf.py
index a13e0ea..c51e42e 100644
--- a/ipaclient/install/ipachangeconf.py
+++ b/ipaclient/install/ipachangeconf.py
@@ -18,566 +18,18 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-import fcntl
-import logging
-import os
-import shutil
+import warnings
+from ipapython.ipachangeconf import IPAChangeConf as realIPAChangeConf
 
-import six
 
-if six.PY3:
-    unicode = str
+class IPAChangeConf(realIPAChangeConf):
+    """Advertise the old name"""
 
-logger = logging.getLogger(__name__)
-
-def openLocked(filename, perms):
-    fd = -1
-    try:
-        fd = os.open(filename, os.O_RDWR | os.O_CREAT, perms)
-
-        fcntl.lockf(fd, fcntl.LOCK_EX)
-    except OSError as e:
-        if fd != -1:
-            try:
-                os.close(fd)
-            except OSError:
-                pass
-        raise IOError(e.errno, e.strerror)
-    return os.fdopen(fd, "r+")
-
-
-    #TODO: add subsection as a concept
-    #      (ex. REALM.NAME = { foo = x bar = y } )
-    #TODO: put section delimiters as separating element of the list
-    #      so that we can process multiple sections in one go
-    #TODO: add a comment all but provided options as a section option
-class IPAChangeConf:
     def __init__(self, name):
-        self.progname = name
-        self.indent = ("", "", "")
-        self.assign = (" = ", "=")
-        self.dassign = self.assign[0]
-        self.comment = ("#",)
-        self.dcomment = self.comment[0]
-        self.eol = ("\n",)
-        self.deol = self.eol[0]
-        self.sectnamdel = ("[", "]")
-        self.subsectdel = ("{", "}")
-        self.case_insensitive_sections = True
-
-    def setProgName(self, name):
-        self.progname = name
-
-    def setIndent(self, indent):
-        if type(indent) is tuple:
-            self.indent = indent
-        elif type(indent) is str:
-            self.indent = (indent, )
-        else:
-            raise ValueError('Indent must be a list of strings')
-
-    def setOptionAssignment(self, assign):
-        if type(assign) is tuple:
-            self.assign = assign
-        else:
-            self.assign = (assign, )
-        self.dassign = self.assign[0]
-
-    def setCommentPrefix(self, comment):
-        if type(comment) is tuple:
-            self.comment = comment
-        else:
-            self.comment = (comment, )
-        self.dcomment = self.comment[0]
-
-    def setEndLine(self, eol):
-        if type(eol) is tuple:
-            self.eol = eol
-        else:
-            self.eol = (eol, )
-        self.deol = self.eol[0]
-
-    def setSectionNameDelimiters(self, delims):
-        self.sectnamdel = delims
-
-    def setSubSectionDelimiters(self, delims):
-        self.subsectdel = delims
-
-    def matchComment(self, line):
-        for v in self.comment:
-            if line.lstrip().startswith(v):
-                return line.lstrip()[len(v):]
-        return False
-
-    def matchEmpty(self, line):
-        if line.strip() == "":
-            return True
-        return False
-
-    def matchSection(self, line):
-        cl = "".join(line.strip().split())
-        cl = cl.lower() if self.case_insensitive_sections else cl
-
-        if len(self.sectnamdel) != 2:
-            return False
-        if not cl.startswith(self.sectnamdel[0]):
-            return False
-        if not cl.endswith(self.sectnamdel[1]):
-            return False
-        return cl[len(self.sectnamdel[0]):-len(self.sectnamdel[1])]
-
-    def matchSubSection(self, line):
-        if self.matchComment(line):
-            return False
-
-        parts = line.split(self.dassign, 1)
-        if len(parts) < 2:
-            return False
-
-        if parts[1].strip() == self.subsectdel[0]:
-            return parts[0].strip()
-
-        return False
-
-    def matchSubSectionEnd(self, line):
-        if self.matchComment(line):
-            return False
-
-        if line.strip() == self.subsectdel[1]:
-            return True
-
-        return False
-
-    def getSectionLine(self, section):
-        if len(self.sectnamdel) != 2:
-            return section
-        return self._dump_line(self.sectnamdel[0],
-                               section,
-                               self.sectnamdel[1],
-                               self.deol)
-
-    def _dump_line(self, *args):
-        return u"".join(unicode(x) for x in args)
-
-    def dump(self, options, level=0):
-        output = []
-        if level >= len(self.indent):
-            level = len(self.indent) - 1
-
-        for o in options:
-            if o['type'] == "section":
-                output.append(self._dump_line(self.sectnamdel[0],
-                                              o['name'],
-                                              self.sectnamdel[1]))
-                output.append(self.dump(o['value'], (level + 1)))
-                continue
-            if o['type'] == "subsection":
-                output.append(self._dump_line(self.indent[level],
-                                              o['name'],
-                                              self.dassign,
-                                              self.subsectdel[0]))
-                output.append(self.dump(o['value'], (level + 1)))
-                output.append(self._dump_line(self.indent[level],
-                                              self.subsectdel[1]))
-                continue
-            if o['type'] == "option":
-                delim = o.get('delim', self.dassign)
-                if delim not in self.assign:
-                    raise ValueError('Unknown delim "%s" must be one of "%s"' % (delim, " ".join([d for d in self.assign])))
-                output.append(self._dump_line(self.indent[level],
-                                              o['name'],
-                                              delim,
-                                              o['value']))
-                continue
-            if o['type'] == "comment":
-                output.append(self._dump_line(self.dcomment, o['value']))
-                continue
-            if o['type'] == "empty":
-                output.append('')
-                continue
-            raise SyntaxError('Unknown type: [%s]' % o['type'])
-
-        # append an empty string to the output so that we add eol to the end
-        # of the file contents in a single join()
-        output.append('')
-        return self.deol.join(output)
-
-    def parseLine(self, line):
-
-        if self.matchEmpty(line):
-            return {'name': 'empty', 'type': 'empty'}
-
-        value = self.matchComment(line)
-        if value:
-            return {'name': 'comment',
-                    'type': 'comment',
-                    'value': value.rstrip()}  # pylint: disable=E1103
-
-        o = dict()
-        parts = line.split(self.dassign, 1)
-        if len(parts) < 2:
-            # The default assign didn't match, try the non-default
-            for d in self.assign[1:]:
-                parts = line.split(d, 1)
-                if len(parts) >= 2:
-                    o['delim'] = d
-                    break
-
-            if 'delim' not in o:
-                raise SyntaxError('Syntax Error: Unknown line format')
-
-        o.update({'name':parts[0].strip(), 'type':'option', 'value':parts[1].rstrip()})
-        return o
-
-    def findOpts(self, opts, type, name, exclude_sections=False):
-
-        num = 0
-        for o in opts:
-            if o['type'] == type and o['name'] == name:
-                return (num, o)
-            if exclude_sections and (o['type'] == "section" or
-                                     o['type'] == "subsection"):
-                return (num, None)
-            num += 1
-        return (num, None)
-
-    def commentOpts(self, inopts, level=0):
-
-        opts = []
-
-        if level >= len(self.indent):
-            level = len(self.indent) - 1
-
-        for o in inopts:
-            if o['type'] == 'section':
-                no = self.commentOpts(o['value'], (level + 1))
-                val = self._dump_line(self.dcomment,
-                                      self.sectnamdel[0],
-                                      o['name'],
-                                      self.sectnamdel[1])
-                opts.append({'name': 'comment',
-                             'type': 'comment',
-                             'value': val})
-                for n in no:
-                    opts.append(n)
-                continue
-            if o['type'] == 'subsection':
-                no = self.commentOpts(o['value'], (level + 1))
-                val = self._dump_line(self.indent[level],
-                                      o['name'],
-                                      self.dassign,
-                                      self.subsectdel[0])
-                opts.append({'name': 'comment',
-                             'type': 'comment',
-                             'value': val})
-                opts.extend(no)
-                val = self._dump_line(self.indent[level], self.subsectdel[1])
-                opts.append({'name': 'comment',
-                             'type': 'comment',
-                             'value': val})
-                continue
-            if o['type'] == 'option':
-                delim = o.get('delim', self.dassign)
-                if delim not in self.assign:
-                    val = self._dump_line(self.indent[level],
-                                          o['name'],
-                                          delim,
-                                          o['value'])
-                opts.append({'name':'comment', 'type':'comment', 'value':val})
-                continue
-            if o['type'] == 'comment':
-                opts.append(o)
-                continue
-            if o['type'] == 'empty':
-                opts.append({'name': 'comment',
-                             'type': 'comment',
-                             'value': ''})
-                continue
-            raise SyntaxError('Unknown type: [%s]' % o['type'])
-
-        return opts
-
-    def mergeOld(self, oldopts, newopts):
-
-        opts = []
-
-        for o in oldopts:
-            if o['type'] == "section" or o['type'] == "subsection":
-                _num, no = self.findOpts(newopts, o['type'], o['name'])
-                if not no:
-                    opts.append(o)
-                    continue
-                if no['action'] == "set":
-                    mo = self.mergeOld(o['value'], no['value'])
-                    opts.append({'name': o['name'],
-                                 'type': o['type'],
-                                 'value': mo})
-                    continue
-                if no['action'] == "comment":
-                    co = self.commentOpts(o['value'])
-                    for c in co:
-                        opts.append(c)
-                    continue
-                if no['action'] == "remove":
-                    continue
-                raise SyntaxError('Unknown action: [%s]' % no['action'])
-
-            if o['type'] == "comment" or o['type'] == "empty":
-                opts.append(o)
-                continue
-
-            if o['type'] == "option":
-                _num, no = self.findOpts(newopts, 'option', o['name'], True)
-                if not no:
-                    opts.append(o)
-                    continue
-                if no['action'] == 'comment' or no['action'] == 'remove':
-                    if (no['value'] is not None and
-                            o['value'] is not no['value']):
-                        opts.append(o)
-                        continue
-                    if no['action'] == 'comment':
-                        value = self._dump_line(self.dcomment,
-                                                o['name'],
-                                                self.dassign,
-                                                o['value'])
-                        opts.append({'name': 'comment',
-                                     'type': 'comment',
-                                     'value': value})
-                    continue
-                if no['action'] == 'set':
-                    opts.append(no)
-                    continue
-                if no['action'] == 'addifnotset':
-                    opts.append({
-                        'name': 'comment',
-                        'type': 'comment',
-                        'value': self._dump_line(
-                            ' ', no['name'], ' modified by IPA'
-                        ),
-                    })
-                    opts.append({'name': 'comment', 'type': 'comment',
-                                'value': self._dump_line(no['name'],
-                                                         self.dassign,
-                                                         no['value'],
-                                                         )})
-                    opts.append(o)
-                    continue
-                raise SyntaxError('Unknown action: [%s]' % no['action'])
-
-            raise SyntaxError('Unknown type: [%s]' % o['type'])
-
-        return opts
-
-    def mergeNew(self, opts, newopts):
-
-        cline = 0
-
-        for no in newopts:
-
-            if no['type'] == "section" or no['type'] == "subsection":
-                (num, o) = self.findOpts(opts, no['type'], no['name'])
-                if not o:
-                    if no['action'] == 'set':
-                        opts.append(no)
-                    continue
-                if no['action'] == "set":
-                    self.mergeNew(o['value'], no['value'])
-                    continue
-                cline = num + 1
-                continue
-
-            if no['type'] == "option":
-                (num, o) = self.findOpts(opts, no['type'], no['name'], True)
-                if not o:
-                    if no['action'] == 'set' or no['action'] == 'addifnotset':
-                        opts.append(no)
-                    continue
-                cline = num + 1
-                continue
-
-            if no['type'] == "comment" or no['type'] == "empty":
-                opts.insert(cline, no)
-                cline += 1
-                continue
-
-            raise SyntaxError('Unknown type: [%s]' % no['type'])
-
-    def merge(self, oldopts, newopts):
-        """
-        Uses a two pass strategy:
-        First we create a new opts tree from oldopts removing/commenting
-          the options as indicated by the contents of newopts
-        Second we fill in the new opts tree with options as indicated
-          in the newopts tree (this is becaus eentire (sub)sections may
-          in the newopts tree (this is becaus entire (sub)sections may
-          exist in the newopts that do not exist in oldopts)
-        """
-        opts = self.mergeOld(oldopts, newopts)
-        self.mergeNew(opts, newopts)
-        return opts
-
-    #TODO: Make parse() recursive?
-    def parse(self, f):
-
-        opts = []
-        sectopts = []
-        section = None
-        subsectopts = []
-        subsection = None
-        curopts = opts
-        fatheropts = opts
-
-        # Read in the old file.
-        for line in f:
-
-            # It's a section start.
-            value = self.matchSection(line)
-            if value:
-                if section is not None:
-                    opts.append({'name': section,
-                                 'type': 'section',
-                                 'value': sectopts})
-                sectopts = []
-                curopts = sectopts
-                fatheropts = sectopts
-                section = value
-                continue
-
-            # It's a subsection start.
-            value = self.matchSubSection(line)
-            if value:
-                if subsection is not None:
-                    raise SyntaxError('nested subsections are not '
-                                      'supported yet')
-                subsectopts = []
-                curopts = subsectopts
-                subsection = value
-                continue
-
-            value = self.matchSubSectionEnd(line)
-            if value:
-                if subsection is None:
-                    raise SyntaxError('Unmatched end subsection terminator '
-                                      'found')
-                fatheropts.append({'name': subsection,
-                                   'type': 'subsection',
-                                   'value': subsectopts})
-                subsection = None
-                curopts = fatheropts
-                continue
-
-            # Copy anything else as is.
-            try:
-                curopts.append(self.parseLine(line))
-            except SyntaxError as e:
-                raise SyntaxError('{error} in file {fname}: [{line}]'.format(
-                    error=e, fname=f.name, line=line.rstrip()))
-
-        #Add last section if any
-        if len(sectopts) is not 0:
-            opts.append({'name': section,
-                         'type': 'section',
-                         'value': sectopts})
-
-        return opts
-
-    def changeConf(self, file, newopts):
-        """
-        Write settings to configuration file
-        :param file: path to the file
-        :param options: set of dictionaries in the form:
-             {'name': 'foo', 'value': 'bar', 'action': 'set/comment'}
-        :param section: section name like 'global'
-        """
-        output = ""
-        f = None
-        try:
-            # Do not catch an unexisting file error
-            # we want to fail in that case
-            shutil.copy2(file, (file + ".ipabkp"))
-
-            f = openLocked(file, 0o644)
-
-            oldopts = self.parse(f)
-
-            options = self.merge(oldopts, newopts)
-
-            output = self.dump(options)
-
-            # Write it out and close it.
-            f.seek(0)
-            f.truncate(0)
-            f.write(output)
-        finally:
-            try:
-                if f:
-                    f.close()
-            except IOError:
-                pass
-        logger.debug("Updating configuration file %s", file)
-        logger.debug(output)
-        return True
-
-    def newConf(self, file, options, file_perms=0o644):
-        """"
-        Write settings to a new file, backup the old
-        :param file: path to the file
-        :param options: a set of dictionaries in the form:
-             {'name': 'foo', 'value': 'bar', 'action': 'set/comment'}
-        :param file_perms: number defining the new file's permissions
-        """
-        output = ""
-        f = None
-        try:
-            try:
-                shutil.copy2(file, (file + ".ipabkp"))
-            except IOError as err:
-                if err.errno == 2:
-                    # The orign file did not exist
-                    pass
-
-            f = openLocked(file, file_perms)
-
-            # Trunkate
-            f.seek(0)
-            f.truncate(0)
-
-            output = self.dump(options)
-
-            f.write(output)
-        finally:
-            try:
-                if f:
-                    f.close()
-            except IOError:
-                pass
-        logger.debug("Writing configuration file %s", file)
-        logger.debug(output)
-        return True
-
-    @staticmethod
-    def setOption(name, value):
-        return {'name': name,
-                'type': 'option',
-                'action': 'set',
-                'value': value}
-
-    @staticmethod
-    def rmOption(name):
-        return {'name': name,
-                'type': 'option',
-                'action': 'remove',
-                'value': None}
-
-    @staticmethod
-    def setSection(name, options):
-        return {'name': name,
-                'type': 'section',
-                'action': 'set',
-                'value': options}
-
-    @staticmethod
-    def emptyLine():
-        return {'name': 'empty',
-                'type': 'empty'}
+        """something"""
+        warnings.warn(
+            "Use 'ipapython.ipachangeconf.IPAChangeConfg'",
+            DeprecationWarning,
+            stacklevel=2
+        )
+        super(IPAChangeConf, self).__init__(name)
diff --git a/ipapython/ipachangeconf.py b/ipapython/ipachangeconf.py
new file mode 100644
index 0000000..cfb4a6e
--- /dev/null
+++ b/ipapython/ipachangeconf.py
@@ -0,0 +1,590 @@
+#
+# ipachangeconf - configuration file manipulation classes and functions
+# partially based on authconfig code
+# Copyright (c) 1999-2007 Red Hat, Inc.
+# Author: Simo Sorce <ssorce@redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import fcntl
+import logging
+import os
+import shutil
+
+import six
+
+if six.PY3:
+    unicode = str
+
+logger = logging.getLogger(__name__)
+
+
+def openLocked(filename, perms):
+    fd = -1
+    try:
+        fd = os.open(filename, os.O_RDWR | os.O_CREAT, perms)
+
+        fcntl.lockf(fd, fcntl.LOCK_EX)
+    except OSError as e:
+        if fd != -1:
+            try:
+                os.close(fd)
+            except OSError:
+                pass
+        raise IOError(e.errno, e.strerror)
+    return os.fdopen(fd, "r+")
+
+    # TODO: add subsection as a concept
+    #       (ex. REALM.NAME = { foo = x bar = y } )
+    # TODO: put section delimiters as separating element of the list
+    #       so that we can process multiple sections in one go
+    # TODO: add a comment all but provided options as a section option
+
+
+class IPAChangeConf:
+    def __init__(self, name):
+        self.progname = name
+        self.indent = ("", "", "")
+        self.assign = (" = ", "=")
+        self.dassign = self.assign[0]
+        self.comment = ("#",)
+        self.dcomment = self.comment[0]
+        self.eol = ("\n",)
+        self.deol = self.eol[0]
+        self.sectnamdel = ("[", "]")
+        self.subsectdel = ("{", "}")
+        self.case_insensitive_sections = True
+
+    def setProgName(self, name):
+        self.progname = name
+
+    def setIndent(self, indent):
+        if type(indent) is tuple:
+            self.indent = indent
+        elif type(indent) is str:
+            self.indent = (indent, )
+        else:
+            raise ValueError('Indent must be a list of strings')
+
+    def setOptionAssignment(self, assign):
+        if type(assign) is tuple:
+            self.assign = assign
+        else:
+            self.assign = (assign, )
+        self.dassign = self.assign[0]
+
+    def setCommentPrefix(self, comment):
+        if type(comment) is tuple:
+            self.comment = comment
+        else:
+            self.comment = (comment, )
+        self.dcomment = self.comment[0]
+
+    def setEndLine(self, eol):
+        if type(eol) is tuple:
+            self.eol = eol
+        else:
+            self.eol = (eol, )
+        self.deol = self.eol[0]
+
+    def setSectionNameDelimiters(self, delims):
+        self.sectnamdel = delims
+
+    def setSubSectionDelimiters(self, delims):
+        self.subsectdel = delims
+
+    def matchComment(self, line):
+        for v in self.comment:
+            if line.lstrip().startswith(v):
+                return line.lstrip()[len(v):]
+        return False
+
+    def matchEmpty(self, line):
+        if line.strip() == "":
+            return True
+        return False
+
+    def matchSection(self, line):
+        cl = "".join(line.strip().split())
+        cl = cl.lower() if self.case_insensitive_sections else cl
+
+        if len(self.sectnamdel) != 2:
+            return False
+        if not cl.startswith(self.sectnamdel[0]):
+            return False
+        if not cl.endswith(self.sectnamdel[1]):
+            return False
+        return cl[len(self.sectnamdel[0]):-len(self.sectnamdel[1])]
+
+    def matchSubSection(self, line):
+        if self.matchComment(line):
+            return False
+
+        parts = line.split(self.dassign, 1)
+        if len(parts) < 2:
+            return False
+
+        if parts[1].strip() == self.subsectdel[0]:
+            return parts[0].strip()
+
+        return False
+
+    def matchSubSectionEnd(self, line):
+        if self.matchComment(line):
+            return False
+
+        if line.strip() == self.subsectdel[1]:
+            return True
+
+        return False
+
+    def getSectionLine(self, section):
+        if len(self.sectnamdel) != 2:
+            return section
+        return self._dump_line(self.sectnamdel[0],
+                               section,
+                               self.sectnamdel[1],
+                               self.deol)
+
+    def _dump_line(self, *args):
+        return u"".join(unicode(x) for x in args)
+
+    def dump(self, options, level=0):
+        output = []
+        if level >= len(self.indent):
+            level = len(self.indent) - 1
+
+        for o in options:
+            if o['type'] == "section":
+                output.append(self._dump_line(self.sectnamdel[0],
+                                              o['name'],
+                                              self.sectnamdel[1]))
+                output.append(self.dump(o['value'], (level + 1)))
+                continue
+            if o['type'] == "subsection":
+                output.append(self._dump_line(self.indent[level],
+                                              o['name'],
+                                              self.dassign,
+                                              self.subsectdel[0]))
+                output.append(self.dump(o['value'], (level + 1)))
+                output.append(self._dump_line(self.indent[level],
+                                              self.subsectdel[1]))
+                continue
+            if o['type'] == "option":
+                delim = o.get('delim', self.dassign)
+                if delim not in self.assign:
+                    raise ValueError(
+                        'Unknown delim "%s" must be one of "%s"' %
+                        (delim, " ".join([d for d in self.assign]))
+                    )
+                output.append(self._dump_line(self.indent[level],
+                                              o['name'],
+                                              delim,
+                                              o['value']))
+                continue
+            if o['type'] == "comment":
+                output.append(self._dump_line(self.dcomment, o['value']))
+                continue
+            if o['type'] == "empty":
+                output.append('')
+                continue
+            raise SyntaxError('Unknown type: [%s]' % o['type'])
+
+        # append an empty string to the output so that we add eol to the end
+        # of the file contents in a single join()
+        output.append('')
+        return self.deol.join(output)
+
+    def parseLine(self, line):
+
+        if self.matchEmpty(line):
+            return {'name': 'empty', 'type': 'empty'}
+
+        value = self.matchComment(line)
+        if value:
+            return {'name': 'comment',
+                    'type': 'comment',
+                    'value': value.rstrip()}  # pylint: disable=E1103
+
+        o = dict()
+        parts = line.split(self.dassign, 1)
+        if len(parts) < 2:
+            # The default assign didn't match, try the non-default
+            for d in self.assign[1:]:
+                parts = line.split(d, 1)
+                if len(parts) >= 2:
+                    o['delim'] = d
+                    break
+
+            if 'delim' not in o:
+                raise SyntaxError('Syntax Error: Unknown line format')
+
+        o.update({'name': parts[0].strip(), 'type': 'option',
+                  'value': parts[1].rstrip()})
+        return o
+
+    def findOpts(self, opts, type, name, exclude_sections=False):
+
+        num = 0
+        for o in opts:
+            if o['type'] == type and o['name'] == name:
+                return (num, o)
+            if exclude_sections and (o['type'] == "section" or
+                                     o['type'] == "subsection"):
+                return (num, None)
+            num += 1
+        return (num, None)
+
+    def commentOpts(self, inopts, level=0):
+
+        opts = []
+
+        if level >= len(self.indent):
+            level = len(self.indent) - 1
+
+        for o in inopts:
+            if o['type'] == 'section':
+                no = self.commentOpts(o['value'], (level + 1))
+                val = self._dump_line(self.dcomment,
+                                      self.sectnamdel[0],
+                                      o['name'],
+                                      self.sectnamdel[1])
+                opts.append({'name': 'comment',
+                             'type': 'comment',
+                             'value': val})
+                for n in no:
+                    opts.append(n)
+                continue
+            if o['type'] == 'subsection':
+                no = self.commentOpts(o['value'], (level + 1))
+                val = self._dump_line(self.indent[level],
+                                      o['name'],
+                                      self.dassign,
+                                      self.subsectdel[0])
+                opts.append({'name': 'comment',
+                             'type': 'comment',
+                             'value': val})
+                opts.extend(no)
+                val = self._dump_line(self.indent[level], self.subsectdel[1])
+                opts.append({'name': 'comment',
+                             'type': 'comment',
+                             'value': val})
+                continue
+            if o['type'] == 'option':
+                delim = o.get('delim', self.dassign)
+                if delim not in self.assign:
+                    val = self._dump_line(self.indent[level],
+                                          o['name'],
+                                          delim,
+                                          o['value'])
+                opts.append({'name': 'comment', 'type': 'comment',
+                             'value': val})
+                continue
+            if o['type'] == 'comment':
+                opts.append(o)
+                continue
+            if o['type'] == 'empty':
+                opts.append({'name': 'comment',
+                             'type': 'comment',
+                             'value': ''})
+                continue
+            raise SyntaxError('Unknown type: [%s]' % o['type'])
+
+        return opts
+
+    def mergeOld(self, oldopts, newopts):
+
+        opts = []
+
+        for o in oldopts:
+            if o['type'] == "section" or o['type'] == "subsection":
+                _num, no = self.findOpts(newopts, o['type'], o['name'])
+                if not no:
+                    opts.append(o)
+                    continue
+                if no['action'] == "set":
+                    mo = self.mergeOld(o['value'], no['value'])
+                    opts.append({'name': o['name'],
+                                 'type': o['type'],
+                                 'value': mo})
+                    continue
+                if no['action'] == "comment":
+                    co = self.commentOpts(o['value'])
+                    for c in co:
+                        opts.append(c)
+                    continue
+                if no['action'] == "remove":
+                    continue
+                raise SyntaxError('Unknown action: [%s]' % no['action'])
+
+            if o['type'] == "comment" or o['type'] == "empty":
+                opts.append(o)
+                continue
+
+            if o['type'] == "option":
+                _num, no = self.findOpts(newopts, 'option', o['name'], True)
+                if not no:
+                    opts.append(o)
+                    continue
+                if no['action'] == 'comment' or no['action'] == 'remove':
+                    if (no['value'] is not None and
+                            o['value'] is not no['value']):
+                        opts.append(o)
+                        continue
+                    if no['action'] == 'comment':
+                        value = self._dump_line(self.dcomment,
+                                                o['name'],
+                                                self.dassign,
+                                                o['value'])
+                        opts.append({'name': 'comment',
+                                     'type': 'comment',
+                                     'value': value})
+                    continue
+                if no['action'] == 'set':
+                    opts.append(no)
+                    continue
+                if no['action'] == 'addifnotset':
+                    opts.append({
+                        'name': 'comment',
+                        'type': 'comment',
+                        'value': self._dump_line(
+                            ' ', no['name'], ' modified by IPA'
+                        ),
+                    })
+                    opts.append({'name': 'comment', 'type': 'comment',
+                                'value': self._dump_line(no['name'],
+                                                         self.dassign,
+                                                         no['value'],
+                                                         )})
+                    opts.append(o)
+                    continue
+                raise SyntaxError('Unknown action: [%s]' % no['action'])
+
+            raise SyntaxError('Unknown type: [%s]' % o['type'])
+
+        return opts
+
+    def mergeNew(self, opts, newopts):
+
+        cline = 0
+
+        for no in newopts:
+
+            if no['type'] == "section" or no['type'] == "subsection":
+                (num, o) = self.findOpts(opts, no['type'], no['name'])
+                if not o:
+                    if no['action'] == 'set':
+                        opts.append(no)
+                    continue
+                if no['action'] == "set":
+                    self.mergeNew(o['value'], no['value'])
+                    continue
+                cline = num + 1
+                continue
+
+            if no['type'] == "option":
+                (num, o) = self.findOpts(opts, no['type'], no['name'], True)
+                if not o:
+                    if no['action'] == 'set' or no['action'] == 'addifnotset':
+                        opts.append(no)
+                    continue
+                cline = num + 1
+                continue
+
+            if no['type'] == "comment" or no['type'] == "empty":
+                opts.insert(cline, no)
+                cline += 1
+                continue
+
+            raise SyntaxError('Unknown type: [%s]' % no['type'])
+
+    def merge(self, oldopts, newopts):
+        """
+        Uses a two pass strategy:
+        First we create a new opts tree from oldopts removing/commenting
+          the options as indicated by the contents of newopts
+        Second we fill in the new opts tree with options as indicated
+          in the newopts tree (this is becaus eentire (sub)sections may
+          in the newopts tree (this is becaus entire (sub)sections may
+          exist in the newopts that do not exist in oldopts)
+        """
+        opts = self.mergeOld(oldopts, newopts)
+        self.mergeNew(opts, newopts)
+        return opts
+
+    # TODO: Make parse() recursive?
+    def parse(self, f):
+
+        opts = []
+        sectopts = []
+        section = None
+        subsectopts = []
+        subsection = None
+        curopts = opts
+        fatheropts = opts
+
+        # Read in the old file.
+        for line in f:
+
+            # It's a section start.
+            value = self.matchSection(line)
+            if value:
+                if section is not None:
+                    opts.append({'name': section,
+                                 'type': 'section',
+                                 'value': sectopts})
+                sectopts = []
+                curopts = sectopts
+                fatheropts = sectopts
+                section = value
+                continue
+
+            # It's a subsection start.
+            value = self.matchSubSection(line)
+            if value:
+                if subsection is not None:
+                    raise SyntaxError('nested subsections are not '
+                                      'supported yet')
+                subsectopts = []
+                curopts = subsectopts
+                subsection = value
+                continue
+
+            value = self.matchSubSectionEnd(line)
+            if value:
+                if subsection is None:
+                    raise SyntaxError('Unmatched end subsection terminator '
+                                      'found')
+                fatheropts.append({'name': subsection,
+                                   'type': 'subsection',
+                                   'value': subsectopts})
+                subsection = None
+                curopts = fatheropts
+                continue
+
+            # Copy anything else as is.
+            try:
+                curopts.append(self.parseLine(line))
+            except SyntaxError as e:
+                raise SyntaxError('{error} in file {fname}: [{line}]'.format(
+                    error=e, fname=f.name, line=line.rstrip()))
+
+        # Add last section if any
+        if len(sectopts) is not 0:
+            opts.append({'name': section,
+                         'type': 'section',
+                         'value': sectopts})
+
+        return opts
+
+    def changeConf(self, file, newopts):
+        """
+        Write settings to configuration file
+        :param file: path to the file
+        :param options: set of dictionaries in the form:
+             {'name': 'foo', 'value': 'bar', 'action': 'set/comment'}
+        :param section: section name like 'global'
+        """
+        output = ""
+        f = None
+        try:
+            # Do not catch an unexisting file error
+            # we want to fail in that case
+            shutil.copy2(file, (file + ".ipabkp"))
+
+            f = openLocked(file, 0o644)
+
+            oldopts = self.parse(f)
+
+            options = self.merge(oldopts, newopts)
+
+            output = self.dump(options)
+
+            # Write it out and close it.
+            f.seek(0)
+            f.truncate(0)
+            f.write(output)
+        finally:
+            try:
+                if f:
+                    f.close()
+            except IOError:
+                pass
+        logger.debug("Updating configuration file %s", file)
+        logger.debug(output)
+        return True
+
+    def newConf(self, file, options, file_perms=0o644):
+        """"
+        Write settings to a new file, backup the old
+        :param file: path to the file
+        :param options: a set of dictionaries in the form:
+             {'name': 'foo', 'value': 'bar', 'action': 'set/comment'}
+        :param file_perms: number defining the new file's permissions
+        """
+        output = ""
+        f = None
+        try:
+            try:
+                shutil.copy2(file, (file + ".ipabkp"))
+            except IOError as err:
+                if err.errno == 2:
+                    # The orign file did not exist
+                    pass
+
+            f = openLocked(file, file_perms)
+
+            # Trunkate
+            f.seek(0)
+            f.truncate(0)
+
+            output = self.dump(options)
+
+            f.write(output)
+        finally:
+            try:
+                if f:
+                    f.close()
+            except IOError:
+                pass
+        logger.debug("Writing configuration file %s", file)
+        logger.debug(output)
+        return True
+
+    @staticmethod
+    def setOption(name, value):
+        return {'name': name,
+                'type': 'option',
+                'action': 'set',
+                'value': value}
+
+    @staticmethod
+    def rmOption(name):
+        return {'name': name,
+                'type': 'option',
+                'action': 'remove',
+                'value': None}
+
+    @staticmethod
+    def setSection(name, options):
+        return {'name': name,
+                'type': 'section',
+                'action': 'set',
+                'value': options}
+
+    @staticmethod
+    def emptyLine():
+        return {'name': 'empty',
+                'type': 'empty'}
diff --git a/ipaserver/install/adtrustinstance.py b/ipaserver/install/adtrustinstance.py
index 7bb9431..47a5a92 100644
--- a/ipaserver/install/adtrustinstance.py
+++ b/ipaserver/install/adtrustinstance.py
@@ -40,11 +40,11 @@ from ipaserver.install.replication import wait_for_task
 from ipalib import errors, api
 from ipalib.util import normalize_zone
 from ipapython.dn import DN
+from ipapython import ipachangeconf
 from ipapython import ipaldap
 from ipapython import ipautil
 import ipapython.errors
 
-import ipaclient.install.ipachangeconf
 from ipaplatform import services
 from ipaplatform.constants import constants
 from ipaplatform.paths import paths
@@ -639,7 +639,7 @@ class ADTRUSTInstance(service.Service):
             self.print_msg("Cannot modify /etc/krb5.conf")
 
         krbconf = (
-            ipaclient.install.ipachangeconf.IPAChangeConf("IPA Installer"))
+            ipachangeconf.IPAChangeConf("IPA Installer"))
         krbconf.setOptionAssignment((" = ", " "))
         krbconf.setSectionNameDelimiters(("[", "]"))
         krbconf.setSubSectionDelimiters(("{", "}"))
diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py
index 02c8f4d..6a81d57 100644
--- a/ipaserver/install/server/install.py
+++ b/ipaserver/install/server/install.py
@@ -19,7 +19,7 @@ import six
 from ipaclient.install import timeconf
 from ipaclient.install.client import (
     check_ldap_conf, sync_time, restore_time_sync)
-from ipaclient.install.ipachangeconf import IPAChangeConf
+from ipapython.ipachangeconf import IPAChangeConf
 from ipalib.install import certmonger, sysrestore
 from ipapython import ipautil, version
 from ipapython.ipautil import (
diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py
index 6da6804..7272640 100644
--- a/ipaserver/install/server/replicainstall.py
+++ b/ipaserver/install/server/replicainstall.py
@@ -23,13 +23,13 @@ from pkg_resources import parse_version
 import six
 
 from ipaclient.install.client import check_ldap_conf
-from ipaclient.install.ipachangeconf import IPAChangeConf
 import ipaclient.install.timeconf
 from ipalib.install import certstore, sysrestore
 from ipalib.install.kinit import kinit_keytab
 from ipapython import ipaldap, ipautil
 from ipapython.dn import DN
 from ipapython.admintool import ScriptError
+from ipapython.ipachangeconf import IPAChangeConf
 from ipaplatform import services
 from ipaplatform.tasks import tasks
 from ipaplatform.paths import paths
diff --git a/ipatests/test_install/test_changeconf.py b/ipatests/test_install/test_changeconf.py
index 2dc2b7d..40c8a1d 100644
--- a/ipatests/test_install/test_changeconf.py
+++ b/ipatests/test_install/test_changeconf.py
@@ -3,7 +3,7 @@
 from __future__ import absolute_import
 
 import pytest
-from ipaclient.install.ipachangeconf import IPAChangeConf
+from ipapython.ipachangeconf import IPAChangeConf
 
 
 @pytest.fixture(scope='function')

From 2da90887632c764a73866c9ad3824ebb53c0aa73 Mon Sep 17 00:00:00 2001
From: Rob Critenden <rcritten@redhat.com>
Date: Aug 29 2019 06:45:12 +0000
Subject: Use tasks to configure automount nsswitch settings


authselect doesn't allow one to directly write to
/etc/nsswitch.conf. It will complain bitterly if it
detects it and will refuse to work until reset.

Instead it wants the user to write to
/etc/authselect/user-nsswitch.conf and then it will handle
merging in any differences.

To complicate matters some databases are not user configurable
like passwd, group and of course, automount. There are some
undocumented options to allow one to override these though so
we utilize that.

tasks are used so that authselect-based installations can still
write directly to /etc/nsswitch.conf and operate as it used to.

Reviewed-By: Francois Cami <fcami@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Rob Critenden <rcritten@redhat.com>
Reviewed-By: François Cami <fcami@redhat.com>

---

diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py
index 9492ca4..1e88ba1 100644
--- a/ipaclient/install/client.py
+++ b/ipaclient/install/client.py
@@ -66,7 +66,7 @@ from ipapython import version
 
 from . import automount, timeconf, sssd
 from ipaclient import discovery
-from .ipachangeconf import IPAChangeConf
+from ipapython.ipachangeconf import IPAChangeConf
 
 NoneType = type(None)
 
@@ -281,72 +281,6 @@ def is_ipa_client_installed(fstore, on_master=False):
     return installed
 
 
-def configure_nsswitch_database(fstore, database, services, preserve=True,
-                                append=True, default_value=()):
-    """
-    Edits the specified nsswitch.conf database (e.g. passwd, group, sudoers)
-    to use the specified service(s).
-
-    Arguments:
-        fstore - FileStore to backup the nsswitch.conf
-        database - database configuration that should be ammended,
-                   e.g. 'sudoers'
-        service - list of services that should be added, e.g. ['sss']
-        preserve - if True, the already configured services will be preserved
-
-    The next arguments modify the behaviour if preserve=True:
-        append - if True, the services will be appended, if False, prepended
-        default_value - list of services that are considered as default (if
-                        the database is not mentioned in nsswitch.conf), e.g.
-                        ['files']
-    """
-
-    # Backup the original version of nsswitch.conf, we're going to edit it now
-    if not fstore.has_file(paths.NSSWITCH_CONF):
-        fstore.backup_file(paths.NSSWITCH_CONF)
-
-    conf = IPAChangeConf("IPA Installer")
-    conf.setOptionAssignment(':')
-
-    if preserve:
-        # Read the existing configuration
-        with open(paths.NSSWITCH_CONF, 'r') as f:
-            opts = conf.parse(f)
-            raw_database_entry = conf.findOpts(opts, 'option', database)[1]
-
-        # Detect the list of already configured services
-        if not raw_database_entry:
-            # If there is no database entry, database is not present in
-            # the nsswitch.conf. Set the list of services to the
-            # default list, if passed.
-            configured_services = list(default_value)
-        else:
-            configured_services = raw_database_entry['value'].strip().split()
-
-        # Make sure no service is added if already mentioned in the list
-        added_services = [s for s in services
-                          if s not in configured_services]
-
-        # Prepend / append the list of new services
-        if append:
-            new_value = ' ' + ' '.join(configured_services + added_services)
-        else:
-            new_value = ' ' + ' '.join(added_services + configured_services)
-
-    else:
-        # Preserve not set, let's rewrite existing configuration
-        new_value = ' ' + ' '.join(services)
-
-    # Set new services as sources for database
-    opts = [
-        conf.setOption(database, new_value),
-        conf.emptyLine(),
-    ]
-
-    conf.changeConf(paths.NSSWITCH_CONF, opts)
-    logger.info("Configured %s in %s", database, paths.NSSWITCH_CONF)
-
-
 def configure_ipa_conf(
         fstore, cli_basedn, cli_realm, cli_domain, cli_server, hostname):
     ipaconf = IPAChangeConf("IPA Installer")
@@ -948,9 +882,7 @@ def configure_sssd_conf(
                 "Unable to activate the SUDO service in SSSD config.")
 
         sssdconfig.activate_service('sudo')
-        configure_nsswitch_database(
-            fstore, 'sudoers', ['sss'],
-            default_value=['files'])
+        tasks.enable_sssd_sudo(fstore)
 
     domain.add_provider('ipa', 'id')
 
diff --git a/ipaclient/install/ipa_client_automount.py b/ipaclient/install/ipa_client_automount.py
index a1dc2a1..3a0896b 100644
--- a/ipaclient/install/ipa_client_automount.py
+++ b/ipaclient/install/ipa_client_automount.py
@@ -41,7 +41,8 @@ from six.moves.urllib.parse import urlsplit
 
 # pylint: enable=import-error
 from optparse import OptionParser  # pylint: disable=deprecated-module
-from ipaclient.install import ipachangeconf, ipadiscovery
+from ipapython import ipachangeconf
+from ipaclient.install import ipadiscovery
 from ipaclient.install.client import (
     CLIENT_NOT_CONFIGURED,
     CLIENT_ALREADY_CONFIGURED,
@@ -177,44 +178,6 @@ def configure_xml(fstore):
         print("Configured %s" % authconf)
 
 
-def configure_nsswitch(statestore, options):
-    """
-    Point automount to ldap in nsswitch.conf.
-    This function is for non-SSSD setups only.
-    """
-    conf = ipachangeconf.IPAChangeConf("IPA Installer")
-    conf.setOptionAssignment(':')
-
-    with open(paths.NSSWITCH_CONF, 'r') as f:
-        current_opts = conf.parse(f)
-        current_nss_value = conf.findOpts(
-            current_opts, name='automount', type='option'
-        )[1]
-        if current_nss_value is None:
-            # no automount database present
-            current_nss_value = False  # None cannot be backed up
-        else:
-            current_nss_value = current_nss_value['value']
-        statestore.backup_state(
-            'ipa-client-automount-nsswitch', 'previous-automount',
-            current_nss_value
-        )
-
-    nss_value = ' files ldap'
-    opts = [
-        {
-            'name': 'automount',
-            'type': 'option',
-            'action': 'set',
-            'value': nss_value,
-        },
-        {'name': 'empty', 'type': 'empty'},
-    ]
-    conf.changeConf(paths.NSSWITCH_CONF, opts)
-
-    print("Configured %s" % paths.NSSWITCH_CONF)
-
-
 def configure_autofs_sssd(fstore, statestore, autodiscover, options):
     try:
         sssdconfig = SSSDConfig.SSSDConfig()
@@ -339,41 +302,8 @@ def uninstall(fstore, statestore):
     ]
     STATES = ['autofs', 'rpcidmapd', 'rpcgssd']
 
-    if statestore.get_state(
-        'ipa-client-automount-nsswitch', 'previous-automount'
-    ) is False:
-        # Previous nsswitch.conf had no automount database configured
-        # so remove it.
-        conf = ipachangeconf.IPAChangeConf("IPA automount installer")
-        conf.setOptionAssignment(':')
-        changes = [conf.rmOption('automount')]
-        conf.changeConf(paths.NSSWITCH_CONF, changes)
-        tasks.restore_context(paths.NSSWITCH_CONF)
-        statestore.delete_state(
-            'ipa-client-automount-nsswitch', 'previous-automount'
-        )
-    elif statestore.get_state(
-        'ipa-client-automount-nsswitch', 'previous-automount'
-    ) is not None:
-        nss_value = statestore.get_state(
-            'ipa-client-automount-nsswitch', 'previous-automount'
-        )
-        opts = [
-            {
-                'name': 'automount',
-                'type': 'option',
-                'action': 'set',
-                'value': nss_value,
-            },
-            {'name': 'empty', 'type': 'empty'},
-        ]
-        conf = ipachangeconf.IPAChangeConf("IPA automount installer")
-        conf.setOptionAssignment(':')
-        conf.changeConf(paths.NSSWITCH_CONF, opts)
-        tasks.restore_context(paths.NSSWITCH_CONF)
-        statestore.delete_state(
-            'ipa-client-automount-nsswitch', 'previous-automount'
-        )
+    if not statestore.get_state('autofs', 'sssd'):
+        tasks.disable_ldap_automount(statestore)
 
     if not any(fstore.has_file(f) for f in RESTORE_FILES) or not any(
         statestore.has_state(s) for s in STATES
@@ -627,7 +557,7 @@ def configure_automount():
 
     try:
         if not options.sssd:
-            configure_nsswitch(statestore, options)
+            tasks.enable_ldap_automount(statestore)
         configure_nfs(fstore, statestore, options)
         if options.sssd:
             configure_autofs_sssd(fstore, statestore, autodiscover, options)
diff --git a/ipaplatform/base/tasks.py b/ipaplatform/base/tasks.py
index 8aa9c5c..7fd7d57 100644
--- a/ipaplatform/base/tasks.py
+++ b/ipaplatform/base/tasks.py
@@ -32,6 +32,7 @@ from pkg_resources import parse_version
 from ipaplatform.constants import constants
 from ipaplatform.paths import paths
 from ipapython import ipautil
+from ipapython.ipachangeconf import IPAChangeConf
 
 logger = logging.getLogger(__name__)
 
@@ -337,5 +338,157 @@ class BaseTaskNamespace:
         """
         raise NotImplementedError
 
+    def configure_nsswitch_database(self, fstore, database, services,
+                                    preserve=True, append=True,
+                                    default_value=()):
+        """
+        Edits the specified nsswitch.conf database (e.g. passwd, group,
+        sudoers) to use the specified service(s).
+
+        Arguments:
+            fstore - FileStore to backup the nsswitch.conf
+            database - database configuration that should be ammended,
+                       e.g. 'sudoers'
+            service - list of services that should be added, e.g. ['sss']
+            preserve - if True, the already configured services will be
+                       preserved
+
+        The next arguments modify the behaviour if preserve=True:
+            append - if True, the services will be appended, if False,
+                     prepended
+            default_value - list of services that are considered as default (if
+                            the database is not mentioned in nsswitch.conf),
+                            e.g. ['files']
+        """
+
+        # Backup the original version of nsswitch.conf, we're going to edit it
+        # now
+        if not fstore.has_file(paths.NSSWITCH_CONF):
+            fstore.backup_file(paths.NSSWITCH_CONF)
+
+        conf = IPAChangeConf("IPA Installer")
+        conf.setOptionAssignment(':')
+
+        if preserve:
+            # Read the existing configuration
+            with open(paths.NSSWITCH_CONF, 'r') as f:
+                opts = conf.parse(f)
+                raw_database_entry = conf.findOpts(opts, 'option', database)[1]
+
+            # Detect the list of already configured services
+            if not raw_database_entry:
+                # If there is no database entry, database is not present in
+                # the nsswitch.conf. Set the list of services to the
+                # default list, if passed.
+                configured_services = list(default_value)
+            else:
+                configured_services = raw_database_entry[
+                    'value'].strip().split()
+
+            # Make sure no service is added if already mentioned in the list
+            added_services = [s for s in services
+                              if s not in configured_services]
+
+            # Prepend / append the list of new services
+            if append:
+                new_value = ' ' + ' '.join(configured_services +
+                                           added_services)
+            else:
+                new_value = ' ' + ' '.join(added_services +
+                                           configured_services)
+
+        else:
+            # Preserve not set, let's rewrite existing configuration
+            new_value = ' ' + ' '.join(services)
+
+        # Set new services as sources for database
+        opts = [
+            conf.setOption(database, new_value),
+            conf.emptyLine(),
+        ]
+
+        conf.changeConf(paths.NSSWITCH_CONF, opts)
+        logger.info("Configured %s in %s", database, paths.NSSWITCH_CONF)
+
+    def enable_sssd_sudo(self, fstore):
+        """Configure nsswitch.conf to use sssd for sudo"""
+        self.configure_nsswitch_database(
+            fstore, 'sudoers', ['sss'],
+            default_value=['files'])
+
+    def enable_ldap_automount(self, statestore):
+        """
+        Point automount to ldap in nsswitch.conf.
+        This function is for non-SSSD setups only.
+        """
+        conf = IPAChangeConf("IPA Installer")
+        conf.setOptionAssignment(':')
+
+        with open(paths.NSSWITCH_CONF, 'r') as f:
+            current_opts = conf.parse(f)
+            current_nss_value = conf.findOpts(
+                current_opts, name='automount', type='option'
+            )[1]
+            if current_nss_value is None:
+                # no automount database present
+                current_nss_value = False  # None cannot be backed up
+            else:
+                current_nss_value = current_nss_value['value']
+            statestore.backup_state(
+                'ipa-client-automount-nsswitch', 'previous-automount',
+                current_nss_value
+            )
+
+        nss_value = ' files ldap'
+        opts = [
+            {
+                'name': 'automount',
+                'type': 'option',
+                'action': 'set',
+                'value': nss_value,
+            },
+            {'name': 'empty', 'type': 'empty'},
+        ]
+        conf.changeConf(paths.NSSWITCH_CONF, opts)
+
+        logger.info("Configured %s", paths.NSSWITCH_CONF)
+
+    def disable_ldap_automount(self, statestore):
+        """Disable automount using LDAP"""
+        if statestore.get_state(
+            'ipa-client-automount-nsswitch', 'previous-automount'
+        ) is False:
+            # Previous nsswitch.conf had no automount database configured
+            # so remove it.
+            conf = IPAChangeConf("IPA automount installer")
+            conf.setOptionAssignment(':')
+            changes = [conf.rmOption('automount')]
+            conf.changeConf(paths.NSSWITCH_CONF, changes)
+            self.restore_context(paths.NSSWITCH_CONF)
+            statestore.delete_state(
+                'ipa-client-automount-nsswitch', 'previous-automount'
+            )
+        elif statestore.get_state(
+            'ipa-client-automount-nsswitch', 'previous-automount'
+        ) is not None:
+            nss_value = statestore.get_state(
+                'ipa-client-automount-nsswitch', 'previous-automount'
+            )
+            opts = [
+                {
+                    'name': 'automount',
+                    'type': 'option',
+                    'action': 'set',
+                    'value': nss_value,
+                },
+                {'name': 'empty', 'type': 'empty'},
+            ]
+            conf = IPAChangeConf("IPA automount installer")
+            conf.setOptionAssignment(':')
+            conf.changeConf(paths.NSSWITCH_CONF, opts)
+            self.restore_context(paths.NSSWITCH_CONF)
+            statestore.delete_state(
+                'ipa-client-automount-nsswitch', 'previous-automount'
+            )
 
 tasks = BaseTaskNamespace()
diff --git a/ipaplatform/redhat/paths.py b/ipaplatform/redhat/paths.py
index 8ccd04b..15bdef6 100644
--- a/ipaplatform/redhat/paths.py
+++ b/ipaplatform/redhat/paths.py
@@ -39,6 +39,7 @@ class RedHatPathNamespace(BasePathNamespace):
     AUTHCONFIG = '/usr/sbin/authconfig'
     AUTHSELECT = '/usr/bin/authselect'
     SYSCONF_NETWORK = '/etc/sysconfig/network'
+    NSSWITCH_CONF = '/etc/authselect/user-nsswitch.conf'
 
 
 paths = RedHatPathNamespace()
diff --git a/ipaplatform/redhat/tasks.py b/ipaplatform/redhat/tasks.py
index be0b641..e18f6fa 100644
--- a/ipaplatform/redhat/tasks.py
+++ b/ipaplatform/redhat/tasks.py
@@ -744,4 +744,23 @@ class RedHatTaskNamespace(BaseTaskNamespace):
 
         return filenames
 
+    def enable_ldap_automount(self, statestore):
+        """
+        Point automount to ldap in nsswitch.conf.
+        This function is for non-SSSD setups only.
+        """
+        super(RedHatTaskNamespace, self).enable_ldap_automount(statestore)
+
+        authselect_cmd = [paths.AUTHSELECT, "enable-feature",
+                          "with-custom-automount"]
+        ipautil.run(authselect_cmd)
+
+    def disable_ldap_automount(self, statestore):
+        """Disable ldap-based automount"""
+        super(RedHatTaskNamespace, self).disable_ldap_automount(statestore)
+
+        authselect_cmd = [paths.AUTHSELECT, "disable-feature",
+                          "with-custom-automount"]
+        ipautil.run(authselect_cmd)
+
 tasks = RedHatTaskNamespace()