Ian Cordasco d1a88bad18 Add --format to the client shell
Add a new way of formatting our output in a consistent way. This turns
print_list and print_dict into a formatter that has the same API as any
other formatter and allows users to create their own formatters and plug
them into cratonclient.

This includes tests for the base level formatter and our two default
formatters as well as some refactoring to allow users to specify their
own --format.

At the moment, however, the subcommand shells do *not* use the pluggable
formatter decided by the user. That change and all of the downstream
effects it has on testing is going to be *very* significant and deserves
its own commit as this one is large enough.

Change-Id: I6649ebce57d5ddf2d4aeb689e77e3c17ef3a2e97
2017-02-27 14:31:59 -06:00

208 lines
8.0 KiB
Python

# 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.
"""Main shell for parsing arguments directed toward Craton."""
from __future__ import print_function
import argparse
import sys
from oslo_utils import encodeutils
from oslo_utils import importutils
from stevedore import extension
from cratonclient import __version__
from cratonclient import exceptions as exc
from cratonclient import session as craton
from cratonclient.common import cliutils
from cratonclient.v1 import client
FORMATTERS_NAMESPACE = 'cratonclient.formatters'
class CratonShell(object):
"""Class used to handle shell definition and parsing."""
def __init__(self):
"""Initialize our shell object.
This sets up our formatters extension manager. If we add further
managers, they will be initialized here.
"""
self.extension_mgr = extension.ExtensionManager(
namespace=FORMATTERS_NAMESPACE,
invoke_on_load=False,
)
def get_base_parser(self):
"""Configure base craton arguments and parsing."""
parser = argparse.ArgumentParser(
prog='craton',
description=__doc__.strip(),
epilog='See "craton help COMMAND" '
'for help on a specific command.',
add_help=False,
formatter_class=argparse.HelpFormatter
)
parser.add_argument('-h', '--help',
action='store_true',
help=argparse.SUPPRESS,
)
parser.add_argument('--version',
action='version',
version=__version__,
)
parser.add_argument('--format',
default='default',
choices=list(sorted(self.extension_mgr.names())),
help='The format to use to print the information '
'to the console. Defaults to pretty-printing '
'using ASCII tables.',
)
parser.add_argument('--craton-url',
default=cliutils.env('CRATON_URL'),
help='The base URL of the running Craton service.'
' Defaults to env[CRATON_URL].',
)
parser.add_argument('--craton-version',
type=int,
default=cliutils.env('CRATON_VERSION',
default=1),
help='The version of the Craton API to use. '
'Defaults to env[CRATON_VERSION].'
)
parser.add_argument('--os-project-id',
default=cliutils.env('OS_PROJECT_ID'),
help='The project ID used to authenticate to '
'Craton. Defaults to env[OS_PROJECT_ID].',
)
parser.add_argument('--os-username',
default=cliutils.env('OS_USERNAME'),
help='The username used to authenticate to '
'Craton. Defaults to env[OS_USERNAME].',
)
parser.add_argument('--os-password',
default=cliutils.env('OS_PASSWORD'),
help='The password used to authenticate to '
'Craton. Defaults to env[OS_PASSWORD].',
)
return parser
# NOTE(cmspence): Credit for this get_subcommand_parser function
# goes to the magnumclient developers and contributors.
def get_subcommand_parser(self, api_version):
"""Get subcommands by parsing COMMAND_MODULES."""
parser = self.get_base_parser()
self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>',
dest='subparser_name')
shell = importutils.import_versioned_module(
'cratonclient.shell',
api_version,
'shell',
)
command_modules = shell.COMMAND_MODULES
for command_module in command_modules:
self._find_subparsers(subparsers, command_module)
self._find_subparsers(subparsers, self)
return parser
# NOTE(cmspence): Credit for this function goes to the
# magnumclient developers and contributors.
def _find_subparsers(self, subparsers, actions_module):
"""Find subparsers by looking at *_shell files."""
help_formatter = argparse.HelpFormatter
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
command = attr[3:].replace('_', '-')
callback = getattr(actions_module, attr)
desc = callback.__doc__ or ''
action_help = desc.strip()
arguments = getattr(callback, 'arguments', [])
subparser = (subparsers.add_parser(command,
help=action_help,
description=desc,
add_help=False,
formatter_class=help_formatter)
)
subparser.add_argument('-h', '--help',
action='help',
help=argparse.SUPPRESS)
self.subcommands[command] = subparser
for (args, kwargs) in arguments:
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=callback)
def main(self, argv):
"""Main entry-point for cratonclient shell argument parsing."""
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
subcommand_parser = (
self.get_subcommand_parser(options.craton_version)
)
self.parser = subcommand_parser
if options.help or not argv:
self.parser.print_help()
return 0
args = subcommand_parser.parse_args(argv)
# Short-circuit and deal with help right away.
if args.func == self.do_help:
self.do_help(args)
return 0
session = craton.Session(
username=args.os_username,
token=args.os_password,
project_id=args.os_project_id,
)
self.cc = client.Client(session, args.craton_url)
formatter_class = self.extension_mgr[args.format].plugin
args.formatter = formatter_class(args)
args.func(self.cc, args)
@cliutils.arg(
'command',
metavar='<subcommand>',
nargs='?',
help='Display help for <subcommand>.')
def do_help(self, args):
"""Display help about this program or one of its subcommands."""
if args.command:
if args.command in self.subcommands:
self.subcommands[args.command].print_help()
else:
raise exc.CommandError("'%s' is not a valid subcommand" %
args.command)
else:
self.parser.print_help()
def main():
"""Main entry-point for cratonclient's CLI."""
try:
CratonShell().main([encodeutils.safe_decode(a) for a in sys.argv[1:]])
except Exception as e:
print("ERROR: {}".format(encodeutils.exception_to_unicode(e)),
file=sys.stderr)
sys.exit(1)
return 0
if __name__ == "__main__":
main()