mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
230 lines
7.8 KiB
Python
230 lines
7.8 KiB
Python
|
|
# 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)
|