summaryrefslogtreecommitdiffstats
path: root/ipapython/admintool.py
diff options
context:
space:
mode:
Diffstat (limited to 'ipapython/admintool.py')
-rw-r--r--ipapython/admintool.py229
1 files changed, 229 insertions, 0 deletions
diff --git a/ipapython/admintool.py b/ipapython/admintool.py
new file mode 100644
index 00000000..60096e08
--- /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)