File: //usr/lib/python2.7/site-packages/redhat_support_tool/plugins/__init__.py
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from collections import deque
from optparse import OptionParser, Option
from redhat_support_tool.helpers.confighelper import _
from redhat_support_tool.helpers.common import set_docstring
import cmd
import gettext
import itertools
import os
import redhat_support_tool.helpers.common as common
import shlex
import sys
import textwrap
__author__ = 'Keith Robertson <kroberts@redhat.com>'
class DisplayOption(object):
'''
A simple container class that holds the text to be displayed
in a numbered menu and the name of the function that should
be called when the user selects that numbered menu option.
'''
display_text = None
function_name = None
def __init__(self, display_text, function_name):
self.display_text = display_text
self.function_name = function_name
class ObjectDisplayOption(DisplayOption):
'''
A simple container class that holds the text to be displayed
in a numbered menu and the name of the function that should
be called when the user selects that numbered menu option.
'''
stored_obj = None
def __init__(self, display_text, function_name, stored_obj):
DisplayOption.__init__(self, display_text, function_name)
self.stored_obj = stored_obj
class HiddenCommand(object):
'''
A marker interface for plug-ins that you do not want to show
to the user. Simply inherit from this class multiple-inhertance
style
'''
def __init(self):
pass
class Plugin(object):
'''
The base class for plugins.
Attributes:
plugin_name The variable is used by redhat-support-tool
as the command which is displayed to the user.
Example: Setting 'plugin_name = foobar' will
create an executable command named 'foobar'.
Example:
Welcome to the Red Hat Support Tool.
Command (? for help): foobar
Command (? for help): help foobar
_args The positional arguments on the command line left over
from running OptionParser. Should be used by
subclasses to see what the user supplied.
Example: addcomment -c 123456 positional_arguments_here
_options A dictionary containing the options that were supplied
by the user.
Example: addcomment -c 123456 positional_arguments_here
results in _options = {'comment': '123456'}
_line The unaltered STDIN line from the user
Methods to override:
- get_usage (required)
- get_desc (required)
- get_epilog (required)
- get_options (optional depending on your command)
- validate_args (recommended)
- postinit (optional)
- non_interactive_action (required)
- config_help
- config-set-option
- config-get-option
'''
plugin_name = None
_line = None
_args = None
_parser = None
_options = None
def __init__(self):
self._line = None
self._args = None
self._options = None
self._init_parser()
#
# Methods subclasses should override
#
# Override this
@classmethod
def get_usage(cls):
'''
The usage statement that will be printed by OptionParser.
Example:
- %prog -c CASENUMBER [options] <comment text here>
Important: %prog is a OptionParser built-in. Use it!
'''
return 'OVERRIDE ME: Plugin::get_usage'
# Override this
@classmethod
def get_desc(cls):
'''
The description statement that will be printed by OptionParser.
Example:
- 'Use the \'%s\' command to add a comment to a case.'\
% cls.plugin_name
'''
return 'OVERRIDE ME: Plugin::get_desc'
# Override this
@classmethod
def get_epilog(cls):
'''
The epilog string that will be printed by OptionParser. Usually
used to print an example of how to use the program.
Example:
Examples:
- %s -c 12345678 Lorem ipsum dolor sit amet, consectetur adipisicing
- %s -c 12345678
'''
return ''
# Override this
@classmethod
def get_options(cls):
'''
Subclasses that need command line options should override this method
and return an array of optparse.Option(s) to be used by the
OptionParser.
Example:
return [Option("-f", "--file", action="store",
dest="filename", help='Some file'),
Option("-c", "--case",
action="store", dest="casenumber",
help='A case')]
Would produce the following:
Command (? for help): help mycommand
Usage: mycommand [options]
Use the 'mycommand' command to find a knowledge base solution by ID
Options:
-h, --help show this help message and exit
-f, --file Some file
-c, --case A case
Example:
- mycommand -c 12345 -f abc.txt
'''
return None
def parse_args(self, line):
'''
Use this method to parse the arguments supplied by the user.
This method will parse the given arguments from STDIN via
the OptionParser. It will set _args, _options, and _line
so that subclasses can use them to see what the user provided.
'''
if common.is_interactive():
if line != None:
self._args = shlex.split(line)
else:
self._args = sys.argv[2:]
self._options, self._args = self._parser.parse_args(self._args)
self._line = line
self._options = vars(self._options)
# Override this
def validate_args(self):
'''
A helper method that will be called by the framework logic after the
plugin has been instantiated and the args have been processed by
the base class's OptionParser. You should place any logic in here
to test for the requisite number of arguments, etc.
Throws:
An exception if _args or _options lack the requisite amount of data
for the command to operate.
Returns:
Nothing
Example:
if len(self._args) <= 0:
msg = _("ERROR: %s requires a knowledge base solution ID. "
"Try \'help %s\' for more information.") % \
(self.plugin_name,
self.plugin_name)
print msg
raise Exception(msg)
'''
pass
# Override this
def postinit(self):
'''
This method is called immediately after the validate_args
method. The intent is to place logic in here that
is executed *after* you know that you have the requisite
args from the user but aren't ready to send anything to
STDOUT yet.
This is useful for subclasses that have interactive and
non-interactive behavior and want to co-locate some init
logic.
'''
pass
# Override this
def insert_obj(self, obj):
'''
This method should be called prior to validate_args, and can
be used to inject an object from an ObjectDisplayOption by
LaunchHelper.
'''
pass
# Override this
def non_interactive_action(self):
'''
This method will be called by redhat-support-tool when the user
issues the command in a non-interactive mode.
It will be called after the constructor, postinit, and validate_args.
All plugin's should implement this method.
Example:
redhat-support-tool addcomment -c 12345 'Problem solved!'
'''
print 'OVERRIDE ME: Plugin::action'
@classmethod
def get_name(cls):
"""Returns the plugin's name as a string. This should return a
lowercase string.
"""
if cls.plugin_name:
return cls.plugin_name
return cls.__name__.lower()
#
# Methods related to OptionParser
#
@classmethod
def _init_parser(cls):
# Python 2.4 compatability check.
if sys.version_info[:2] >= (2, 5):
OptionParser.format_epilog = lambda self, formatter: self.epilog
OptionParser.format_description = \
lambda self, formatter: self.description
cls._parser = OptionParser(usage=cls.get_usage(),
description=cls.get_desc(),
prog=cls.get_name(),
epilog=cls.get_epilog())
else:
OptionParser.format_description = \
lambda self, formatter: self.description
cls._parser = OptionParser(usage=cls.get_usage(),
description=cls.get_desc(),
prog=cls.get_name())
# Check to see if the subclass has any optparse.Options
# for the OptionParser.
if cls.get_options():
cls._parser.add_options(cls.get_options())
# OptionParser will annoyingly call sys.exit when parse_args
# is called with an invalid set of options. Clearly, this doesn't
# work well in this context. Hence, we override this behavior.
setattr(cls._parser,
'error',
getattr(cls,
'_print_opt_parse_error'))
@classmethod
def _print_opt_parse_error(cls, msg):
'''
A utility function to override OptionParser's annoying
habit of calling sys.exit on invalid parameters.
'''
print msg
@classmethod
def show_command_help(cls):
'''
This function will display OptionParser help for the command.
'''
cls._init_parser()
cls._parser.print_help()
if sys.version_info[:2] <= (2, 5) and cls.get_epilog() != "":
print cls.get_epilog()
#
# Methods related to configuration options.
#
# Override this
@classmethod
def config_help(self):
'''
If your plugin stores any options that can be set by the user
override this function and return a string containing the options,
one per line, with the option name and a description, including
any default values.
Example:
return " %-10s: %-67s\n" % ('url',
_('The support services URL. Default=%s') % self.DEFAULT_URL)
'''
return ''
class InteractivePlugin(Plugin, cmd.Cmd):
'''
A helper class for plug-ins that need an interactive sub-menu.
Some plug-ins require an interactive sub-menu (see example 1).
These plug-ins should inherit from this class and implement the methods
marked by '# Override this'. These classes should also
override the similarly marked methods in Plugin.
Plug-ins that are subclasses of InteractivePlugin will have their
superclass's 'cmdloop' function called when the user is running
redhat-support-tool interactively. This will allow the user
to select the options that you have provided.
Methods to override:
- get_sub_menu_options (requied)
- get_intro_text (optional)
- get_prompt_text (optional)
- get_more_options (optional)
Example 1:
$ redhat-support-tool
Welcome to the Red Hat Support Tool.
Command (? for help): <-- This is the main menu
Command (? for help): listcases
Type the number of the case to view or 'e' to return to the main menu.
0 [Closed] This is just a test
1 [Waiting on Red Hat] Test case for strata
Select a case: <-- This is a sub-menu
'''
DEFAULT_INTRO_TEXT = _('Make a selection or \'e\' '
'to return to the main menu.')
DEFAULT_PROMPT = _('Selection: ')
DEFAULT_END_OF_ENTRIES = _('End of options.')
DEFAULT_PARTIAL_ENTRIES = _('%s of %s entries printed.'
' Type \'m\' to see more, or \'r\' to start'
' from the beginning again.')
DEFAULT_MORE_ENTRIES_MAYBE = _('More entries may be available.'
' Type \'m\' to try and retrieve more.')
intro_text = DEFAULT_INTRO_TEXT
prompt = DEFAULT_PROMPT
end_of_entries = DEFAULT_END_OF_ENTRIES
partial_entries = DEFAULT_PARTIAL_ENTRIES
more_entries_maybe = DEFAULT_MORE_ENTRIES_MAYBE
_sub_menu_index = 1
help_is_options = True
opts_updated = False
def __init__(self,
intro_text=DEFAULT_INTRO_TEXT,
prompt=DEFAULT_PROMPT):
'''
Arguments:
intro_text - Command specific intro-text which is used when the
sub-menu is displayed.
prompt - Command specific prompt(eg. Show case:, Select section:,
etc.)
'''
cmd.Cmd.__init__(self)
Plugin.__init__(self)
if intro_text == None or intro_text == self.DEFAULT_INTRO_TEXT:
self.intro_text = self.get_intro_text()
if prompt == None or prompt == self.DEFAULT_PROMPT:
self.prompt = self.get_prompt_text()
# Override this
def get_sub_menu_options(self):
'''
Override this method to tell this base class what your sub-menu
options are.
Sub-class implementations should return a collections.deque containing
DisplayOption objects. Items are printed in the order in which they
are added.
Example 1:
deque.append(DisplayOption('Display Option 1', 'function1')
deque.append(DisplayOption('Display Option 2', 'function1')
Produces:
Make a selection or type 'e' to return to the main menu.
0 Display Option 1
1 Display Option 2
Selection:
Example 2: A sub-menu:
$ redhat-support-tool
Welcome to the Red Hat Support Tool.
Command (? for help): <-- This is the main menu
Command (? for help): listcases
Type the number of the case to view or 'e' to return to the main menu.
0 [Closed] This is just a test
1 [Waiting on Red Hat] Test case for strata
Select a case: <-- This is a sub-menu
'''
return None
# Override this
def get_intro_text(self):
'''
If you want to supply a sub-menu intro text (see example)
other than the default and you don't want to supply it via
the constructor. Override this.
Example:
Command (? for help): listcases
Select a case. <-- This is the submenu intro
0 [Closed] This is just a test
'''
return self.DEFAULT_INTRO_TEXT
# Override this
def get_prompt_text(self):
'''
If you want to supply custom a sub-menu prompt (see example)
other than the default and you don't want to supply it via
the constructor. Override this.
Example:
Command (? for help): listcases
Select a case.
0 [Closed] This is just a test
Selection: <-- this is the prompt
'''
return self.DEFAULT_PROMPT
# Override this
# pylint: disable=W0613
def get_more_options(self, num_options):
'''
If you want to support fetching of additional records for display
in the submenu, override this method.
If there are additional entries get_sub_menu_options() will be
called to obtain the updated deque() object.
Returns True if additional entries are available
Returns False if unsupported, or no additional entries are available
'''
return False
#
# Nothing to override below this point
#
def _print_submenu(self):
'''
This method will call get_sub_menu_options an print them
to stdout as a selectable list for the user. Generally,
no need to override this.
'''
terminfo = common.get_terminfo()
paginate = False
display_opt_deque = self.get_sub_menu_options()
currentpos = self._sub_menu_index
moreresults = False
# If we have terminal information available (i.e. interactive &
# via a known terminal) calculate number of entries to return
# in one screen.
if terminfo:
paginate = True
termheight = terminfo[0]
termwidth = terminfo[1]
else:
termheight = 24
termwidth = 80
# It seems strange to get the len of a str of a len, but we need
# to, so we can get an accurate width of the index column.
idx_width = len(str(len(display_opt_deque)))
opt_width = termwidth - idx_width - 2
# We need to work out the min & max size of the headers
intro_prompt_size = common.get_linecount(termwidth, True,
self.intro_text) + \
common.get_linecount(termwidth, True,
self.prompt)
min_header_size = intro_prompt_size + \
common.get_linecount(termwidth, False,
self.partial_entries,
self.more_entries_maybe,
self.end_of_entries)
max_header_size = intro_prompt_size + \
common.get_linecount(termwidth, False,
self.partial_entries,
self.more_entries_maybe,
self.end_of_entries)
if len(display_opt_deque) <= (currentpos + (termheight -
min_header_size)):
# Prefetch some more results now
moreresults = self.get_more_options(termheight - min_header_size)
# If we are going to run out of options during this
# _print_submenu call, or there will none left once we have
# completed printing. Try and get more options from the plugin.
display_opt_deque = self.get_sub_menu_options()
# If we have reached the end of the list, remind the user
# and return from the function.
if (currentpos > len(display_opt_deque)):
print self.end_of_entries
return
if paginate:
lines_to_fill = termheight - max_header_size - 1
else:
lines_to_fill = sys.maxint
iter_entries = list(itertools.islice(display_opt_deque,
currentpos - 1, (lines_to_fill +
currentpos - 1)))
outputbuff = []
# Print intro text
outputbuff.append(self.intro_text)
for display_opt, idx in itertools.izip(iter_entries,
itertools.count(currentpos)):
if paginate:
output = " % *s %-*s" % (idx_width, idx,
opt_width, display_opt.display_text)
output_wrapped = textwrap.wrap(output, termwidth,
subsequent_indent=' ' *
(idx_width + 2))
if (len(outputbuff) + len(output_wrapped) +
max_header_size) > termheight:
break
else:
outputbuff.extend(output_wrapped)
self._sub_menu_index = idx + 1
else:
output = " % *s %-s" % (idx_width, idx,
display_opt.display_text)
outputbuff.append(output)
for line in outputbuff:
print line
if (self._sub_menu_index <= len(display_opt_deque)):
print self.partial_entries % (self._sub_menu_index - 1,
len(display_opt_deque))
elif ((self._sub_menu_index - 1) == len(display_opt_deque)
and moreresults):
print self.more_entries_maybe
else:
print self.end_of_entries
#
# Methods related to shell interaction. Nothing to see here move
# along please ;)
#
def _invalid(self, line):
print _('%s is an invalid selection. Type \'help\' to see '
'valid selections again.') % line
def emptyline(self):
'''
Override the default implementation of emptyline so
that the last command isn't repeated.
'''
return None
def precmd(self, line):
num = -1
try:
num = int(line)
# pylint: disable=W0702
except:
line = str(line).strip()
if line == 'e' or line == 'q':
return 'EOF'
elif (line == 'help') or \
(line == '') or \
(line == 'm') or \
(line == 'r') or \
(line == '?') or \
(line.startswith('shell')) or \
(line.startswith('!')):
return line
elif num <= len(self.get_sub_menu_options()) and num > 0:
num = num - 1
display_opt_deque = self.get_sub_menu_options()
func = getattr(self, display_opt_deque[num].function_name)
func(display_opt_deque[num])
if self.opts_updated:
self._sub_menu_index = 1
self._print_submenu()
self.opts_updated = False
return ''
else:
self._invalid(line)
return ''
def do_help(self, line):
if not line:
# Help can either be the options from the submenu, or
# a listing of docstrings from the help_???? methods
if self.help_is_options:
# Plugin is using the options from _print_submenu as help
# reset index to 1, and print again.
self._sub_menu_index = 1
self._print_submenu()
else:
common.do_help(self)
else:
cmd.Cmd.do_help(self, line)
def do_m(self, line):
self._print_submenu()
@set_docstring(_('Show more options if available.'))
def help_m(self):
print
print '\n'.join([_('Prints additional results. '
'if available.')])
def do_r(self, line):
self._sub_menu_index = 1
self._print_submenu()
@set_docstring(_('Restart display of options.'))
def help_r(self):
print
print '\n'.join([_('Restarts display of results from '
'the start.')])
@set_docstring(_('Return to previous menu.'))
def help_e(self):
print
print '\n'.join([_('Exit this subcommand shell. '
'CTRL-D and CTRL-C also work.')])
@set_docstring(_('Return to previous menu.'))
def help_q(self):
print
self.help_e()
def do_EOF(self, line):
# EOF (^D) doesn't start a new line, lets do that
# so it looks better.
print
return 'EOF'
def do_shell(self, line):
output = os.popen(line).read()
print output
@set_docstring(_('Execute a shell command. You can also use \'!\''))
def help_shell(self):
print
print '\n'.join(['shell COMMAND',
_('Execute a shell command. You can also use \'!\''),
_('Example:'),
' shell ls',
' !ls'])
def default(self, line):
if 'EOF' == str(line).strip():
return True