On 07/19/2012 08:01 PM, John Dennis wrote:
Overall I really like the approach, good work.

I have not applied and run the patch, so my comments are based only on
code reading.

I have just a small things which might need changing.

One of the ideas in the log manager is to easily support per class
logging (borrowed from Java). This allows you to enable/disable or
otherwise configure logging per logical code unit as opposed to a big
global hammer (i.e. root logger). It also allows for the class name
(logger name) to show up in the log output which can be really handy so
you know which class is emitting a message (we don't currently turn this
on but it's a trival one-line change to the log format spec.).

Each significant class should initialize it's own logging, which is very
easy to do with this line at the top of a class's __init__()

     log_mgr.get_logger(self, True)

so instead of:

     self.logger.info()

you would say:

     self.info()

A fair amount of the code in the framework is doing this now, but the
install code was never cleaned up. That was left for another day, I
guess that day is here.

Updated. I also added the necessary lint exception.
I'm curious as to why it works that way, though. Normally, to put methods in a class, you would use a base class/mixin. Why this dynamic magic?

Also each log method (i.e. info(), warn(), error(), etc) should pass
it's format substitutions as parameters rather than preformatting the
message before submitting it to the logger. This is a performance
optimization because the logger delays building the message until it
knows the message will actually be emitted and not discarded (common
case with debug messages).

So instead of:

     self.debug("the contents of this big dict is: %s" % my_dict)

You would write:

     self.debug("the contents of this big dict is: %s", my_dict)

A small different in coding, but a lot less work at run time.

Fixed, thanks for noticing.

I'm not sure the following message handling is optimal

+        message = '\n'.join(
+            "The hostname resolves to the localhost address
(127.0.0.1/::1)",
+            "Please change your /etc/hosts file so that the hostname",
+            "resolves to the ip address of your network interface.",
+            "",
+            "Please fix your /etc/hosts file and restart the setup
program",


I'm not a big fan of individual lines of text in the source that are
glued together or are emitted individually via a series of print
statements. I'd rather see multi-line text as multi-line text using
Python's multi-line string operator (e.g. ''' or """). That way you can
see the text as it's meant to appear, but there is a bigger reason.

You're right, fixed. To keep sane indentation I used textwrap.dedent.

Shouldn't this message be internationalized? (e.g. wrapped in _()). When
a message is internationalized it's even more important for multi-line
text to appear in it entirety to give translators the greatest context
and latitude for their translation rather than arbitrarily splitting it
up into fragments according to English norms (fragments that might not
even translate in another language).

Also, a quick glance suggests a number of the messages need i18n treatment.

None of the tools are internationalized. Perhaps they need to be, but it should be a separate ticket.

Overall though, I really love the approach, great work!

Thanks for the review :)

--
PetrĀ³


From cfa0d80b78bf9ea8651a0364703154c47eae9ff6 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Fri, 20 Apr 2012 04:39:59 -0400
Subject: [PATCH] Framework for admin/install tools, with ipa-ldap-updater

Currently, FreeIPA's install/admin scripts are long pieces of code
that aren't very reusable, importable, or testable.
They have been extended over time with features such as logging and
error handling, but since each tool was extended individually, there
is much inconsistency and code duplication.
This patch starts a framework which the admin tools can use, and
converts ipa-ldap-updater to use the framework.

Common tasks the tools do -- option parsing, validation, logging
setup, error handling -- are represented as methods. Individual
tools can extend, override or reuse the defaults as they see fit.

The ipa-ldap-updater has two modes (normal and --upgrade) that
don't share much functionality. They are represented by separate
classes. Option parsing, and selecting which class to run, happens
before they're instantiated.

All code is moved to importable modules to aid future testing. The
only thing that remains in the ipa-ldap-updater script is a two-line
call to the library.

First part of the work for:
https://fedorahosted.org/freeipa/ticket/2652
---
 install/tools/ipa-ldap-updater        |  206 ++++-------------------------
 ipapython/admintool.py                |  229 +++++++++++++++++++++++++++++++++
 ipaserver/install/installutils.py     |   92 ++++++-------
 ipaserver/install/ipa_ldap_updater.py |  189 +++++++++++++++++++++++++++
 ipaserver/install/ldapupdate.py       |    2 +-
 make-lint                             |    1 +
 6 files changed, 486 insertions(+), 233 deletions(-)
 rewrite install/tools/ipa-ldap-updater (86%)
 create mode 100644 ipapython/admintool.py
 create mode 100644 ipaserver/install/ipa_ldap_updater.py

diff --git a/install/tools/ipa-ldap-updater b/install/tools/ipa-ldap-updater
dissimilarity index 86%
index 8f5c76645d9ba2b204f3b1051d9dc8b23eacce9d..0fc5a5bc448d04eb9ce8562ee1feebbf8f0fe356 100755
--- a/install/tools/ipa-ldap-updater
+++ b/install/tools/ipa-ldap-updater
@@ -1,181 +1,25 @@
-#!/usr/bin/python
-# Authors: Rob Crittenden <rcrit...@redhat.com>
-#
-# Copyright (C) 2008  Red Hat
-# see file 'COPYING' for use and warranty information
-#
-# 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/>.
-#
-
-# Documentation can be found at http://freeipa.org/page/LdapUpdate
-
-# TODO
-# save undo files?
-
-import os
-import sys
-try:
-    from ipapython.config import IPAOptionParser
-    from ipapython import ipautil, config
-    from ipaserver.install import installutils
-    from ipaserver.install.ldapupdate import LDAPUpdate, BadSyntax, UPDATES_DIR
-    from ipaserver.install.upgradeinstance import IPAUpgrade
-    from ipapython import sysrestore
-    import krbV
-    from ipalib import api
-    from ipapython.ipa_log_manager import *
-except ImportError:
-    print >> sys.stderr, """\
-There was a problem importing one of the required Python modules. The
-error was:
-
-    %s
-""" % sys.exc_value
-    sys.exit(1)
-
-def parse_options():
-    usage = "%prog [options] input_file(s)\n"
-    usage += "%prog [options]\n"
-    parser = IPAOptionParser(usage=usage, formatter=config.IPAFormatter())
-
-    parser.add_option("-d", "--debug", action="store_true", dest="debug",
-                      help="Display debugging information about the update(s)",
-                      default=False)
-    parser.add_option("-t", "--test", action="store_true", dest="test",
-                      help="Run through the update without changing anything",
-                      default=False)
-    parser.add_option("-y", dest="password",
-                      help="File containing the Directory Manager password")
-    parser.add_option("-l", '--ldapi', action="store_true", dest="ldapi",
-                      default=False, help="Connect to the LDAP server using the ldapi socket")
-    parser.add_option("-u", '--upgrade', action="store_true", dest="upgrade",
-                      default=False, help="Upgrade an installed server in offline mode")
-    parser.add_option("-p", '--plugins', action="store_true", dest="plugins",
-                      default=False, help="Execute update plugins. Always true when applying all update files.")
-    parser.add_option("-W", '--password', action="store_true",
-                      dest="ask_password",
-                      help="Prompt for the Directory Manager password")
-
-    options, args = parser.parse_args()
-    safe_options = parser.get_safe_opts(options)
-
-    return safe_options, options, args
-
-def get_dirman_password():
-    """Prompt the user for the Directory Manager password and verify its
-       correctness.
-    """
-    password = installutils.read_password("Directory Manager", confirm=False, validate=False)
-
-    return password
-
-def main():
-    badsyntax = False
-    upgradefailed = False
-
-    safe_options, options, args = parse_options()
-
-    run_plugins = options.plugins
-
-    files = []
-    if len(args) > 0:
-        files = args
-
-    if len(files) < 1:
-        run_plugins = True
-
-    if os.getegid() == 0:
-        try:
-            installutils.check_server_configuration()
-        except RuntimeError, e:
-            print unicode(e)
-            sys.exit(1)
-    else:
-        if not os.path.exists('/etc/ipa/default.conf'):
-            print "IPA is not configured on this system."
-            sys.exit(1)
-        if options.upgrade:
-            sys.exit('Upgrade can only be done as root')
-        if run_plugins:
-            sys.exit('Plugins can only be run as root.')
-
-    dirman_password = ""
-    if options.password:
-        pw = ipautil.template_file(options.password, [])
-        dirman_password = pw.strip()
-    else:
-        if (options.ask_password or not options.ldapi) and not options.upgrade:
-            dirman_password = get_dirman_password()
-            if dirman_password is None:
-                sys.exit("\nDirectory Manager password required")
-
-    console_format = '%(levelname)s: %(message)s'
-    if options.upgrade:
-        standard_logging_setup('/var/log/ipaupgrade.log', debug=options.debug,
-                console_format=console_format, filemode='a')
-    else:
-        standard_logging_setup(None, console_format=console_format,
-                debug=options.debug)
-
-    cfg = dict (
-        in_server=True,
-        context='updates',
-        debug=options.debug,
-    )
-    api.bootstrap(**cfg)
-    api.finalize()
-
-    updates = None
-    if options.upgrade:
-        root_logger.debug('%s was invoked with arguments %s and options: %s' % (sys.argv[0], args, safe_options))
-        realm = krbV.default_context().default_realm
-        upgrade = IPAUpgrade(realm, files, live_run=not options.test)
-        upgrade.create_instance()
-        modified = upgrade.modified
-        badsyntax = upgrade.badsyntax
-        upgradefailed = upgrade.upgradefailed
-    else:
-        ld = LDAPUpdate(dm_password=dirman_password, sub_dict={}, live_run=not options.test, ldapi=options.ldapi, plugins=run_plugins)
-        if len(files) < 1:
-            files = ld.get_all_files(UPDATES_DIR)
-        modified = ld.update(files)
-
-    if badsyntax:
-        root_logger.info('Bad syntax detected in upgrade file(s).')
-        print 'Bad syntax detected in upgrade file(s).'
-        return 1
-    elif upgradefailed:
-        root_logger.info('IPA upgrade failed.')
-        print 'IPA upgrade failed.'
-        return 1
-    elif modified and options.test:
-        root_logger.info('Update complete, changes to be made, test mode')
-        return 2
-    else:
-        root_logger.info('Update complete')
-        return 0
-
-try:
-    if __name__ == "__main__":
-        sys.exit(main())
-except BadSyntax, e:
-    print "There is a syntax error in this update file:"
-    print "  %s" % e
-    sys.exit(1)
-except RuntimeError, e:
-    sys.exit(e)
-except SystemExit, e:
-    sys.exit(e)
-except KeyboardInterrupt, e:
-    sys.exit(1)
+#!/usr/bin/python
+# Authors: Rob Crittenden <rcrit...@redhat.com>
+#
+# Copyright (C) 2008  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# 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/>.
+#
+
+# Documentation can be found at http://freeipa.org/page/LdapUpdate
+
+from ipaserver.install.ipa_ldap_updater import LDAPUpdater
+
+LDAPUpdater.run_cli()
diff --git a/ipapython/admintool.py b/ipapython/admintool.py
new file mode 100644
index 0000000000000000000000000000000000000000..60096e083ef209e943886cbe33189e5cdf063787
--- /dev/null
+++ b/ipapython/admintool.py
@@ -0,0 +1,229 @@
+# Authors:
+#   Petr Viktorin <pvikt...@redhat.com>
+#
+# Copyright (C) 2012  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# 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/>.
+
+"""A common framework for command-line admin tools, e.g. install scripts
+
+Handles common operations like option parsing and logging
+"""
+
+import sys
+import os
+import traceback
+from optparse import OptionGroup
+
+from ipapython import version
+from ipapython import config
+from ipapython import ipa_log_manager
+
+
+class ScriptError(StandardError):
+    """An exception that records an error message and a return value
+    """
+    def __init__(self, msg='', rval=1):
+        self.msg = msg
+        self.rval = rval
+
+    def __str__(self):
+        return self.msg or ''
+
+
+class AdminTool(object):
+    """Base class for command-line admin tools
+
+    To run the tool, call the main() classmethod with a list of command-line
+    arguments.
+    Alternatively, call run_cli() to run with command-line arguments in
+    sys.argv, and call sys.exit() with the return value.
+
+    Some commands actually represent multiple related tools, e.g.
+    ``ipa-server-install`` and ``ipa-server-install --uninstall`` would be
+    represented by separate classes. Only their options are the same.
+
+    To handle this, AdminTool provides classmethods for option parsing
+    and selecting the appropriate command class.
+
+    A class-wide option parser is made by calling add_options.
+    The options are then parsed into options and arguments, and
+    get_command_class is called with those to retrieve the class.
+    That class is then instantiated and run.
+
+    Running consists of a few steps:
+    - validating options or the environment (validate_options)
+    - setting up logging (setup_logging)
+    - running the actual command (run)
+
+    Any unhandled exceptions are handled in handle_error.
+    And at the end, either log_success or log_failure is called.
+
+    Class attributes to define in subclasses:
+    command_name - shown in logs
+    log_file_name - if None, logging is to stderr only
+    needs_root - if true, non-root users can't run the tool
+    usage - text shown in help
+    """
+    command_name = None
+    log_file_name = None
+    needs_root = False
+    usage = None
+
+    _option_parsers = dict()
+
+    @classmethod
+    def make_parser(cls):
+        """Create an option parser shared across all instances of this class"""
+        parser = config.IPAOptionParser(version=version.VERSION,
+            usage=cls.usage, formatter=config.IPAFormatter())
+        cls.option_parser = parser
+        cls.add_options(parser)
+
+    @classmethod
+    def add_options(cls, parser):
+        """Add command-specific options to the option parser"""
+        parser.add_option("-d", "--debug", dest="debug", default=False,
+            action="store_true", help="print debugging information")
+
+    @classmethod
+    def run_cli(cls):
+        """Run this command with sys.argv, exit process with the return value
+        """
+        sys.exit(cls.main(sys.argv))
+
+    @classmethod
+    def main(cls, argv):
+        """The main entry point
+
+        Parses command-line arguments, selects the actual command class to use
+        based on them, and runs that command.
+
+        :param argv: Command-line arguments.
+        :return: Command exit code
+        """
+        if cls not in cls._option_parsers:
+            # We use cls._option_parsers, a dictionary keyed on class, to check
+            # if we need to create a parser. This is because cls.option_parser
+            # can refer to the parser of a superclass.
+            cls.make_parser()
+            cls._option_parsers[cls] = cls.option_parser
+
+        options, args = cls.option_parser.parse_args(argv[1:])
+
+        command_class = cls.get_command_class(options, args)
+        command = command_class(options, args)
+
+        return command.execute()
+
+    @classmethod
+    def get_command_class(cls, options, args):
+        return cls
+
+    def __init__(self, options, args):
+        self.options = options
+        self.args = args
+        self.safe_options = self.option_parser.get_safe_opts(options)
+
+    def execute(self):
+        """Do everything needed after options are parsed
+
+        This includes validating options, setting up logging, doing the
+        actual work, and handling the result.
+        """
+        try:
+            self.validate_options()
+            self.ask_for_options()
+            self.setup_logging()
+            return_value = self.run()
+        except BaseException, exception:
+            traceback = sys.exc_info()[2]
+            error_message, return_value = self.handle_error(exception)
+            if return_value:
+                self.log_failure(error_message, return_value, exception,
+                    traceback)
+                return return_value
+        self.log_success()
+        return return_value
+
+    def validate_options(self):
+        """Validate self.options
+
+        It's also possible to compute and store information that will be
+        useful later, but no changes to the system should be made here.
+        """
+        if self.needs_root and os.getegid() != 0:
+            raise ScriptError('Must be root to run %s' % self.command_name, 1)
+
+    def ask_for_options(self):
+        """Ask for missing options interactively
+
+        Similar to validate_options. This is separate method because we want
+        any validation errors to abort the script before bothering the user
+        with prompts.
+        """
+        pass
+
+    def setup_logging(self):
+        """Set up logging"""
+        ipa_log_manager.standard_logging_setup(
+            self.log_file_name, debug=self.options.debug)
+        ipa_log_manager.log_mgr.get_logger(self, True)
+
+    def handle_error(self, exception):
+        """Given an exception, return a message (or None) and process exit code
+        """
+        if isinstance(exception, ScriptError):
+            return exception.msg, exception.rval or 1
+        elif isinstance(exception, SystemExit):
+            if isinstance(exception.code, int):
+                return None, exception.code
+            return str(exception.code), 1
+
+        return str(exception), 1
+
+    def run(self):
+        """Actual running of the command
+
+        This is where the hard work is done. The base implementation logs
+        the invocation of the command.
+
+        If this method returns (i.e. doesn't raise an exception), the tool is
+        assumed to have run successfully, and the return value is used as the
+        SystemExit code.
+        """
+        self.debug('%s was invoked with arguments %s and options: %s',
+                self.command_name, self.args, self.safe_options)
+
+    def log_failure(self, error_message, return_value, exception, backtrace):
+        try:
+            self.log
+        except AttributeError:
+            # Logging was not set up yet
+            print >> sys.stderr, '\n', error_message
+        else:
+            self.info(''.join(traceback.format_tb(backtrace)))
+            self.info('The %s command failed, exception: %s: %s',
+                self.command_name, type(exception).__name__, exception)
+            if error_message:
+                self.error(error_message)
+
+    def log_success(self):
+        try:
+            self.log
+        except AttributeError:
+            pass
+        else:
+            self.info('The %s command was successful', self.command_name)
diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index 903e8f185c54182e800eda547b80b380f24ae8e4..388a11e26000a045a13c35ec54e02a2b5a2ea41e 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -32,12 +32,14 @@
 import shutil
 from ConfigParser import SafeConfigParser
 import traceback
+import textwrap
 
 from dns import resolver, rdatatype
 from dns.exception import DNSException
 import ldap
 
-from ipapython import ipautil, sysrestore
+from ipapython import ipautil, sysrestore, admintool
+from ipapython.admintool import ScriptError
 from ipapython.ipa_log_manager import *
 from ipalib.util import validate_hostname
 from ipapython import config
@@ -61,18 +63,6 @@ class HostReverseLookupError(HostLookupError):
 class HostnameLocalhost(HostLookupError):
     pass
 
-
-class ScriptError(StandardError):
-    """An exception that records an error message and a return value
-    """
-    def __init__(self, msg = '', rval = 1):
-        self.msg = msg
-        self.rval = rval
-
-    def __str__(self):
-        return self.msg
-
-
 class ReplicaConfig:
     def __init__(self):
         self.realm_name = ""
@@ -639,65 +629,65 @@ def run_script(main_function, operation_name, log_file_name=None,
             sys.exit(return_value)
 
     except BaseException, error:
-        handle_error(error, log_file_name)
+        message, exitcode = handle_error(error, log_file_name)
+        if message:
+            print >> sys.stderr, message
+        sys.exit(exitcode)
 
 
 def handle_error(error, log_file_name=None):
-    """Handle specific errors"""
+    """Handle specific errors. Returns a message and return code"""
 
     if isinstance(error, SystemExit):
-        sys.exit(error)
+        if isinstance(error.code, int):
+            return None, error.code
+        elif error.code is None:
+            return None, 0
+        else:
+            return str(error), 1
     if isinstance(error, RuntimeError):
-        sys.exit(error)
+        return str(error), 1
     if isinstance(error, KeyboardInterrupt):
-        print >> sys.stderr, "Cancelled."
-        sys.exit(1)
+        return "Cancelled.", 1
 
-    if isinstance(error, ScriptError):
-        if error.msg:
-            print >> sys.stderr, error.msg
-        sys.exit(error.rval)
+    if isinstance(error, admintool.ScriptError):
+        return error.msg, error.rval
 
     if isinstance(error, socket.error):
-        print >> sys.stderr, error
-        sys.exit(1)
+        return error, 1
 
     if isinstance(error, ldap.INVALID_CREDENTIALS):
-        print >> sys.stderr, "Invalid password"
-        sys.exit(1)
+        return "Invalid password", 1
     if isinstance(error, ldap.INSUFFICIENT_ACCESS):
-        print >> sys.stderr, "Insufficient access"
-        sys.exit(1)
+        return "Insufficient access", 1
     if isinstance(error, ldap.LOCAL_ERROR):
-        print >> sys.stderr, error.args[0]['info']
-        sys.exit(1)
+        return error.args[0]['info'], 1
     if isinstance(error, ldap.SERVER_DOWN):
-        print >> sys.stderr, error.args[0]['desc']
-        sys.exit(1)
+        return error.args[0]['desc'], 1
     if isinstance(error, ldap.LDAPError):
-        print >> sys.stderr, 'LDAP error: %s' % type(error).__name__
-        print >> sys.stderr, error.args[0]['info']
-        sys.exit(1)
+        return 'LDAP error: %s\n%s' % (
+            type(error).__name__, error.args[0]['info']), 1
 
     if isinstance(error, config.IPAConfigError):
-        print >> sys.stderr, "An IPA server to update cannot be found. Has one been configured yet?"
-        print >> sys.stderr, "The error was: %s" % error
-        sys.exit(1)
+        message = "An IPA server to update cannot be found. Has one been configured yet?"
+        message += "\nThe error was: %s" % error
+        return message, 1
     if isinstance(error, errors.LDAPError):
-        print >> sys.stderr, "An error occurred while performing operations: %s" % error
-        sys.exit(1)
+        return "An error occurred while performing operations: %s" % error, 1
 
     if isinstance(error, HostnameLocalhost):
-        print >> sys.stderr, "The hostname resolves to the localhost address (127.0.0.1/::1)"
-        print >> sys.stderr, "Please change your /etc/hosts file so that the hostname"
-        print >> sys.stderr, "resolves to the ip address of your network interface."
-        print >> sys.stderr, ""
-        print >> sys.stderr, "Please fix your /etc/hosts file and restart the setup program"
-        sys.exit(1)
+        message = textwrap.dedent("""
+            The hostname resolves to the localhost address (127.0.0.1/::1)
+            Please change your /etc/hosts file so that the hostname
+            resolves to the ip address of your network interface.
+
+            Please fix your /etc/hosts file and restart the setup program
+            """).strip()
+        return message, 1
 
     if log_file_name:
-        print >> sys.stderr, "Unexpected error - see %s for details:" % log_file_name
+        message = "Unexpected error - see %s for details:" % log_file_name
     else:
-        print >> sys.stderr, "Unexpected error"
-    print >> sys.stderr, '%s: %s' % (type(error).__name__, error)
-    sys.exit(1)
+        message = "Unexpected error"
+    message += '\n%s: %s' % (type(error).__name__, error)
+    return message, 1
diff --git a/ipaserver/install/ipa_ldap_updater.py b/ipaserver/install/ipa_ldap_updater.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c7d940be9b0d9777b7739dc5b63a0fe79b534b9
--- /dev/null
+++ b/ipaserver/install/ipa_ldap_updater.py
@@ -0,0 +1,189 @@
+#!/usr/bin/python
+# Authors: Rob Crittenden <rcrit...@redhat.com>
+#          Petr Viktorin <pvikt...@redhat.com>
+#
+# Copyright (C) 2008  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# 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/>.
+#
+
+# Documentation can be found at http://freeipa.org/page/LdapUpdate
+
+# TODO
+# save undo files?
+
+import os
+
+import krbV
+
+from ipalib import api
+from ipapython import ipautil, admintool
+from ipaserver.install import installutils
+from ipaserver.install.ldapupdate import LDAPUpdate, UPDATES_DIR
+from ipaserver.install.upgradeinstance import IPAUpgrade
+from ipapython import ipa_log_manager
+
+
+class LDAPUpdater(admintool.AdminTool):
+    command_name = 'ipa-ldap-updater'
+
+    usage = "%prog [options] input_file(s)\n"
+    usage += "%prog [options]\n"
+
+    @classmethod
+    def add_options(cls, parser):
+        super(LDAPUpdater, cls).add_options(parser)
+
+        parser.add_option("-t", "--test", action="store_true", dest="test",
+            default=False,
+            help="Run through the update without changing anything")
+        parser.add_option("-y", dest="password",
+            help="File containing the Directory Manager password")
+        parser.add_option("-l", '--ldapi', action="store_true", dest="ldapi",
+            default=False,
+            help="Connect to the LDAP server using the ldapi socket")
+        parser.add_option("-u", '--upgrade', action="store_true",
+            dest="upgrade", default=False,
+            help="Upgrade an installed server in offline mode")
+        parser.add_option("-p", '--plugins', action="store_true",
+            dest="plugins", default=False,
+            help="Execute update plugins. " +
+                "Always true when applying all update files.")
+        parser.add_option("-W", '--password', action="store_true",
+            dest="ask_password",
+            help="Prompt for the Directory Manager password")
+
+    @classmethod
+    def get_command_class(cls, options, args):
+        if options.upgrade:
+            return LDAPUpdater_Upgrade
+        else:
+            return LDAPUpdater_NonUpgrade
+
+    def validate_options(self):
+        options = self.options
+        super(LDAPUpdater, self).validate_options()
+
+        self.files = self.args
+
+        for filename in self.files:
+            if not os.path.exists(filename):
+                raise admintool.ScriptError("%s: file not found" % filename)
+
+        if os.getegid() == 0:
+            installutils.check_server_configuration()
+        elif not os.path.exists('/etc/ipa/default.conf'):
+            raise admintool.ScriptError(
+                "IPA is not configured on this system.")
+
+        if options.password:
+            pw = ipautil.template_file(options.password, [])
+            self.dirman_password = pw.strip()
+        else:
+            self.dirman_password = None
+
+    def setup_logging(self):
+        ipa_log_manager.standard_logging_setup(self.log_file_name,
+            console_format='%(levelname)s: %(message)s',
+            debug=self.options.debug, filemode='a')
+        ipa_log_manager.log_mgr.get_logger(self, True)
+
+    def run(self):
+        super(LDAPUpdater, self).run()
+
+        api.bootstrap(
+                in_server=True,
+                context='updates',
+                debug=self.options.debug,
+            )
+        api.finalize()
+
+    def handle_error(self, exception):
+        return installutils.handle_error(exception, self.log_file_name)
+
+
+class LDAPUpdater_Upgrade(LDAPUpdater):
+    needs_root = True
+    log_file_name = '/var/log/ipaupgrade.log'
+
+    def validate_options(self):
+        if os.getegid() != 0:
+            raise admintool.ScriptError('Must be root to do an upgrade.', 1)
+
+        super(LDAPUpdater_Upgrade, self).validate_options()
+
+    def run(self):
+        super(LDAPUpdater_Upgrade, self).run()
+        options = self.options
+
+        updates = None
+        realm = krbV.default_context().default_realm
+        upgrade = IPAUpgrade(realm, self.files, live_run=not options.test)
+        upgrade.create_instance()
+        upgradefailed = upgrade.upgradefailed
+
+        if upgrade.badsyntax:
+            raise admintool.ScriptError(
+                'Bad syntax detected in upgrade file(s).', 1)
+        elif upgrade.upgradefailed:
+            raise admintool.ScriptError('IPA upgrade failed.', 1)
+        elif upgrade.modified and options.test:
+            self.info('Update complete, changes to be made, test mode')
+            return 2
+
+
+class LDAPUpdater_NonUpgrade(LDAPUpdater):
+    def validate_options(self):
+        super(LDAPUpdater_NonUpgrade, self).validate_options()
+        options = self.options
+
+        # Only run plugins if no files are given
+        self.run_plugins = not self.files or options.plugins
+
+        # Need root for running plugins
+        if self.run_plugins and os.getegid() != 0:
+            raise admintool.ScriptError('Plugins can only be run as root.', 1)
+
+    def ask_for_options(self):
+        super(LDAPUpdater_NonUpgrade, self).ask_for_options()
+        options = self.options
+        if not self.dirman_password:
+            if options.ask_password or not options.ldapi:
+                password = installutils.read_password("Directory Manager",
+                    confirm=False, validate=False)
+                if password is None:
+                    raise admintool.ScriptError(
+                        "Directory Manager password required")
+                self.dirman_password = password
+
+    def run(self):
+        super(LDAPUpdater_NonUpgrade, self).run()
+        options = self.options
+
+        ld = LDAPUpdate(
+            dm_password=self.dirman_password,
+            sub_dict={},
+            live_run=not options.test,
+            ldapi=options.ldapi,
+            plugins=options.plugins or self.run_plugins)
+
+        if not self.files:
+            self.files = ld.get_all_files(UPDATES_DIR)
+
+        modified = ld.update(self.files)
+
+        if modified and options.test:
+            self.info('Update complete, changes to be made, test mode')
+            return 2
diff --git a/ipaserver/install/ldapupdate.py b/ipaserver/install/ldapupdate.py
index e75ee804a1d201512f2770156eef73cf1bb1e7bb..c64139889d9f84866ac0cd358ed3a3a7d95af7dc 100644
--- a/ipaserver/install/ldapupdate.py
+++ b/ipaserver/install/ldapupdate.py
@@ -825,7 +825,7 @@ def update(self, files):
                     data = self.read_file(f)
                 except Exception, e:
                     print e
-                    sys.exit(1)
+                    sys.exit(e)
 
                 (all_updates, dn_list) = self.parse_update_file(data, all_updates, dn_list)
 
diff --git a/make-lint b/make-lint
index f619260434e33886175f5b7d5a1d008466f92a54..05a1bb14f3e1cfdb5404d9a0987f699f6d63428c 100755
--- a/make-lint
+++ b/make-lint
@@ -73,6 +73,7 @@ class IPATypeChecker(TypeChecker):
         'ipalib.session.SessionManager' : ['log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'],
         'ipalib.session.SessionCCache' : ['log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'],
         'ipalib.session.MemcacheSessionManager' : ['log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'],
+        'ipapython.admintool.AdminTool' : ['log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'],
     }
 
     def _related_classes(self, klass):
-- 
1.7.10.4

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to