Implement a rough CLI foundation
This is a foundation for CLI work, still has rough spots but works for two PoC commands - rack-list and rack-show (communicates with Tuskar, prints). There's remaining stuff to be solved: * Allow arbitrary formatting of attributes (bug #1213056) * Add help support for subcommands (bug #1213050) * Allow finding resources by name (bug #1213053) * Allow auth with pre-existing token and Keystone URL (bug #1213052) Fixes bug #1211826 Change-Id: I9364be37c7111c85ef46be82b157782a14743004
This commit is contained in:
parent
ac1875bd52
commit
add9e5da58
@ -24,6 +24,34 @@ from tuskarclient import exc
|
||||
from tuskarclient.openstack.common import importutils
|
||||
|
||||
|
||||
def define_commands_from_module(subparsers, command_module):
|
||||
'''Find all methods beginning with 'do_' in a module, and add them
|
||||
as commands into a subparsers collection.
|
||||
'''
|
||||
for method_name in (a for a in dir(command_module) if a.startswith('do_')):
|
||||
# Commands should be hypen-separated instead of underscores.
|
||||
command = method_name[3:].replace('_', '-')
|
||||
callback = getattr(command_module, method_name)
|
||||
define_command(subparsers, command, callback)
|
||||
|
||||
|
||||
def define_command(subparsers, command, callback):
|
||||
'''Define a command in the subparsers collection.
|
||||
|
||||
:param subparsers: subparsers collection where the command will go
|
||||
:param command: command name
|
||||
:param callback: function that will be used to process the command
|
||||
'''
|
||||
desc = callback.__doc__ or ''
|
||||
help = desc.strip().split('\n')[0]
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(command, help=help, description=desc)
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
|
||||
# Decorator for cli-args
|
||||
def arg(*args, **kwargs):
|
||||
def _decorator(func):
|
||||
|
@ -16,10 +16,14 @@ Command-line interface to the Heat API.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import logging.handlers
|
||||
import sys
|
||||
import tuskarclient.v1.argparsers
|
||||
|
||||
from tuskarclient import client
|
||||
import tuskarclient.common.utils as utils
|
||||
from tuskarclient import exc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -30,15 +34,23 @@ class TuskarShell(object):
|
||||
self.raw_args = raw_args
|
||||
|
||||
def run(self):
|
||||
parser = tuskarclient.v1.argparsers.create_top_parser()
|
||||
args = parser.parse_args(self.raw_args)
|
||||
'''Run the CLI. Parse arguments and do the respective action.'''
|
||||
|
||||
if args.help or not self.raw_args:
|
||||
nonversioned_parser = self._nonversioned_parser()
|
||||
partial_args = nonversioned_parser.parse_known_args(self.raw_args)[0]
|
||||
parser = self._parser(partial_args.tuskar_api_version)
|
||||
|
||||
if partial_args.help or not self.raw_args:
|
||||
parser.print_help()
|
||||
return 0
|
||||
|
||||
args = parser.parse_args(self.raw_args)
|
||||
self._ensure_auth_info(args)
|
||||
|
||||
tuskar_client = client.get_client(partial_args.tuskar_api_version,
|
||||
**args.__dict__)
|
||||
args.func(tuskar_client, args)
|
||||
|
||||
def _ensure_auth_info(self, args):
|
||||
'''Ensure that authentication information is provided. Two variants
|
||||
of authentication are supported:
|
||||
@ -47,37 +59,133 @@ class TuskarShell(object):
|
||||
'''
|
||||
if not args.os_auth_token:
|
||||
if not args.os_username:
|
||||
raise UsageError("You must provide username via either "
|
||||
"--os-username or env[OS_USERNAME]")
|
||||
raise exc.CommandError("You must provide username via either "
|
||||
"--os-username or env[OS_USERNAME]")
|
||||
|
||||
if not args.os_password:
|
||||
raise UsageError("You must provide password via either "
|
||||
"--os-password or env[OS_PASSWORD]")
|
||||
raise exc.CommandError("You must provide password via either "
|
||||
"--os-password or env[OS_PASSWORD]")
|
||||
|
||||
if not args.os_tenant_id and not args.os_tenant_name:
|
||||
raise UsageError("You must provide tenant via either "
|
||||
"--os-tenant-name or --os-tenant-id or "
|
||||
"env[OS_TENANT_NAME] or env[OS_TENANT_ID]")
|
||||
raise exc.CommandError("You must provide tenant via either "
|
||||
"--os-tenant-name or --os-tenant-id or "
|
||||
"env[OS_TENANT_NAME] or "
|
||||
"env[OS_TENANT_ID]")
|
||||
|
||||
if not args.os_auth_url:
|
||||
raise UsageError("You must provide auth URL via either "
|
||||
"--os-auth-url or env[OS_AUTH_URL]")
|
||||
raise exc.CommandError("You must provide auth URL via either "
|
||||
"--os-auth-url or env[OS_AUTH_URL]")
|
||||
else:
|
||||
if not args.tuskar_url and not args.os_auth_url:
|
||||
raise UsageError("You must provide either "
|
||||
"--tuskar_url or --os_auth_url or "
|
||||
"env[TUSKAR_URL] or env[OS_AUTH_URL]")
|
||||
raise exc.CommandError("You must provide either "
|
||||
"--tuskar-url or --os-auth-url or "
|
||||
"env[TUSKAR_URL] or env[OS_AUTH_URL]")
|
||||
|
||||
def _parser(self, version):
|
||||
'''Create a top level argument parser.
|
||||
|
||||
class UsageError(Exception):
|
||||
pass
|
||||
:param version: version of Tuskar API (and corresponding CLI
|
||||
commands) to use
|
||||
'''
|
||||
parser = self._nonversioned_parser()
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
versioned_shell = utils.import_versioned_module(version, 'shell')
|
||||
versioned_shell.enhance_parser(parser, subparsers)
|
||||
return parser
|
||||
|
||||
def _nonversioned_parser(self):
|
||||
'''Create a basic parser that doesn't contain version-specific
|
||||
subcommands. This one is mainly useful for parsing which API
|
||||
version should be used for the versioned full blown parser and
|
||||
defining common version-agnostic options.
|
||||
'''
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='tuskar',
|
||||
description='OpenStack Management CLI',
|
||||
add_help=False
|
||||
)
|
||||
|
||||
parser.add_argument('-h', '--help',
|
||||
action='store_true',
|
||||
help="Print this help message and exit.",
|
||||
)
|
||||
|
||||
parser.add_argument('--os-username',
|
||||
default=utils.env('OS_USERNAME'),
|
||||
help='Defaults to env[OS_USERNAME]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_username',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-password',
|
||||
default=utils.env('OS_PASSWORD'),
|
||||
help='Defaults to env[OS_PASSWORD]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_password',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-tenant-id',
|
||||
default=utils.env('OS_TENANT_ID'),
|
||||
help='Defaults to env[OS_TENANT_ID]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_tenant_id',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-tenant-name',
|
||||
default=utils.env('OS_TENANT_NAME'),
|
||||
help='Defaults to env[OS_TENANT_NAME]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_tenant_name',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-auth-url',
|
||||
default=utils.env('OS_AUTH_URL'),
|
||||
help='Defaults to env[OS_AUTH_URL]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_auth_url',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-auth-token',
|
||||
default=utils.env('OS_AUTH_TOKEN'),
|
||||
help='Defaults to env[OS_AUTH_TOKEN]')
|
||||
|
||||
parser.add_argument('--os_auth_token',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--tuskar-url',
|
||||
default=utils.env('TUSKAR_URL'),
|
||||
help='Defaults to env[TUSKAR_URL]')
|
||||
|
||||
parser.add_argument('--tuskar_url',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--tuskar-api-version',
|
||||
default=utils.env('TUSKAR_API_VERSION',
|
||||
default='1'),
|
||||
help='Defaults to env[TUSKAR_API_VERSION] '
|
||||
'or 1')
|
||||
|
||||
parser.add_argument('--tuskar_api_version',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
logger.addHandler(logging.StreamHandler(sys.stderr))
|
||||
try:
|
||||
TuskarShell(sys.argv[1:]).run()
|
||||
except UsageError as e:
|
||||
except exc.CommandError as e:
|
||||
print(e.message, file=sys.stderr)
|
||||
except Exception as e:
|
||||
logger.exception("Exiting due to an error:")
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
|
||||
import cStringIO
|
||||
import mock
|
||||
import sys
|
||||
|
||||
from tuskarclient.common import utils
|
||||
@ -45,3 +46,31 @@ class UtilsTest(test_utils.TestCase):
|
||||
| Key | Value |
|
||||
+----------+-------+
|
||||
''')
|
||||
|
||||
|
||||
class DefineCommandsTest(test_utils.TestCase):
|
||||
|
||||
def test_define_commands_from_module(self):
|
||||
subparsers = mock.Mock()
|
||||
subparser = mock.MagicMock()
|
||||
subparsers.add_parser.return_value = subparser
|
||||
dummy_module = self.dummy_command_module()
|
||||
|
||||
utils.define_commands_from_module(subparsers, dummy_module)
|
||||
subparsers.add_parser.assert_called_with(
|
||||
'dummy-list', help="Docstring.", description="Docstring.")
|
||||
subparser.add_argument.assert_called_with(
|
||||
'-a', metavar='<NUMBER>', help="Add a number.")
|
||||
subparser.set_defaults.assert_called_with(
|
||||
func=dummy_module.do_dummy_list)
|
||||
|
||||
def dummy_command_module(self):
|
||||
@utils.arg('-a', metavar='<NUMBER>', help="Add a number.")
|
||||
def do_dummy_list():
|
||||
'''Docstring.'''
|
||||
return 42
|
||||
|
||||
dummy = mock.Mock()
|
||||
dummy.do_dummy_list = do_dummy_list
|
||||
dummy.other_method = mock.Mock('other_method', return_value=43)
|
||||
return dummy
|
@ -10,47 +10,62 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tuskarclient import exc
|
||||
from tuskarclient import shell
|
||||
import tuskarclient.tests.utils as tutils
|
||||
|
||||
|
||||
class ShellTest(tutils.TestCase):
|
||||
|
||||
args_attributes = [
|
||||
'os_username', 'os_password', 'os_tenant_name', 'os_tenant_id',
|
||||
'os_auth_url', 'os_auth_token', 'tuskar_url', 'tuskar_api_version',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(ShellTest, self).setUp()
|
||||
self.s = shell.TuskarShell({})
|
||||
|
||||
def empty_args(self):
|
||||
args = lambda: None # i'd use object(), but it can't have attributes
|
||||
args_attributes = [
|
||||
'os_username', 'os_password', 'os_tenant_name', 'os_tenant_id',
|
||||
'os_auth_url', 'os_auth_token', 'tuskar_url',
|
||||
]
|
||||
for attr in args_attributes:
|
||||
for attr in self.args_attributes:
|
||||
setattr(args, attr, None)
|
||||
|
||||
return args
|
||||
|
||||
def test_ensure_auth_info_with_credentials(self):
|
||||
ensure = self.s._ensure_auth_info
|
||||
usage_error = shell.UsageError
|
||||
command_error = exc.CommandError
|
||||
args = self.empty_args()
|
||||
|
||||
args.os_username = 'user'
|
||||
args.os_password = 'pass'
|
||||
args.os_tenant_name = 'tenant'
|
||||
self.assertRaises(usage_error, ensure, args)
|
||||
self.assertRaises(command_error, ensure, args)
|
||||
|
||||
args.os_auth_url = 'keystone'
|
||||
ensure(args) # doesn't raise
|
||||
|
||||
def test_ensure_auth_info_with_token(self):
|
||||
ensure = self.s._ensure_auth_info
|
||||
usage_error = shell.UsageError
|
||||
command_error = exc.CommandError
|
||||
args = self.empty_args()
|
||||
|
||||
args.os_auth_token = 'token'
|
||||
self.assertRaises(usage_error, ensure, args)
|
||||
self.assertRaises(command_error, ensure, args)
|
||||
|
||||
args.tuskar_url = 'tuskar'
|
||||
ensure(args) # doesn't raise
|
||||
|
||||
def test_parser_v1(self):
|
||||
v1_commands = [
|
||||
'rack-list', 'rack-show',
|
||||
]
|
||||
parser = self.s._parser(1)
|
||||
tuskar_help = parser.format_help()
|
||||
|
||||
for arg in map(lambda a: a.replace('_', '-'), self.args_attributes):
|
||||
self.assertIn(arg, tuskar_help)
|
||||
|
||||
for command in v1_commands:
|
||||
self.assertIn(command, tuskar_help)
|
||||
|
@ -1,87 +0,0 @@
|
||||
# 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.
|
||||
|
||||
import argparse
|
||||
import tuskarclient.utils as utils
|
||||
|
||||
|
||||
def create_top_parser():
|
||||
parser = argparse.ArgumentParser(prog='tuskar',
|
||||
description='OpenStack Management CLI',
|
||||
add_help=False
|
||||
)
|
||||
|
||||
parser.add_argument('-h', '--help',
|
||||
action='store_true',
|
||||
help="Print this help message and exit.",
|
||||
)
|
||||
|
||||
parser.add_argument('--os-username',
|
||||
default=utils.env('OS_USERNAME'),
|
||||
help='Defaults to env[OS_USERNAME]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_username',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-password',
|
||||
default=utils.env('OS_PASSWORD'),
|
||||
help='Defaults to env[OS_PASSWORD]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_password',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-tenant-id',
|
||||
default=utils.env('OS_TENANT_ID'),
|
||||
help='Defaults to env[OS_TENANT_ID]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_tenant_id',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-tenant-name',
|
||||
default=utils.env('OS_TENANT_NAME'),
|
||||
help='Defaults to env[OS_TENANT_NAME]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_tenant_name',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-auth-url',
|
||||
default=utils.env('OS_AUTH_URL'),
|
||||
help='Defaults to env[OS_AUTH_URL]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_auth_url',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-auth-token',
|
||||
default=utils.env('OS_AUTH_TOKEN'),
|
||||
help='Defaults to env[OS_AUTH_TOKEN]')
|
||||
|
||||
parser.add_argument('--os_auth_token',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--tuskar-url',
|
||||
default=utils.env('TUSKAR_URL'),
|
||||
help='Defaults to env[TUSKAR_URL]')
|
||||
|
||||
parser.add_argument('--tuskar_url',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
return parser
|
@ -9,18 +9,3 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def env(*vars, **kwargs):
|
||||
"""Search for the first defined of possibly many env vars
|
||||
|
||||
Returns the first environment variable defined in vars, or
|
||||
returns the default defined in kwargs.
|
||||
"""
|
||||
for v in vars:
|
||||
value = os.environ.get(v, None)
|
||||
if value:
|
||||
return value
|
||||
return kwargs.get('default', '')
|
11
tuskarclient/v1/flavors_shell.py
Normal file
11
tuskarclient/v1/flavors_shell.py
Normal file
@ -0,0 +1,11 @@
|
||||
# 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.
|
35
tuskarclient/v1/racks_shell.py
Normal file
35
tuskarclient/v1/racks_shell.py
Normal file
@ -0,0 +1,35 @@
|
||||
# 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 tuskarclient.common import utils
|
||||
from tuskarclient import exc
|
||||
|
||||
|
||||
# TODO(jistr): This is PoC, not final implementation
|
||||
@utils.arg('id', metavar="<NAME or ID>", help="Name or ID of rack to show.")
|
||||
def do_rack_show(tuskar, args):
|
||||
try:
|
||||
rack = utils.find_resource(tuskar.racks, args.id)
|
||||
except exc.HTTPNotFound:
|
||||
raise exc.CommandError("Rack not found: %s" % args.id)
|
||||
|
||||
utils.print_dict(rack.to_dict())
|
||||
|
||||
|
||||
# TODO(jistr): This is PoC, not final implementation
|
||||
def do_rack_list(tuskar, args):
|
||||
racks = tuskar.racks.list()
|
||||
fields = ['name', 'subnet', 'state', 'nodes']
|
||||
labels = ["Name", "Subnet", "State", "Nodes"]
|
||||
formatters = {'nodes': lambda rack: len(rack.nodes)}
|
||||
|
||||
utils.print_list(racks, fields, labels, formatters, 0)
|
11
tuskarclient/v1/resource_classes_shell.py
Normal file
11
tuskarclient/v1/resource_classes_shell.py
Normal file
@ -0,0 +1,11 @@
|
||||
# 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.
|
35
tuskarclient/v1/shell.py
Normal file
35
tuskarclient/v1/shell.py
Normal file
@ -0,0 +1,35 @@
|
||||
# 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 tuskarclient.common import utils
|
||||
from tuskarclient.v1 import data_centers_shell
|
||||
from tuskarclient.v1 import flavors_shell
|
||||
from tuskarclient.v1 import racks_shell
|
||||
from tuskarclient.v1 import resource_classes_shell
|
||||
|
||||
COMMAND_MODULES = [
|
||||
data_centers_shell,
|
||||
flavors_shell,
|
||||
racks_shell,
|
||||
resource_classes_shell,
|
||||
]
|
||||
|
||||
|
||||
def enhance_parser(parser, subparsers):
|
||||
'''Take a basic (nonversioned) parser and enhance it with
|
||||
commands and options specific for this version of API.
|
||||
|
||||
:param parser: top level parser :param subparsers: top level
|
||||
parser's subparsers collection where subcommands will go
|
||||
'''
|
||||
for command_module in COMMAND_MODULES:
|
||||
utils.define_commands_from_module(subparsers, command_module)
|
Loading…
x
Reference in New Issue
Block a user