-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 11/17/2009 12:16 PM, Jakub Hrozek wrote:
> On 11/17/2009 01:06 AM, Jakub Hrozek wrote:
>> [PATCH 1/2] Add Simo's ipachangeconf
>> This patch adds the ipachangeconf class from FreeIPA and packages it in
>> makefile and with python setuptools
> 
>> [PATCH 2/2] Change the upgrade script to use ipachangeconf
>> With this patch, the upgrade script we use for changing the config files
>> is able to keep ordering and comments.
> 
>> Fixes: #249
> 
> Self-NACK to patch 2 & new patches attached.

As discussed on IRC, I'm resending the same two patches, just formatted
with git format-patch -M to detect renamed upgrade script
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Fedora - http://enigmail.mozdev.org/

iEYEARECAAYFAksCvj0ACgkQHsardTLnvCXzigCghisbBaqeUL9UAI/7nKuwl5Q9
eysAni2bnD5vpvTqRZJllVktcBxN5sVq
=O0WU
-----END PGP SIGNATURE-----
>From dffe621d043cbbeb2e1d5794da720207cf503a25 Mon Sep 17 00:00:00 2001
From: Jakub Hrozek <jhro...@redhat.com>
Date: Wed, 11 Nov 2009 13:07:45 +0100
Subject: [PATCH 1/2] Add Simo's ipachangeconf

---
 server/Makefile.am             |    1 +
 server/config/ipachangeconf.py |  459 ++++++++++++++++++++++++++++++++++++++++
 server/config/setup.py         |    1 +
 3 files changed, 461 insertions(+), 0 deletions(-)
 create mode 100644 server/config/ipachangeconf.py

diff --git a/server/Makefile.am b/server/Makefile.am
index 08c0295..33c4bf1 100644
--- a/server/Makefile.am
+++ b/server/Makefile.am
@@ -111,6 +111,7 @@ endif
 
 dist_noinst_SCRIPTS = \
     config/setup.py \
+    config/ipachangeconf.py \
     config/SSSDConfig.py
 
 ###############################
