>From dffe621d043cbbeb2e1d5794da720207cf503a25 Mon Sep 17 00:00:00 2001
From: Jakub Hrozek <jhrozek@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 <ssorce@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

