diff options
-rwxr-xr-x | install/tools/ipa-ldap-updater | 160 | ||||
-rw-r--r-- | ipapython/admintool.py | 229 | ||||
-rw-r--r-- | ipaserver/install/installutils.py | 92 | ||||
-rw-r--r-- | ipaserver/install/ipa_ldap_updater.py | 189 | ||||
-rw-r--r-- | ipaserver/install/ldapupdate.py | 2 | ||||
-rwxr-xr-x | make-lint | 1 |
6 files changed, 463 insertions, 210 deletions
diff --git a/install/tools/ipa-ldap-updater b/install/tools/ipa-ldap-updater index 8f5c76645..0fc5a5bc4 100755 --- a/install/tools/ipa-ldap-updater +++ b/install/tools/ipa-ldap-updater @@ -20,162 +20,6 @@ # Documentation can be found at http://freeipa.org/page/LdapUpdate -# TODO -# save undo files? +from ipaserver.install.ipa_ldap_updater import LDAPUpdater -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) +LDAPUpdater.run_cli() diff --git a/ipapython/admintool.py b/ipapython/admintool.py new file mode 100644 index 000000000..60096e083 --- /dev/null +++ b/ipapython/admintool.py @@ -0,0 +1,229 @@ +# Authors: +# Petr Viktorin <pviktori@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 903e8f185..388a11e26 100644 --- a/ipaserver/install/installutils.py +++ b/ipaserver/install/installutils.py @@ -32,12 +32,14 @@ import tempfile 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 000000000..0c7d940be --- /dev/null +++ b/ipaserver/install/ipa_ldap_updater.py @@ -0,0 +1,189 @@ +#!/usr/bin/python +# Authors: Rob Crittenden <rcritten@redhat.com> +# Petr Viktorin <pviktori@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 e75ee804a..c64139889 100644 --- a/ipaserver/install/ldapupdate.py +++ b/ipaserver/install/ldapupdate.py @@ -825,7 +825,7 @@ class LDAPUpdate: 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) @@ -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): |