diff options
Diffstat (limited to 'ipapython')
-rw-r--r-- | ipapython/admintool.py | 229 |
1 files changed, 229 insertions, 0 deletions
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) |