-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 [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 -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.10 (GNU/Linux) Comment: Using GnuPG with Fedora - http://enigmail.mozdev.org/ iEYEARECAAYFAksB5/4ACgkQHsardTLnvCUldACfRNgjEW9aWboF9Jv7RKKTA+ws Ys0AoOBp+s1K6pNmVPrBHgHUonT0It2m =ekhm -----END PGP SIGNATURE-----
>From dffe621d043cbbeb2e1d5794da720207cf503a25 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek <[email protected]> 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 <[email protected]> +# +# 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 1c537807703625f47f04af92747661ee6d6f186e Mon Sep 17 00:00:00 2001 From: Jakub Hrozek <[email protected]> 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/config/upgrade_config.py | 386 ++++++++++++++++++++++++++++++++++++ server/upgrade/upgrade_config.py | 407 -------------------------------------- 4 files changed, 429 insertions(+), 408 deletions(-) create mode 100644 server/config/upgrade_config.py delete mode 100644 server/upgrade/upgrade_config.py 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..bb63eae 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, opts): + return [ o for o in opts if o['type'] == 'section' ] + + def delete_option(self, opts, type, name, exclude_sections=False): + index, item = self.findOpts(opts, type, name, exclude_sections) + if item: + del opts[index] + + def get_option(self, opts, parent_name, name, type='option'): + subtree = None + if parent_name: + pindex, pdata = self.findOpts(opts, 'section', parent_name) + if not pdata: + return (-1, None) + subtree = pdata['value'] + else: + subtree = opts + return self.findOpts(subtree, type, name) + + def rename_opts(self, opts, parent_name, rename_kw, type='option'): + for new_name, old_name in rename_kw.items(): + index, item = self.get_option(opts, parent_name, old_name, type) + if item: + item['name'] = new_name + diff --git a/server/config/upgrade_config.py b/server/config/upgrade_config.py new file mode 100644 index 0000000..220a378 --- /dev/null +++ b/server/config/upgrade_config.py @@ -0,0 +1,386 @@ +#!/usr/bin/python +#coding=utf-8 + +# SSSD +# +# upgrade_config.py +# +# Copyright (C) Jakub Hrozek <[email protected]> 2009 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import shutil +import traceback +from optparse import OptionParser + +from ipachangeconf import openLocked +from ipachangeconf import SSSDChangeConf + +class SSSDConfigFile(SSSDChangeConf): + def __init__(self, filename): + SSSDChangeConf.__init__(self) + self.filename = filename + + 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(file_name + self.backup_suffix, 0600) + + def get_version(self): + ver = self.get_option(self.opts, '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, opts): + # remove Data Provider + srvlist = self.get_option(opts, '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(self.opts, 'section', 'dp') + + def _update_option(self, to_section_name, from_section_name, opts): + to_section = [ s for s in self.sections(self.opts) if s['name'].strip() == to_section_name ] + from_section = [ s for s in self.sections(self.opts) 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 " + enum = self.findOpts(domain, 'option', 'enumerate')[1] + if enum: + if enum['value'].upper() not in ['TRUE', 'FALSE']: + try: + enum['value'] = int(enum['value']) + except ValueError: + raise ValueError('Cannot convert value %s in domain %s' % (enum['value'], domain['name'])) + + if enum['value'] == 0: + enum['value'] = 'FALSE' + elif enum['value'] > 0: + enum['value'] = 'TRUE' + else: + raise ValueError('Cannot convert value %s in domain %s' % (enum['value'], domain['name'])) + + def _migrate_domain(self, domain): + # rename the section + domain['name'] = domain['name'].strip().replace('domains', 'domain') + + # Generic options - new:old + generic_kw = { 'min_id' : 'minId', + 'max_id': 'maxId', + 'timeout': 'timeout', + 'magic_private_groups' : 'magicPrivateGroups', + 'cache_credentials' : 'cache-credentials', + 'id_provider' : 'provider', + 'auth_provider' : 'auth-module', + 'access_provider' : 'access-module', + 'chpass_provider' : 'chpass-module', + 'use_fully_qualified_names' : 'useFullyQualifiedNames', + } + # Proxy options + proxy_kw = { 'proxy_pam_target' : 'pam-target', + 'proxy_lib_name' : 'libName', + } + # LDAP options - new:old + ldap_kw = { 'ldap_uri' : 'ldapUri', + 'ldap_schema' : 'ldapSchema', + 'ldap_default_bind_dn' : 'defaultBindDn', + 'ldap_default_authtok_type' : 'defaultAuthtokType', + 'ldap_default_authtok' : 'defaultAuthtok', + 'ldap_user_search_base' : 'userSearchBase', + 'ldap_user_search_scope' : 'userSearchScope', + 'ldap_user_search_filter' : 'userSearchFilter', + 'ldap_user_object_class' : 'userObjectClass', + 'ldap_user_name' : 'userName', + 'ldap_user_pwd' : 'userPassword', + 'ldap_user_uid_number' : 'userUidNumber', + 'ldap_user_gid_number' : 'userGidNumber', + 'ldap_user_gecos' : 'userGecos', + 'ldap_user_home_directory' : 'userHomeDirectory', + 'ldap_user_shell' : 'userShell', + 'ldap_user_uuid' : 'userUUID', + 'ldap_user_principal' : 'userPrincipal', + 'ldap_force_upper_case_realm' : 'force_upper_case_realm', + 'ldap_user_fullname' : 'userFullname', + 'ldap_user_member_of' : 'userMemberOf', + 'ldap_user_modify_timestamp' : 'modifyTimestamp', + 'ldap_group_search_base' : 'groupSearchBase', + 'ldap_group_search_scope' : 'groupSearchScope', + 'ldap_group_search_filter' : 'groupSearchFilter', + 'ldap_group_object_class' : 'groupObjectClass', + 'ldap_group_name' : 'groupName', + 'ldap_group_pwd' : 'userPassword', + 'ldap_group_gid_number' : 'groupGidNumber', + 'ldap_group_member' : 'groupMember', + 'ldap_group_uuid' : 'groupUUID', + 'ldap_group_modify_timestamp' : 'modifyTimestamp', + 'ldap_network_timeout' : 'network_timeout', + 'ldap_offline_timeout' : 'offline_timeout', + 'ldap_enumeration_refresh_timeout' : 'enumeration_refresh_timeout', + 'ldap_stale_time' : 'stale_time', + 'ldap_opt_timeout' : 'opt_timeout', + 'ldap_tls_reqcert' : 'tls_reqcert', + } + krb5_kw = { 'krb5_kdcip' : 'krb5KDCIP', + 'krb5_realm' : 'krb5REALM', + 'krb5_try_simple_upn' : 'krb5try_simple_upn', + 'krb5_changepw_principal' : 'krb5changepw_principle', + 'krb5_ccachedir' : 'krb5ccache_dir', + 'krb5_auth_timeout' : 'krb5auth_timeout', + 'krb5_ccname_template' : 'krb5ccname_template', + } + user_defaults_kw = { 'default_shell' : 'defaultShell', + 'base_directory' : 'baseDirectory', + } + + self._migrate_enumerate(domain['value']) + self.rename_opts(self.opts, domain['name'], generic_kw) + self.rename_opts(self.opts, domain['name'], proxy_kw) + self.rename_opts(self.opts, domain['name'], ldap_kw) + self.rename_opts(self.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) + 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['value'] == 'local': + self._update_option(domain['name'], 'user_defaults', user_defaults_kw.values()) + self.delete_option(self.opts, 'section', 'user_defaults') + self.rename_opts(self.opts, domain['name'], user_defaults_kw) + + def _migrate_domains(self): + for domain in [ s for s in self.sections(self.opts) if s['name'].startswith("domains/") ]: + self._migrate_domain(domain) + + def _update_if_exists(self, opt, to_name, from_section, from_name): + index, item = self.get_option(self.opts, 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', + 'debug_timestamps' : 'debug-timestamps', + 'command' : 'command', + 'timeout' : 'timeout', + } + + # rename services sections + names_kw = { 'nss' : 'services/nss', + 'pam' : 'services/pam', + 'dp' : 'services/dp', + } + self.rename_opts(self.opts, None, names_kw, 'section') + + # [sssd] - monitor service + 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 + nss_kw = { 'enum_cache_timeout' : 'EnumCacheTimeout', + 'entry_cache_timeout' : 'EntryCacheTimeout', + 'entry_cache_nowait_timeout' : 'EntryCacheNoWaitRefreshTimeout', + 'entry_negative_timeout ' : 'EntryNegativeTimeout', + 'filter_users' : 'filterUsers', + 'filter_groups' : 'filterGroups', + 'filter_users_in_groups' : 'filterUsersInGroups', + } + nss_kw.update(service_kw) + self._update_option('nss', 'services', service_kw.values()) + self.rename_opts(self.opts, 'nss', nss_kw) + + # [pam] - Authentication service + pam_kw = {} + pam_kw.update(service_kw) + self._update_option('pam', 'services', service_kw.values()) + self.rename_opts(self.opts, 'pam', pam_kw) + + # remove obsolete sections + self.delete_option(self.opts, 'section', 'services') + self.delete_option(self.opts, 'section', 'names') + self.delete_option(self.opts, 'section', 'domains') + self.delete_option(self.opts, '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) + + self._do_v2_changes(self.opts) + + # 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 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(self.opts) + + # 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() + parser.add_option("-f", "--file", + dest="filename", default="/etc/sssd/sssd.conf", + help="Set input file to FILE", metavar="FILE") + parser.add_option("-o", "--outfile", + dest="outfile", default=None, + help="Set output file to OUTFILE", metavar="OUTFILE") + parser.add_option("", "--no-backup", action="store_false", + dest="backup", default=True, + help="""Do not provide backup file after conversion. +The script copies the original file with the suffix .bak +by default""") + parser.add_option("-v", "--verbose", action="store_true", + dest="verbose", default=False, + help="Be verbose") + (options, args) = parser.parse_args() + if len(args) > 0: + print >>sys.stderr, "Stray arguments: %s" % ' '.join([a for a in args]) + return None + + # do the conversion in place by default + if not options.outfile: + options.outfile = options.filename + + return options + +def verbose(msg, verbose): + if verbose: + print msg + +def main(): + options = parse_options() + if not options: + print >>sys.stderr, "Cannot parse options" + return 1 + + try: + config = SSSDConfigFile(options.filename) + except SyntaxError: + verbose(traceback.format_exc(), options.verbose) + print >>sys.stderr, "Cannot parse config file %s" % options.filename + return 1 + + # make sure we keep strict settings when creating new files + os.umask(0077) + + 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: + print "ERROR: %s" % e + 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: + print "ERROR: %s" % e + verbose(traceback.format_exc(), options.verbose) + return 1 + else: + print >>sys.stderr, "Can only upgrade from v1 to v2, file %s looks like version %d" % (options.filename, config.get_version()) + return 1 + + return 0 + +if __name__ == "__main__": + ret = main() + sys.exit(ret) + diff --git a/server/upgrade/upgrade_config.py b/server/upgrade/upgrade_config.py deleted file mode 100644 index 213bb73..0000000 --- a/server/upgrade/upgrade_config.py +++ /dev/null @@ -1,407 +0,0 @@ -#!/usr/bin/python -#coding=utf-8 - -# SSSD -# -# upgrade_config.py -# -# Copyright (C) Jakub Hrozek <[email protected]> 2009 -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import 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) - - 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') - - def _backup_file(self): - " Copy the file we operate on to a backup location " - shutil.copy(self.file_name, self.file_name+".bak") - - # 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): - " 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: - try: - enumvalue = int(enumvalue) - except ValueError: - raise ValueError('Cannot convert value %s in domain %s' % (enumvalue, from_section)) - - if enumvalue == 0: - self._new_config.set(to_section, 'enumerate', 'FALSE') - elif enumvalue > 0: - self._new_config.set(to_section, 'enumerate', 'TRUE') - else: - raise ValueError('Cannot convert value %s in domain %s' % (enumvalue, from_section)) - - def _migrate_domain(self, domain): - new_domsec = 'domain/%s' % domain - old_domsec = 'domains/%s' % domain - self._new_config.add_section(new_domsec) - - # Generic options - new:old - generic_kw = { 'min_id' : 'minID', - 'max_id': 'maxID', - 'timeout': 'timeout', - 'magic_private_groups' : 'magicPrivateGroups', - 'cache_credentials' : 'cache-credentials', - 'id_provider' : 'provider', - 'auth_provider' : 'auth-module', - 'access_provider' : 'access-module', - 'chpass_provider' : 'chpass-module', - 'use_fully_qualified_names' : 'useFullyQualifiedNames', - } - # Proxy options - proxy_kw = { 'proxy_pam_target' : 'pam-target', - 'proxy_lib_name' : 'libName', - } - # LDAP options - new:old - ldap_kw = { 'ldap_uri' : 'ldapUri', - 'ldap_schema' : 'ldapSchema', - 'ldap_default_bind_dn' : 'defaultBindDn', - 'ldap_default_authtok_type' : 'defaultAuthtokType', - 'ldap_default_authtok' : 'defaultAuthtok', - 'ldap_user_search_base' : 'userSearchBase', - 'ldap_user_search_scope' : 'userSearchScope', - 'ldap_user_search_filter' : 'userSearchFilter', - 'ldap_user_object_class' : 'userObjectClass', - 'ldap_user_name' : 'userName', - 'ldap_user_pwd' : 'userPassword', - 'ldap_user_uid_number' : 'userUidNumber', - 'ldap_user_gid_number' : 'userGidNumber', - 'ldap_user_gecos' : 'userGecos', - 'ldap_user_home_directory' : 'userHomeDirectory', - 'ldap_user_shell' : 'userShell', - 'ldap_user_uuid' : 'userUUID', - 'ldap_user_principal' : 'userPrincipal', - 'ldap_force_upper_case_realm' : 'force_upper_case_realm', - 'ldap_user_fullname' : 'userFullname', - 'ldap_user_member_of' : 'userMemberOf', - 'ldap_user_modify_timestamp' : 'modifyTimestamp', - 'ldap_group_search_base' : 'groupSearchBase', - 'ldap_group_search_scope' : 'groupSearchScope', - 'ldap_group_search_filter' : 'groupSearchFilter', - 'ldap_group_object_class' : 'groupObjectClass', - 'ldap_group_name' : 'groupName', - 'ldap_group_pwd' : 'userPassword', - 'ldap_group_gid_number' : 'groupGidNumber', - 'ldap_group_member' : 'groupMember', - 'ldap_group_uuid' : 'groupUUID', - 'ldap_group_modify_timestamp' : 'modifyTimestamp', - 'ldap_network_timeout' : 'network_timeout', - 'ldap_offline_timeout' : 'offline_timeout', - 'ldap_enumeration_refresh_timeout' : 'enumeration_refresh_timeout', - 'ldap_stale_time' : 'stale_time', - 'ldap_opt_timeout' : 'opt_timeout', - 'ldap_tls_reqcert' : 'tls_reqcert', - } - krb5_kw = { 'krb5_kdcip' : 'krb5KDCIP', - 'krb5_realm' : 'krb5REALM', - 'krb5_try_simple_upn' : 'krb5try_simple_upn', - 'krb5_changepw_principal' : 'krb5changepw_principle', - 'krb5_ccachedir' : 'krb5ccache_dir', - 'krb5_auth_timeout' : 'krb5auth_timeout', - 'krb5_ccname_template' : 'krb5ccname_template', - } - user_defaults_kw = { 'default_shell' : 'defaultShell', - '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) - - # 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) - - # if domain was local, update with parameters from [user_defaults] - if prv == 'local': - self._migrate_kw(new_domsec, 'user_defaults', 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() - 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() - - # [service] - options common to all services, no section as in v1 - service_kw = { 'reconnection_retries' : 'reconnection_retries', - 'debug_level' : 'debug-level', - 'debug_timestamps' : 'debug-timestamps', - 'command' : 'command', - 'timeout' : 'timeout', - } - - # [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) - - # [nss] - Name service - self._new_config.add_section('nss') - nss_kw = { 'enum_cache_timeout' : 'EnumCacheTimeout', - 'entry_cache_timeout' : 'EntryCacheTimeout', - 'entry_cache_nowait_timeout' : 'EntryCacheNoWaitRefreshTimeout', - 'entry_negative_timeout ' : 'EntryNegativeTimeout', - 'filter_users' : 'filterUsers', - 'filter_groups' : 'filterGroups', - '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) - - # [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) - - # Migrate domains - self._migrate_domains() - - # Perform neccessary changes - 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 parse_options(): - parser = OptionParser() - parser.add_option("-f", "--file", - dest="filename", default="/etc/sssd/sssd.conf", - help="Set input file to FILE", metavar="FILE") - parser.add_option("-o", "--outfile", - dest="outfile", default=None, - help="Set output file to OUTFILE", metavar="OUTFILE") - parser.add_option("", "--no-backup", action="store_false", - dest="backup", default=True, - help="""Do not provide backup file after conversion. -The script copies the original file with the suffix .bak -by default""") - parser.add_option("-v", "--verbose", action="store_true", - dest="verbose", default=False, - help="Be verbose") - (options, args) = parser.parse_args() - if len(args) > 0: - print >>sys.stderr, "Stray arguments: %s" % ' '.join([a for a in args]) - return None - - # do the conversion in place by default - if not options.outfile: - options.outfile = options.filename - - return options - -def verbose(msg, verbose): - if verbose: - print msg - -def main(): - options = parse_options() - if not options: - print >>sys.stderr, "Cannot parse options" - return 1 - - try: - config = SSSDConfigFile(options.filename) - except SSSDConfigParser.ParsingError: - print >>sys.stderr, "Cannot parse config file %s" % options.filename - return 1 - - # make sure we keep strict settings when creating new files - os.umask(0077) - - version = config.get_version() - if version == 2: - try: - config.v2_changes(options.outfile, options.backup) - except Exception, e: - print "ERROR: %s" % e - verbose(traceback.format_exc(), options.verbose) - return 1 - elif version == 1: - try: - config.upgrade_v2(options.outfile, options.backup) - except Exception, e: - print "ERROR: %s" % e - verbose(traceback.format_exc(), options.verbose) - return 1 - else: - print >>sys.stderr, "Can only upgrade from v1 to v2, file %s looks like version %d" % (options.filename, config.get_version()) - return 1 - - return 0 - -if __name__ == "__main__": - ret = main() - sys.exit(ret) - -- 1.6.2.5
_______________________________________________ sssd-devel mailing list [email protected] https://fedorahosted.org/mailman/listinfo/sssd-devel
