mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
Changed calling signature of output_for_cli(); started work on 'textui' backend plugin
This commit is contained in:
7
ipa
7
ipa
@@ -30,7 +30,12 @@ from ipalib import api
|
|||||||
from ipalib.cli import CLI
|
from ipalib.cli import CLI
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
# If we can't explicitly determin the encoding, we assume UTF-8:
|
||||||
|
if sys.stdin.encoding is None:
|
||||||
|
encoding = 'UTF-8'
|
||||||
|
else:
|
||||||
|
encoding = sys.stdin.encoding
|
||||||
cli = CLI(api,
|
cli = CLI(api,
|
||||||
(s.decode('utf-8') for s in sys.argv[1:])
|
(s.decode(encoding) for s in sys.argv[1:])
|
||||||
)
|
)
|
||||||
sys.exit(cli.run())
|
sys.exit(cli.run())
|
||||||
|
|||||||
219
ipalib/cli.py
219
ipalib/cli.py
@@ -22,20 +22,20 @@ Functionality for Command Line Interface.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import textwrap
|
||||||
import sys
|
import sys
|
||||||
import code
|
import code
|
||||||
import optparse
|
import optparse
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
import frontend
|
import frontend
|
||||||
|
import backend
|
||||||
import errors
|
import errors
|
||||||
import plugable
|
import plugable
|
||||||
import ipa_types
|
import ipa_types
|
||||||
from config import set_default_env, read_config
|
from config import set_default_env, read_config
|
||||||
import util
|
import util
|
||||||
|
from constants import CLI_TAB
|
||||||
def exit_error(error):
|
|
||||||
sys.exit('ipa: ERROR: %s' % error)
|
|
||||||
|
|
||||||
|
|
||||||
def to_cli(name):
|
def to_cli(name):
|
||||||
@@ -55,21 +55,189 @@ def from_cli(cli_name):
|
|||||||
return str(cli_name).replace('-', '_')
|
return str(cli_name).replace('-', '_')
|
||||||
|
|
||||||
|
|
||||||
class text_ui(frontend.Application):
|
class textui(backend.Backend):
|
||||||
"""
|
"""
|
||||||
Base class for CLI commands with special output needs.
|
Backend plugin to nicely format output to stdout.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def print_dashed(self, string, top=True, bottom=True):
|
def get_tty_width(self):
|
||||||
|
"""
|
||||||
|
Return the width (in characters) of output tty.
|
||||||
|
|
||||||
|
If stdout is not a tty, this method will return ``None``.
|
||||||
|
"""
|
||||||
|
if sys.stdout.isatty():
|
||||||
|
return 80 # FIXME: we need to return the actual tty width
|
||||||
|
|
||||||
|
def max_col_width(self, rows, col=None):
|
||||||
|
"""
|
||||||
|
Return the max width (in characters) of a specified column.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
>>> ui = textui()
|
||||||
|
>>> rows = [
|
||||||
|
... ('a', 'package'),
|
||||||
|
... ('an', 'egg'),
|
||||||
|
... ]
|
||||||
|
>>> ui.max_col_width(rows, col=0) # len('an')
|
||||||
|
2
|
||||||
|
>>> ui.max_col_width(rows, col=1) # len('package')
|
||||||
|
7
|
||||||
|
>>> ui.max_col_width(['a', 'cherry', 'py']) # len('cherry')
|
||||||
|
6
|
||||||
|
"""
|
||||||
|
if type(rows) not in (list, tuple):
|
||||||
|
raise TypeError(
|
||||||
|
'rows: need %r or %r; got %r' % (list, tuple, rows)
|
||||||
|
)
|
||||||
|
if len(rows) == 0:
|
||||||
|
return 0
|
||||||
|
if col is None:
|
||||||
|
return max(len(row) for row in rows)
|
||||||
|
return max(len(row[col]) for row in rows)
|
||||||
|
|
||||||
|
def print_dashed(self, string, above=True, below=True):
|
||||||
|
"""
|
||||||
|
Print a string with with a dashed line above and/or below.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
>>> ui = textui()
|
||||||
|
>>> ui.print_dashed('Dashed above and below.')
|
||||||
|
-----------------------
|
||||||
|
Dashed above and below.
|
||||||
|
-----------------------
|
||||||
|
>>> ui.print_dashed('Only dashed below.', above=False)
|
||||||
|
Only dashed below.
|
||||||
|
------------------
|
||||||
|
>>> ui.print_dashed('Only dashed above.', below=False)
|
||||||
|
------------------
|
||||||
|
Only dashed above.
|
||||||
|
"""
|
||||||
dashes = '-' * len(string)
|
dashes = '-' * len(string)
|
||||||
if top:
|
if above:
|
||||||
print dashes
|
print dashes
|
||||||
print string
|
print string
|
||||||
if bottom:
|
if below:
|
||||||
print dashes
|
print dashes
|
||||||
|
|
||||||
def print_name(self, **kw):
|
def print_line(self, text, width=None):
|
||||||
self.print_dashed('%s:' % self.name, **kw)
|
"""
|
||||||
|
Force printing on a single line, using ellipsis if needed.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
>>> ui = textui()
|
||||||
|
>>> ui.print_line('This line can fit!', width=18)
|
||||||
|
This line can fit!
|
||||||
|
>>> ui.print_line('This line wont quite fit!', width=18)
|
||||||
|
This line wont ...
|
||||||
|
|
||||||
|
The above example aside, you normally should not specify the
|
||||||
|
``width``. When you don't, it is automatically determined by calling
|
||||||
|
`textui.get_tty_width()`.
|
||||||
|
"""
|
||||||
|
if width is None:
|
||||||
|
width = self.get_tty_width()
|
||||||
|
if width is not None and width < len(text):
|
||||||
|
text = text[:width - 3] + '...'
|
||||||
|
print text
|
||||||
|
|
||||||
|
def print_indented(self, text, indent=1):
|
||||||
|
"""
|
||||||
|
Print at specified indentation level.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
>>> ui = textui()
|
||||||
|
>>> ui.print_indented('One indentation level.')
|
||||||
|
One indentation level.
|
||||||
|
>>> ui.print_indented('Two indentation levels.', indent=2)
|
||||||
|
Two indentation levels.
|
||||||
|
>>> ui.print_indented('No indentation.', indent=0)
|
||||||
|
No indentation.
|
||||||
|
"""
|
||||||
|
print (CLI_TAB * indent + text)
|
||||||
|
|
||||||
|
def print_name(self, name):
|
||||||
|
"""
|
||||||
|
Print a command name.
|
||||||
|
|
||||||
|
The typical use for this is to mark the start of output from a
|
||||||
|
command. For example, a hypothetical ``show_status`` command would
|
||||||
|
output something like this:
|
||||||
|
|
||||||
|
>>> ui = textui()
|
||||||
|
>>> ui.print_name('show_status')
|
||||||
|
------------
|
||||||
|
show-status:
|
||||||
|
------------
|
||||||
|
"""
|
||||||
|
self.print_dashed('%s:' % to_cli(name))
|
||||||
|
|
||||||
|
def print_keyval(self, rows, indent=1):
|
||||||
|
"""
|
||||||
|
Print (key = value) pairs, one pair per line.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
>>> items = [
|
||||||
|
... ('in_server', True),
|
||||||
|
... ('mode', 'production'),
|
||||||
|
... ]
|
||||||
|
>>> ui = textui()
|
||||||
|
>>> ui.print_keyval(items)
|
||||||
|
in_server = True
|
||||||
|
mode = 'production'
|
||||||
|
>>> ui.print_keyval(items, indent=0)
|
||||||
|
in_server = True
|
||||||
|
mode = 'production'
|
||||||
|
|
||||||
|
Also see `textui.print_indented`.
|
||||||
|
"""
|
||||||
|
for row in rows:
|
||||||
|
self.print_indented('%s = %r' % row, indent)
|
||||||
|
|
||||||
|
def print_count(self, count, singular, plural=None):
|
||||||
|
"""
|
||||||
|
Print a summary count.
|
||||||
|
|
||||||
|
The typical use for this is to print the number of items returned
|
||||||
|
by a command, especially when this return count can vary. This
|
||||||
|
preferably should be used as a summary and should be the final text
|
||||||
|
a command outputs.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
>>> ui = textui()
|
||||||
|
>>> ui.print_count(1, '%d goose', '%d geese')
|
||||||
|
-------
|
||||||
|
1 goose
|
||||||
|
-------
|
||||||
|
>>> ui.print_count(['Don', 'Sue'], 'Found %d user', 'Found %d users')
|
||||||
|
-------------
|
||||||
|
Found 2 users
|
||||||
|
-------------
|
||||||
|
|
||||||
|
If ``count`` is not an integer, it must be a list or tuple, and then
|
||||||
|
``len(count)`` is used as the count.
|
||||||
|
"""
|
||||||
|
if type(count) is not int:
|
||||||
|
assert type(count) in (list, tuple)
|
||||||
|
count = len(count)
|
||||||
|
self.print_dashed(
|
||||||
|
self.choose_number(count, singular, plural)
|
||||||
|
)
|
||||||
|
|
||||||
|
def choose_number(self, n, singular, plural=None):
|
||||||
|
if n == 1 or plural is None:
|
||||||
|
return singular % n
|
||||||
|
return plural % n
|
||||||
|
|
||||||
|
|
||||||
|
def exit_error(error):
|
||||||
|
sys.exit('ipa: ERROR: %s' % error)
|
||||||
|
|
||||||
|
|
||||||
class help(frontend.Application):
|
class help(frontend.Application):
|
||||||
@@ -87,10 +255,8 @@ class help(frontend.Application):
|
|||||||
self.application.build_parser(cmd).print_help()
|
self.application.build_parser(cmd).print_help()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class console(frontend.Application):
|
class console(frontend.Application):
|
||||||
'Start the IPA interactive Python console.'
|
"""Start the IPA interactive Python console."""
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
code.interact(
|
code.interact(
|
||||||
@@ -99,7 +265,7 @@ class console(frontend.Application):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class show_api(text_ui):
|
class show_api(frontend.Application):
|
||||||
'Show attributes on dynamic API object'
|
'Show attributes on dynamic API object'
|
||||||
|
|
||||||
takes_args = ('namespaces*',)
|
takes_args = ('namespaces*',)
|
||||||
@@ -153,7 +319,7 @@ class show_api(text_ui):
|
|||||||
self.__traverse_namespace(n, attr, lines, tab + 2)
|
self.__traverse_namespace(n, attr, lines, tab + 2)
|
||||||
|
|
||||||
|
|
||||||
class plugins(text_ui):
|
class plugins(frontend.Application):
|
||||||
"""Show all loaded plugins"""
|
"""Show all loaded plugins"""
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
@@ -162,21 +328,13 @@ class plugins(text_ui):
|
|||||||
(p.plugin, p.bases) for p in plugins
|
(p.plugin, p.bases) for p in plugins
|
||||||
)
|
)
|
||||||
|
|
||||||
def output_for_cli(self, result):
|
def output_for_cli(self, textui, result, **kw):
|
||||||
self.print_name()
|
textui.print_name(self.name)
|
||||||
first = True
|
|
||||||
for (plugin, bases) in result:
|
for (plugin, bases) in result:
|
||||||
if first:
|
textui.print_indented(
|
||||||
first = False
|
'%s: %s' % (plugin, ', '.join(bases))
|
||||||
else:
|
)
|
||||||
print ''
|
textui.print_count(result, '%d plugin loaded', '%s plugins loaded')
|
||||||
print ' Plugin: %s' % plugin
|
|
||||||
print ' In namespaces: %s' % ', '.join(bases)
|
|
||||||
if len(result) == 1:
|
|
||||||
s = '1 plugin loaded.'
|
|
||||||
else:
|
|
||||||
s = '%d plugins loaded.' % len(result)
|
|
||||||
self.print_dashed(s)
|
|
||||||
|
|
||||||
|
|
||||||
cli_application_commands = (
|
cli_application_commands = (
|
||||||
@@ -293,6 +451,7 @@ class CLI(object):
|
|||||||
self.api.load_plugins()
|
self.api.load_plugins()
|
||||||
for klass in cli_application_commands:
|
for klass in cli_application_commands:
|
||||||
self.api.register(klass)
|
self.api.register(klass)
|
||||||
|
self.api.register(textui)
|
||||||
|
|
||||||
def bootstrap(self):
|
def bootstrap(self):
|
||||||
"""
|
"""
|
||||||
@@ -376,7 +535,7 @@ class CLI(object):
|
|||||||
try:
|
try:
|
||||||
ret = cmd(**kw)
|
ret = cmd(**kw)
|
||||||
if callable(cmd.output_for_cli):
|
if callable(cmd.output_for_cli):
|
||||||
cmd.output_for_cli(ret)
|
cmd.output_for_cli(self.api.Backend.textui, ret, **kw)
|
||||||
return 0
|
return 0
|
||||||
except socket.error, e:
|
except socket.error, e:
|
||||||
print e[1]
|
print e[1]
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
All constants centralized in one file.
|
All constants centralized in one file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Used for a tab (or indentation level) when formatting for CLI:
|
||||||
|
CLI_TAB = ' ' # Two spaces
|
||||||
|
|
||||||
# The section to read in the config files, i.e. [global]
|
# The section to read in the config files, i.e. [global]
|
||||||
CONFIG_SECTION = 'global'
|
CONFIG_SECTION = 'global'
|
||||||
|
|||||||
@@ -24,22 +24,11 @@ Misc frontend plugins.
|
|||||||
from ipalib import api, Command, Param, Bool
|
from ipalib import api, Command, Param, Bool
|
||||||
|
|
||||||
|
|
||||||
class env_and_context(Command):
|
# FIXME: We should not let env return anything in_server
|
||||||
"""
|
# when mode == 'production'. This would allow an attacker to see the
|
||||||
Base class for `env` and `context` commands.
|
# configuration of the server, potentially revealing compromising
|
||||||
"""
|
# information. However, it's damn handy for testing/debugging.
|
||||||
|
class env(Command):
|
||||||
def run(self, **kw):
|
|
||||||
if kw.get('server', False) and not self.api.env.in_server:
|
|
||||||
return self.forward()
|
|
||||||
return self.execute()
|
|
||||||
|
|
||||||
def output_for_cli(self, ret):
|
|
||||||
for (key, value) in ret:
|
|
||||||
print '%s = %r' % (key, value)
|
|
||||||
|
|
||||||
|
|
||||||
class env(env_and_context):
|
|
||||||
"""Show environment variables"""
|
"""Show environment variables"""
|
||||||
|
|
||||||
takes_options = (
|
takes_options = (
|
||||||
@@ -48,26 +37,19 @@ class env(env_and_context):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def run(self, **kw):
|
||||||
|
if kw.get('server', False) and not self.api.env.in_server:
|
||||||
|
return self.forward()
|
||||||
|
return self.execute()
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
return tuple(
|
return tuple(
|
||||||
(key, self.api.env[key]) for key in self.api.env
|
(key, self.api.env[key]) for key in self.api.env
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def output_for_cli(self, textui, result, **kw):
|
||||||
|
textui.print_name(self.name)
|
||||||
|
textui.print_keyval(result)
|
||||||
|
textui.print_count(result, '%d variable', '%d variables')
|
||||||
|
|
||||||
api.register(env)
|
api.register(env)
|
||||||
|
|
||||||
|
|
||||||
class context(env_and_context):
|
|
||||||
"""Show request context"""
|
|
||||||
|
|
||||||
takes_options = (
|
|
||||||
Param('server?', type=Bool(), default=False,
|
|
||||||
doc='Show request context in server',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def execute(self):
|
|
||||||
return [
|
|
||||||
(key, self.api.context[key]) for key in self.api.Context
|
|
||||||
]
|
|
||||||
|
|
||||||
api.register(context)
|
|
||||||
|
|||||||
@@ -25,6 +25,31 @@ from tests.util import raises, get_api, ClassChecker
|
|||||||
from ipalib import cli, plugable, frontend, backend
|
from ipalib import cli, plugable, frontend, backend
|
||||||
|
|
||||||
|
|
||||||
|
class test_textui(ClassChecker):
|
||||||
|
_cls = cli.textui
|
||||||
|
|
||||||
|
def test_max_col_width(self):
|
||||||
|
"""
|
||||||
|
Test the `ipalib.cli.textui.max_col_width` method.
|
||||||
|
"""
|
||||||
|
o = self.cls()
|
||||||
|
e = raises(TypeError, o.max_col_width, 'hello')
|
||||||
|
assert str(e) == 'rows: need %r or %r; got %r' % (list, tuple, 'hello')
|
||||||
|
rows = [
|
||||||
|
'hello',
|
||||||
|
'naughty',
|
||||||
|
'nurse',
|
||||||
|
]
|
||||||
|
assert o.max_col_width(rows) == len('naughty')
|
||||||
|
rows = (
|
||||||
|
( 'a', 'bbb', 'ccccc'),
|
||||||
|
('aa', 'bbbb', 'cccccc'),
|
||||||
|
)
|
||||||
|
assert o.max_col_width(rows, col=0) == 2
|
||||||
|
assert o.max_col_width(rows, col=1) == 4
|
||||||
|
assert o.max_col_width(rows, col=2) == 6
|
||||||
|
|
||||||
|
|
||||||
def test_to_cli():
|
def test_to_cli():
|
||||||
"""
|
"""
|
||||||
Test the `ipalib.cli.to_cli` function.
|
Test the `ipalib.cli.to_cli` function.
|
||||||
|
|||||||
Reference in New Issue
Block a user