diff --git a/server/config/ipachangeconf.py b/server/config/ipachangeconf.py
new file mode 100644
index 0000000..e083055
--- /dev/null
+++ b/server/config/ipachangeconf.py
@@ -0,0 +1,459 @@
+#
+# ipachangeconf - configuration file manipulation classes and functions
+# partially based on authconfig code
+# Copyright (c) 1999-2007 Red Hat, Inc.
+# Author: Simo Sorce <sso...@redhat.com>
+#
+# This is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+
+import fcntl
+import os
+import string
+import time
+import shutil
+
+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, (errno, strerr):
+        if fd != -1:
+            try:
+                os.close(fd)
+            except OSError:
+                pass
+        raise IOError(errno, strerr)
+    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.backup_suffix = ".ipabkp"
+
+    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())
+        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.sectnamdel[0]+section+self.sectnamdel[1]+self.deol
+
+    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 += self.sectnamdel[0]+o['name']+self.sectnamdel[1]+self.deol
+                output += self.dump(o['value'], level+1)
+                continue
+            if o['type'] == "subsection":
+                output += self.indent[level]+o['name']+self.dassign+self.subsectdel[0]+self.deol
+                output += self.dump(o['value'], level+1)
+                output += self.indent[level]+self.subsectdel[1]+self.deol
+                continue
+            if o['type'] == "option":
+                output += self.indent[level]+o['name']+self.dassign+o['value']+self.deol
+                continue
+            if o['type'] == "comment":
+                output += self.dcomment+o['value']+self.deol
+                continue
+            if o['type'] == "empty":
+                output += self.deol
+                continue
+            raise SyntaxError, 'Unknown type: ['+o['type']+']'
+
+        return 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()}
+
+        parts = line.split(self.dassign, 1)
+        if len(parts) < 2:
+            raise SyntaxError, 'Syntax Error: Unknown line format'
+
+        return {'name':parts[0].strip(), 'type':'option', 'value':parts[1].rstrip()}
+
+    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.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.indent[level]+o['name']+self.dassign+self.subsectdel[0]
+                opts.append({'name':'comment', 'type':'comment', 'value':val})
+                for n in no:
+                    opts.append(n)
+                val = self.indent[level]+self.subsectdel[1]
+                opts.append({'name':'comment', 'type':'comment', 'value':val})
+                continue
+            if o['type'] == 'option':
+                val = self.indent[level]+o['name']+self.dassign+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: ['+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: ['+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'] != None and o['value'] != no['value']:
+                        opts.append(o)
+                        continue
+                    if no['action'] == 'comment':
+                       opts.append({'name':'comment', 'type':'comment',
+                                    'value':self.dcomment+o['name']+self.dassign+o['value']})
+                    continue
+                if no['action'] == 'set':
+                    opts.append(no)
+                    continue
+                raise SyntaxError, 'Unknown action: ['+o['action']+']'
+
+            raise SyntaxError, 'Unknown type: ['+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':
+                        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: ['+no['type']+']'
+
+
+    def merge(self, oldopts, newopts):
+
+        #Use 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
+        #  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.
+            curopts.append(self.parseLine(line))
+
+        #Add last section if any
+        if len(sectopts) is not 0:
+            opts.append({'name':section, 'type':'section', 'value':sectopts})
+
+        return opts
+
+    # Write settings to configuration file
+    # file is a path
+    # options is a set of dictionaries in the form:
+    #     [{'name': 'foo', 'value': 'bar', 'action': 'set/comment'}]
+    # section is a section name like 'global'
+    def changeConf(self, file, newopts):
+        autosection = False
+        savedsection = None
+        done = False
+        output = ""
+        f = None
+        try:
+            #Do not catch an unexisting file error, we want to fail in that case
+            shutil.copy2(file, file+self.backup_suffix)
+
+            f = openLocked(file, 0644)
+
+            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
+        return True
+
+    # Write settings to new file, backup old
+    # file is a path
+    # options is a set of dictionaries in the form:
+    #     [{'name': 'foo', 'value': 'bar', 'action': 'set/comment'}]
+    # section is a section name like 'global'
+    def newConf(self, file, options):
+        autosection = False
+        savedsection = None
+        done = False
+        output = ""
+        f = None
+        try:
+            try:
+                shutil.copy2(file, file+self.backup_suffix)
+            except IOError, err:
+                if err.errno == 2:
+                    # The orign file did not exist
+                    pass
+
+            f = openLocked(file, 0644)
+
+            # Trunkate
+            f.seek(0)
+            f.truncate(0)
+
+            output = self.dump(options)
+
+            f.write(output)
+        finally:
+            try:
+                if f:
+                    f.close()
+            except IOError:
+                pass
+        return True
diff --git a/server/config/setup.py b/server/config/setup.py
index 7f108a3..46a8106 100644
--- a/server/config/setup.py
+++ b/server/config/setup.py
@@ -30,5 +30,6 @@ setup(
     url='http://fedorahosted.org/sssd',
     py_modules=[
     'SSSDConfig',
+    'ipachangeconf',
     ],
 )
-- 
1.6.2.5

>From 1eb7c4edac2ee744271d35ee57c3178798e7dca2 Mon Sep 17 00:00:00 2001
From: Jakub Hrozek <jhro...@redhat.com>
Date: Wed, 11 Nov 2009 18:39:16 +0100
Subject: [PATCH 2/2] Change the upgrade script to use ipachangeconf

With this patch, the upgrade script we use for changing the config files
is able to keep ordering and comments.

Fixes: #249
---
 server/Makefile.am                           |    2 +-
 server/config/ipachangeconf.py               |   42 +++
 server/{upgrade => config}/upgrade_config.py |  357 ++++++++++++--------------
 3 files changed, 211 insertions(+), 190 deletions(-)
 rename server/{upgrade => config}/upgrade_config.py (56%)

diff --git a/server/Makefile.am b/server/Makefile.am
index 33c4bf1..3701ef1 100644
--- a/server/Makefile.am
+++ b/server/Makefile.am
@@ -59,7 +59,7 @@ sssdlibexec_PROGRAMS = \
     $(sssd_info)
 
 dist_sssdlibexec_SCRIPTS = \
-    upgrade/upgrade_config.py
+    config/upgrade_config.py
 
 if HAVE_CHECK
     non_interactive_check_based_tests = \
diff --git a/server/config/ipachangeconf.py b/server/config/ipachangeconf.py
index e083055..ddad680 100644
--- a/server/config/ipachangeconf.py
+++ b/server/config/ipachangeconf.py
@@ -457,3 +457,45 @@ class IPAChangeConf:
             except IOError:
                 pass
         return True
+
+# A SSSD-specific subclass of IPAChangeConf
+class SSSDChangeConf(IPAChangeConf):
+    def __init__(self):
+        IPAChangeConf.__init__(self, "SSSD")
+        self.comment = ("#",";")
+        self.backup_suffix = ".bak"
+        self.opts = []
+
+    def readfd(self, fd):
+        self.opts.extend(self.parse(fd))
+
+    def read(self, filename):
+        fd = open(filename, 'r')
+        self.readfd(fd)
+        fd.close()
+
+    def sections(self):
+        return [ o for o in self.opts if o['type'] == 'section' ]
+
+    def delete_option(self, type, name, exclude_sections=False):
+        index, item = self.findOpts(self.opts, type, name, exclude_sections)
+        if item:
+            del self.opts[index]
+
+    def get_option(self, parent_name, name, type='option'):
+        subtree = None
+        if parent_name:
+            pindex, pdata = self.findOpts(self.opts, 'section', parent_name)
+            if not pdata:
+                return (-1, None)
+            subtree = pdata['value']
+        else:
+            subtree = self.opts
+        return self.findOpts(subtree, type, name)
+
+    def rename_opts(self, parent_name, rename_kw, type='option'):
+        for new_name, old_name in rename_kw.items():
+            index, item = self.get_option(parent_name, old_name, type)
+            if item:
+                item['name'] = new_name
+
diff --git a/server/upgrade/upgrade_config.py b/server/config/upgrade_config.py
similarity index 56%
rename from server/upgrade/upgrade_config.py
rename to server/config/upgrade_config.py
index 213bb73..c973ad2 100644
--- a/server/upgrade/upgrade_config.py
+++ b/server/config/upgrade_config.py
@@ -24,124 +24,96 @@ import os
 import sys
 import shutil
 import traceback
-import copy
-from ConfigParser import RawConfigParser
-from ConfigParser import NoOptionError
 from optparse import OptionParser
 
-class SSSDConfigParser(RawConfigParser):
-    def raw_set(self, section, option):
-        " set without interpolation "
-        pass
-
-    def raw_get(self, section, option):
-        " get without interpolation "
-        return self._sections[section].get(option)
-
-    def _write_section(self, section, fp):
-        fp.write("[%s]\n" % section)
-        for (key, value) in sorted(self._sections[section].items()):
-            if key != "__name__":
-                fp.write("%s = %s\n" %
-                        (key, str(value).replace('\n', '\n\t')))
-        fp.write("\n")
-
-    def write(self, fp):
-        """
-        SSSD Config file uses a logical order of sections
-        ConfigParser does not allow sorting the sections, so
-        we hackishly sort them here..
-        """
-        # Write SSSD first
-        if "sssd" in self._sections:
-            self._write_section("sssd", fp)
-            if (self.has_option('sssd', 'domains')):
-                active_domains = [s.strip() for s in self.get('sssd','domains').split(',')]
-            else:
-                #There were no active domains configured
-                active_domains = []
-            del self._sections["sssd"]
-        # Write the other services
-        for service in [ s for s in self._sections if not s.startswith('domain/') ]:
-            self._write_section(service, fp)
-            del self._sections[service]
-
-        # Write the domains in the order that is specified in domains =
-        for dom in active_domains:
-            self._write_section('domain/%s' % dom, fp)
-            del self._sections['domain/%s' % dom]
-
-        # Write inactive domains
-        for section in sorted(self._sections):
-            self._write_section(section, fp)
-
-class SSSDConfigFile(object):
-    def __init__(self, file_name):
-        self.file_name = file_name
-        self._config = SSSDConfigParser()
-        self._new_config = SSSDConfigParser()
-        self._config.read(file_name)
+from ipachangeconf import openLocked
+from ipachangeconf import SSSDChangeConf
 
-    def get_version(self):
-        " Guess if we are looking at v1 config file "
-        if not self._config.has_section('sssd'):
-            return 1
-        if not self._config.has_option('sssd', 'config_file_version'):
-            return 1
-        return self._config.getint('sssd', 'config_file_version')
+class SSSDConfigFile(SSSDChangeConf):
+    def __init__(self, filename):
+        SSSDChangeConf.__init__(self)
+        self.filename = filename
 
-    def _backup_file(self):
-        " Copy the file we operate on to a backup location "
-        shutil.copy(self.file_name, self.file_name+".bak")
+        f = openLocked(self.filename, 0600)
+        self.opts = self.parse(f)
+        f.close()
 
+    def _backup_file(self, file_name):
+        " Copy the file we operate on to a backup location "
+        shutil.copy(file_name, file_name + self.backup_suffix)
         # make sure we don't leak data, force permissions on the backup
-        os.chmod(self.file_name+".bak", 0600)
-
-    def _migrate_if_exists(self, to_section, to_option, from_section, from_option):
-        """
-        Move value of parameter from one section to another, renaming the parameter
-        """
-        if self._config.has_section(from_section) and \
-           self._config.has_option(from_section, from_option):
-            self._new_config.set(to_section, to_option,
-                                 self._config.get(from_section, from_option))
-
-    def _migrate_kw(self, to_section, from_section, new_old_dict):
-        """
-        Move value of parameter from one section to another according to
-        mapping in ``new_old_dict``
-        """
-        for new, old in new_old_dict.items():
-            self._migrate_if_exists(to_section, new, from_section, old)
-
-    def _migrate_enumerate(self, to_section, from_section):
+        os.chmod(file_name + self.backup_suffix, 0600)
+
+    def get_version(self):
+        ver = self.get_option('sssd', 'config_file_version')[1]
+        if not ver:
+            return 1
+        try:
+            return int(ver['value'])
+        except ValueError:
+            raise SyntaxError, 'config_file_version not an integer'
+
+    def _do_v2_changes(self):
+        # remove Data Provider
+        srvlist = self.get_option('sssd', 'services')[1]
+        if srvlist:
+            services = [ srv.strip() for srv in srvlist['value'].split(',') ]
+            if 'dp' in services:
+                services.remove('dp')
+            srvlist['value'] = ", ".join([srv for srv in services])
+        self.delete_option('section', 'dp')
+
+    def _update_option(self, to_section_name, from_section_name, opts):
+        to_section = [ s for s in self.sections() if s['name'].strip() == to_section_name ]
+        from_section = [ s for s in self.sections() if s['name'].strip() == from_section_name ]
+
+        if len(to_section) > 0 and len(from_section) > 0:
+            vals = to_section[0]['value']
+            for o in [one_opt for one_opt in from_section[0]['value'] if one_opt['name'] in opts]:
+                updated = False
+                for v in vals:
+                    if v['type'] == 'empty':
+                        continue
+                    # if already in list, just update
+                    if o['name'] == v['name']:
+                        o['value'] = v['value']
+                        updated = True
+                # not in list, add there
+                if not updated:
+                    vals.insert(0, { 'name' : o['name'], 'type' : o['type'], 'value' : o['value'] })
+
+    def _add_section(self, name, optkw, index=0):
+        optkw.append({'type':'empty', 'value':'empty'})
+        addkw = { 'type'   : 'section',
+                   'name'   : name,
+                   'value'  : optkw,
+                }
+        self.opts.insert(index, addkw)
+
+    def _migrate_enumerate(self, domain):
         " Enumerate was special as it turned into bool from (0,1,2,3) enum "
-        if self._config.has_section(from_section) and \
-           self._config.has_option(from_section, 'enumerate'):
-            enumvalue = self._config.get(from_section, 'enumerate')
-            if enumvalue.upper() in ['TRUE', 'FALSE']:
-                self._new_config.set(to_section, 'enumerate', enumvalue)
-            else:
+        enum = self.findOpts(domain, 'option', 'enumerate')[1]
+        if enum:
+            if enum['value'].upper() not in ['TRUE', 'FALSE']:
                 try:
-                    enumvalue = int(enumvalue)
+                    enum['value'] = int(enum['value'])
                 except ValueError:
-                    raise ValueError('Cannot convert value %s in domain %s' % (enumvalue, from_section))
+                    raise ValueError('Cannot convert value %s in domain %s' % (enum['value'], domain['name']))
 
-                if enumvalue == 0:
-                    self._new_config.set(to_section, 'enumerate', 'FALSE')
-                elif enumvalue > 0:
-                    self._new_config.set(to_section, 'enumerate', 'TRUE')
+                if enum['value'] == 0:
+                    enum['value'] = 'FALSE'
+                elif enum['value'] > 0:
+                    enum['value'] = 'TRUE'
                 else:
-                    raise ValueError('Cannot convert value %s in domain %s' % (enumvalue, from_section))
+                    raise ValueError('Cannot convert value %s in domain %s' % (enum['value'], domain['name']))
 
     def _migrate_domain(self, domain):
-        new_domsec = 'domain/%s' % domain
-        old_domsec = 'domains/%s' % domain
-        self._new_config.add_section(new_domsec)
+        # rename the section
+        domain['name'] = domain['name'].strip().replace('domains', 'domain')
 
         # Generic options - new:old
-        generic_kw = { 'min_id' : 'minID',
-                       'max_id': 'maxID',
+        generic_kw = { 'min_id' : 'minId',
+                       'max_id': 'maxId',
                        'timeout': 'timeout',
                        'magic_private_groups' : 'magicPrivateGroups',
                        'cache_credentials' : 'cache-credentials',
@@ -207,74 +179,39 @@ class SSSDConfigFile(object):
                              'base_directory' : 'baseDirectory',
                            }
 
-        self._migrate_enumerate(new_domsec, old_domsec)
-        self._migrate_kw(new_domsec, old_domsec, generic_kw)
-        self._migrate_kw(new_domsec, old_domsec, proxy_kw)
-        self._migrate_kw(new_domsec, old_domsec, ldap_kw)
-        self._migrate_kw(new_domsec, old_domsec, krb5_kw)
+        self._migrate_enumerate(domain['value'])
+        self.rename_opts(domain['name'], generic_kw)
+        self.rename_opts(domain['name'], proxy_kw)
+        self.rename_opts(domain['name'], ldap_kw)
+        self.rename_opts(domain['name'], krb5_kw)
 
         # configuration files before 0.5.0 did not enforce provider= in local domains
         # it did special-case by domain name (LOCAL)
-        try:
-            prv = self._new_config.get(new_domsec, 'id_provider')
-        except NoOptionError:
-            if old_domsec == 'domains/LOCAL':
-                prv = 'local'
-                self._new_config.set(new_domsec, 'id_provider', prv)
-
+        prv = self.findOpts(domain['value'], 'option', 'id_provider')[1]
+        if not prv and domain['name'] == 'domain/LOCAL':
+            prv = { 'type'  : 'option',
+                    'name'  : 'id_provider',
+                    'value' : 'local',
+                    'action': 'set',
+                  }
+            domain['value'].insert(0, prv)
         # if domain was local, update with parameters from [user_defaults]
-        if prv == 'local':
-            self._migrate_kw(new_domsec, 'user_defaults', user_defaults_kw)
+        if prv['value'] == 'local':
+            self._update_option(domain['name'], 'user_defaults', user_defaults_kw.values())
+            self.delete_option('section', 'user_defaults')
+            self.rename_opts(domain['name'], user_defaults_kw)
 
     def _migrate_domains(self):
-        for domain in [ s.replace('domains/','') for s in self._config.sections() if s.startswith("domains/") ]:
-            domain = domain.strip()
+        for domain in [ s for s in self.sections() if s['name'].startswith("domains/") ]:
             self._migrate_domain(domain)
 
-    def _remove_dp(self):
-        # If data provider is in the list of active services, remove it
-        if self._new_config.has_option('sssd', 'services'):
-            services = [ srv.strip() for srv in self._new_config.get('sssd', 'services').split(',') ]
-            if 'dp' in services:
-                services.remove('dp')
-
-        self._new_config.set('sssd', 'services', ", ".join([srv for srv in services]))
-
-        # also remove the [dp] section
-        self._new_config.remove_section('dp')
-
-    def _do_v2_changes(self):
-        # the changes themselves
-        self._remove_dp()
-
-    def v2_changes(self, out_file_name, backup=True):
-        """
-        Check for needed changes in V2 format and write the result into
-        ``out_file_name```.
-        """
-        # basically a wrapper around _do_v2_changes
-        self._new_config = copy.deepcopy(self._config)
-
-        if backup:
-            self._backup_file()
-
-        self._do_v2_changes()
-
-        # all done, open the file for writing
-        of = open(out_file_name, "wb")
-
-        # make sure it has the right permissions too
-        os.chmod(out_file_name, 0600)
-        self._new_config.write(of)
-
-    def upgrade_v2(self, out_file_name, backup=True):
-        """
-        Upgrade the config file to V2 format and write the result into
-        ``out_file_name```.
-        """
-        if backup:
-            self._backup_file()
+    def _update_if_exists(self, opt, to_name, from_section, from_name):
+        index, item = self.get_option(from_section, from_name)
+        if item:
+            item['name'] = to_name
+            opt.append(item)
 
+    def _migrate_services(self):
         # [service] - options common to all services, no section as in v1
         service_kw = { 'reconnection_retries' : 'reconnection_retries',
                        'debug_level' : 'debug-level',
@@ -283,24 +220,37 @@ class SSSDConfigFile(object):
                        'timeout' : 'timeout',
                      }
 
+        # rename services sections
+        names_kw = { 'nss' : 'services/nss',
+                     'pam' : 'services/pam',
+                     'dp'  : 'services/dp',
+                   }
+        self.rename_opts(None, names_kw, 'section')
+
         # [sssd] - monitor service
-        self._new_config.add_section('sssd')
-        self._new_config.set('sssd', 'config_file_version', '2')
-        self._migrate_if_exists('sssd', 'domains',
-                                'domains', 'domains')
-        self._migrate_if_exists('sssd', 'services',
-                                'services', 'activeServices')
-        self._migrate_if_exists('sssd', 'sbus_timeout',
-                                 'services/monitor', 'sbusTimeout')
-        self._migrate_if_exists('sssd', 're_expression',
-                                'names', 're-expression')
-        self._migrate_if_exists('sssd', 're_expression',
-                                'names', 'full-name-format')
-        self._migrate_kw('sssd', 'services', service_kw)
-        self._migrate_kw('sssd', 'services/monitor', service_kw)
+        sssd_kw = [
+                    { 'type'  : 'option',
+                      'name'  : 'config_file_version',
+                      'value' : '2',
+                      'action': 'set',
+                    }
+                  ]
+        self._update_if_exists(sssd_kw, 'domains',
+                               'domains', 'domains')
+        self._update_if_exists(sssd_kw, 'services',
+                               'services', 'activeServices')
+        self._update_if_exists(sssd_kw, 'sbus_timeout',
+                               'services/monitor', 'sbusTimeout')
+        self._update_if_exists(sssd_kw, 're_expression',
+                              'names', 're-expression')
+        self._update_if_exists(sssd_kw, 're_expression',
+                              'names', 'full-name-format')
+        self._add_section('sssd', sssd_kw)
+        # update from general services section and monitor
+        self._update_option('sssd', 'services', service_kw.values())
+        self._update_option('sssd', 'services/monitor', service_kw.values())
 
         # [nss] - Name service
-        self._new_config.add_section('nss')
         nss_kw = { 'enum_cache_timeout' : 'EnumCacheTimeout',
                    'entry_cache_timeout' : 'EntryCacheTimeout',
                    'entry_cache_nowait_timeout' : 'EntryCacheNoWaitRefreshTimeout',
@@ -310,29 +260,55 @@ class SSSDConfigFile(object):
                    'filter_users_in_groups' : 'filterUsersInGroups',
                    }
         nss_kw.update(service_kw)
-        self._migrate_kw('nss', 'services', service_kw)
-        self._migrate_kw('nss', 'services/nss', nss_kw)
+        self._update_option('nss', 'services', service_kw.values())
+        self.rename_opts('nss', nss_kw)
 
         # [pam] - Authentication service
-        self._new_config.add_section('pam')
         pam_kw = {}
         pam_kw.update(service_kw)
-        self._migrate_kw('pam', 'services', service_kw)
-        self._migrate_kw('pam', 'services/pam', pam_kw)
+        self._update_option('pam', 'services', service_kw.values())
+        self.rename_opts('pam', pam_kw)
 
-        # Migrate domains
-        self._migrate_domains()
+        # remove obsolete sections
+        self.delete_option('section', 'services')
+        self.delete_option('section', 'names')
+        self.delete_option('section', 'domains')
+        self.delete_option('section', 'services/monitor')
+
+    def v2_changes(self, out_file_name, backup=True):
+        # read in the old file, make backup if needed
+        if backup:
+            self._backup_file(self.filename)
 
-        # Perform neccessary changes
         self._do_v2_changes()
 
-        # all done, open the file for writing
+        # all done, write the file
         of = open(out_file_name, "wb")
-
+        output = self.dump(self.opts)
+        of.write(output)
+        of.close()
         # make sure it has the right permissions too
         os.chmod(out_file_name, 0600)
 
-        self._new_config.write(of)
+    def upgrade_v2(self, out_file_name, backup=True):
+        # read in the old file, make backup if needed
+        if backup:
+            self._backup_file(self.filename)
+
+        # do the migration to v2 format
+        # do the upgrade
+        self._migrate_services()
+        self._migrate_domains()
+        # also include any changes in the v2 format
+        self._do_v2_changes()
+
+        # all done, write the file
+        of = open(out_file_name, "wb")
+        output = self.dump(self.opts)
+        of.write(output)
+        of.close()
+        # make sure it has the right permissions too
+        os.chmod(out_file_name, 0600)
 
 def parse_options():
     parser = OptionParser()
@@ -373,7 +349,8 @@ def main():
 
     try:
         config = SSSDConfigFile(options.filename)
-    except SSSDConfigParser.ParsingError:
+    except SyntaxError:
+        verbose(traceback.format_exc(), options.verbose)
         print >>sys.stderr, "Cannot parse config file %s" % options.filename
         return 1
 
@@ -382,6 +359,7 @@ def main():
 
     version = config.get_version()
     if version == 2:
+        verbose("Looks like v2, only checking changes", options.verbose)
         try:
             config.v2_changes(options.outfile, options.backup)
         except Exception, e:
@@ -389,6 +367,7 @@ def main():
             verbose(traceback.format_exc(), options.verbose)
             return 1
     elif version == 1:
+        verbose("Looks like v1, performing full upgrade", options.verbose)
         try:
             config.upgrade_v2(options.outfile, options.backup)
         except Exception, e:
-- 
1.6.2.5

_______________________________________________
sssd-devel mailing list
sssd-devel@lists.fedorahosted.org
https://fedorahosted.org/mailman/listinfo/sssd-devel

Reply via email to