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