diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eeccc0f --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.log +*.pyc +*.swp +*~ +.openstackclient-venv +.venv +build +dist +python_openstackclient.egg-info diff --git a/openstackclient/__init__.py b/openstackclient/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openstackclient/common/__init__.py b/openstackclient/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py new file mode 100644 index 0000000..edd71f9 --- /dev/null +++ b/openstackclient/common/utils.py @@ -0,0 +1,117 @@ +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# 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 os +import uuid + +import prettytable + +from glanceclient.common import exceptions + + +# Decorator for cli-args +def arg(*args, **kwargs): + def _decorator(func): + # Because of the sematics of decorator composition if we just append + # to the options list positional options will appear to be backwards. + func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs)) + return func + return _decorator + + +def pretty_choice_list(l): + return ', '.join("'%s'" % i for i in l) + + +def print_list(objs, fields, formatters={}): + pt = prettytable.PrettyTable([f for f in fields], caching=False) + pt.aligns = ['l' for f in fields] + + for o in objs: + row = [] + for field in fields: + if field in formatters: + row.append(formatters[field](o)) + else: + field_name = field.lower().replace(' ', '_') + data = getattr(o, field_name, '') + row.append(data) + pt.add_row(row) + + pt.printt(sortby=fields[0]) + + +def print_dict(d): + pt = prettytable.PrettyTable(['Property', 'Value'], caching=False) + pt.aligns = ['l', 'l'] + [pt.add_row(list(r)) for r in d.iteritems()] + pt.printt(sortby='Property') + + +def find_resource(manager, name_or_id): + """Helper for the _find_* methods.""" + # first try to get entity as integer id + try: + if isinstance(name_or_id, int) or name_or_id.isdigit(): + return manager.get(int(name_or_id)) + except exceptions.NotFound: + pass + + # now try to get entity as uuid + try: + uuid.UUID(str(name_or_id)) + return manager.get(name_or_id) + except (ValueError, exceptions.NotFound): + pass + + # finally try to find entity by name + try: + return manager.find(name=name_or_id) + except exceptions.NotFound: + msg = "No %s with a name or ID of '%s' exists." % \ + (manager.resource_class.__name__.lower(), name_or_id) + raise exceptions.CommandError(msg) + + +def skip_authentication(f): + """Function decorator used to indicate a caller may be unauthenticated.""" + f.require_authentication = False + return f + + +def is_authentication_required(f): + """Checks to see if the function requires authentication. + + Use the skip_authentication decorator to indicate a caller may + skip the authentication step. + """ + return getattr(f, 'require_authentication', True) + + +def string_to_bool(arg): + return arg.strip().lower() in ('t', 'true', 'yes', '1') + + +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', '') diff --git a/openstackclient/shell.py b/openstackclient/shell.py new file mode 100644 index 0000000..29ff854 --- /dev/null +++ b/openstackclient/shell.py @@ -0,0 +1,305 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved +# +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +""" +Command-line interface to the OpenStack Identity, Compute and Storage APIs +""" + +import argparse +import httplib2 +import os +import sys + +from openstackclient.common import utils + + +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', '') + + +class OpenStackShell(object): + + def _find_actions(self, subparsers, actions_module): + if self.debug: + print "_find_actions(module: %s)" % actions_module + for attr in (a for a in dir(actions_module) if a.startswith('do_')): + # I prefer to be hypen-separated instead of underscores. + command = attr[3:].replace('_', '-') + cmd = command.split('-', 1) + action = cmd[0] + if len(cmd) > 1: + subject = cmd[1] + else: + subject = '' + callback = getattr(actions_module, attr) + desc = callback.__doc__ or '' + help = desc.strip().split('\n')[0] + arguments = getattr(callback, 'arguments', []) + + if self.debug: + print " command: %s" % command + print " action: %s" % action + print " subject: %s" % subject + print " arguments: %s" % arguments + + subparser = subparsers.add_parser(command, + help=help, + description=desc, + add_help=False, + formatter_class=OpenStackHelpFormatter + ) + 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) + + @utils.arg('command', metavar='', nargs='?', + help='Display help for ') + def do_help(self, args): + """ + Display help about this program or one of its subcommands. + """ + if getattr(args, 'command', None): + 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 get_base_parser(self): + parser = argparse.ArgumentParser( + prog='stack', + description=__doc__.strip(), + epilog='See "stack help COMMAND" ' + 'for help on a specific command.', + add_help=False, + formatter_class=OpenStackHelpFormatter, + ) + + # Global arguments + parser.add_argument('-h', '--help', + action='store_true', + help=argparse.SUPPRESS, + ) + + parser.add_argument('--os-auth-url', metavar='', + default=env('OS_AUTH_URL'), + help='Authentication URL (Env: OS_AUTH_URL)') + + parser.add_argument('--os-tenant-name', metavar='', + default=env('OS_TENANT_NAME'), + help='Authentication tenant name (Env: OS_TENANT_NAME)') + + parser.add_argument('--os-tenant-id', metavar='', + default=env('OS_TENANT_ID'), + help='Authentication tenant ID (Env: OS_TENANT_ID)') + + parser.add_argument('--os-username', metavar='', + default=utils.env('OS_USERNAME'), + help='Authentication username (Env: OS_USERNAME)') + + parser.add_argument('--os-password', metavar='', + default=utils.env('OS_PASSWORD'), + help='Authentication password (Env: OS_PASSWORD)') + + parser.add_argument('--os-region-name', metavar='', + default=env('OS_REGION_NAME'), + help='Authentication region name (Env: OS_REGION_NAME)') + + parser.add_argument('--debug', + default=False, + action='store_true', + help=argparse.SUPPRESS) + + parser.add_argument('--os-identity-api-version', + metavar='', + default=env('OS_IDENTITY_API_VERSION', default='2.0'), + help='Identity API version, default=2.0 (Env: OS_IDENTITY_API_VERSION)') + + parser.add_argument('--os-compute-api-version', + metavar='', + default=env('OS_COMPUTE_API_VERSION', default='2'), + help='Compute API version, default=2.0 (Env: OS_COMPUTE_API_VERSION)') + + parser.add_argument('--os-image-api-version', + metavar='', + default=env('OS_IMAGE_API_VERSION', default='1.0'), + help='Image API version, default=1.0 (Env: OS_IMAGE_API_VERSION)') + + parser.add_argument('--service-token', metavar='', + default=env('SERVICE_TOKEN'), + help=argparse.SUPPRESS) + + parser.add_argument('--service-endpoint', metavar='', + default=env('SERVICE_ENDPOINT'), + help=argparse.SUPPRESS) + + parser.add_argument('action', metavar='', + default='help', + help=argparse.SUPPRESS) + + parser.add_argument('subject', metavar='', + default='', nargs='?', + help=argparse.SUPPRESS) + + return parser + + def get_subcommand_parser(self, cmd_subject): + parser = self.get_base_parser() + + self.subcommands = {} + subparsers = parser.add_subparsers(metavar='') + + if cmd_subject is None or cmd_subject == '': + # TODO(dtroyer): iterate over all known subjects to produce + # the complete help list + print "Get all subjects here - exit" + exit(1) + + (module, version) = self._map_subject(cmd_subject) + if module is None or cmd_subject is None: + print "Module %s not found - exit" % cmd_subject + exit(1) + if self.debug: + print "module: %s" % module + exec("from %s.v%s import %s as cmd" % (module, self.api_version[module], cmd_subject)) + self._find_actions(subparsers, cmd) + + self._find_actions(subparsers, self) + + return parser + + def _map_subject(self, cmd_subject): + '''Convert from subject to the module that implements it''' + COMPUTE = ['server'] + IDENTITY = ['key'] + IMAGE = ['image'] + if cmd_subject in COMPUTE: + version = self.api_version['compute'].replace('.', '_') + return ('compute', version) + elif cmd_subject in IDENTITY: + version = self.api_version['identity'].replace('.', '_') + return ('identity', version) + elif cmd_subject in IMAGE: + version = self.api_version['imade'].replace('.', '_') + return ('image', version) + else: + return None + + def main(self, argv): + ''' + - get api version + - get version command set + - import version-subject module + - is verb-subject supported? + ''' + # Parse global args to find version + parser = self.get_base_parser() + (options, args) = parser.parse_known_args(argv) + + # stash selected API versions for later + # TODO(dtroyer): how do extenstions add their version requirements? + self.api_version = { + 'compute': options.os_compute_api_version, + 'identity': options.os_identity_api_version, + 'image': options.os_image_api_version, + } + + # Setup debugging + if getattr(options, 'debug', None): + self.debug = 1 + else: + self.debug = 0 + + if self.debug: + print "API: Identity=%s Compute=%s Image=%s" % (self.api_version['identity'], self.api_version['compute'], self.api_version['image']) + print "Action: %s" % options.action + print "subject: %s" % getattr(options, 'subject', '') + print "args: %s" % args + + # Handle top-level --help/-h before attempting to parse + # a command off the command line + if getattr(options, 'help', None) or getattr(options, 'action', None) == 'help': + print "top-level help" + # Build available subcommands + self.parser = self.get_subcommand_parser(options.subject) + self.do_help(options) + return 0 + + # Build selected subcommands + self.parser = self.get_subcommand_parser(options.subject) + + # Parse args again and call whatever callback was selected + args.insert(0, '%s-%s' % (options.action, options.subject)) + if self.debug: + print "args: %s" % args + args = self.parser.parse_args(args) + + if self.debug: + print "Testing command parsing" + print "Auth username: %s" % options.os_username + #print "Action: %s" % options.action + #print "Subject: %s" % options.subject + print "args: %s" % args + +class OpenStackHelpFormatter(argparse.HelpFormatter): + def start_section(self, heading): + # Title-case the headings + heading = '%s%s' % (heading[0].upper(), heading[1:]) + super(OpenStackHelpFormatter, self).start_section(heading) + + +def main(): + try: + OpenStackShell().main(sys.argv[1:]) + + except Exception, e: + if httplib2.debuglevel == 1: + raise # dump stack. + else: + print >> sys.stderr, e + sys.exit(1) + +def test_main(argv): + # The argparse/optparse/cmd2 modules muck about with sys.argv + # so we save it and restore at the end to let the tests + # run repeatedly without concatenating the args on each run + save_argv = sys.argv + + main() + + # Put it back so the next test has a clean copy + sys.argv = save_argv + +if __name__ == "__main__": + main()