From 11d3ba457018ac2179669309c65d806c53476649 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 19 Apr 2012 22:41:44 -0500 Subject: [PATCH] Add openstackclient bits --- .gitignore | 9 + MANIFEST.in | 5 + openstackclient/__init__.py | 0 openstackclient/common/__init__.py | 0 openstackclient/common/utils.py | 117 ++++++++++ openstackclient/compute/__init__.py | 0 openstackclient/compute/v2/__init__.py | 0 openstackclient/compute/v2/server.py | 55 +++++ openstackclient/identity/__init__.py | 0 openstackclient/image/__init__.py | 0 openstackclient/shell.py | 305 +++++++++++++++++++++++++ openstackclient/utils.py.dt | 131 +++++++++++ tests/__init__.py | 0 tests/test_utils.py | 51 +++++ tests/utils.py | 7 + 15 files changed, 680 insertions(+) create mode 100644 .gitignore create mode 100644 MANIFEST.in create mode 100644 openstackclient/__init__.py create mode 100644 openstackclient/common/__init__.py create mode 100644 openstackclient/common/utils.py create mode 100644 openstackclient/compute/__init__.py create mode 100644 openstackclient/compute/v2/__init__.py create mode 100644 openstackclient/compute/v2/server.py create mode 100644 openstackclient/identity/__init__.py create mode 100644 openstackclient/image/__init__.py create mode 100644 openstackclient/shell.py create mode 100644 openstackclient/utils.py.dt create mode 100644 tests/__init__.py create mode 100644 tests/test_utils.py create mode 100644 tests/utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..eeccc0fb8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.log +*.pyc +*.swp +*~ +.openstackclient-venv +.venv +build +dist +python_openstackclient.egg-info diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..f1f2e4d0e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include AUTHORS +include LICENSE +include README.rst +recursive-inlcude docs * +recursive-include tests * diff --git a/openstackclient/__init__.py b/openstackclient/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstackclient/common/__init__.py b/openstackclient/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py new file mode 100644 index 000000000..edd71f955 --- /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/compute/__init__.py b/openstackclient/compute/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstackclient/compute/v2/__init__.py b/openstackclient/compute/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py new file mode 100644 index 000000000..9ca3f1424 --- /dev/null +++ b/openstackclient/compute/v2/server.py @@ -0,0 +1,55 @@ +# 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. + +from glanceclient.common import utils + + +def _find_server(cs, server): + """Get a server by name or ID.""" + return utils.find_resource(cs.servers, server) + +def _print_server(cs, server): + # By default when searching via name we will do a + # findall(name=blah) and due a REST /details which is not the same + # as a .get() and doesn't get the information about flavors and + # images. This fix it as we redo the call with the id which does a + # .get() to get all informations. + if not 'flavor' in server._info: + server = _find_server(cs, server.id) + + networks = server.networks + info = server._info.copy() + for network_label, address_list in networks.items(): + info['%s network' % network_label] = ', '.join(address_list) + + flavor = info.get('flavor', {}) + flavor_id = flavor.get('id', '') + info['flavor'] = _find_flavor(cs, flavor_id).name + + image = info.get('image', {}) + image_id = image.get('id', '') + info['image'] = _find_image(cs, image_id).name + + info.pop('links', None) + info.pop('addresses', None) + + utils.print_dict(info) + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_show_server(cs, args): + """Show details about the given server.""" + print "do_show_server(%s)" % args.server + #s = _find_server(cs, args.server) + #_print_server(cs, s) diff --git a/openstackclient/identity/__init__.py b/openstackclient/identity/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstackclient/image/__init__.py b/openstackclient/image/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstackclient/shell.py b/openstackclient/shell.py new file mode 100644 index 000000000..29ff854f6 --- /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() diff --git a/openstackclient/utils.py.dt b/openstackclient/utils.py.dt new file mode 100644 index 000000000..3719a2a8c --- /dev/null +++ b/openstackclient/utils.py.dt @@ -0,0 +1,131 @@ +# 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 + +""" +Utility functions for OpenStack Client +""" + +import copy + +import prettytable + +#from novaclient import utils + + +# lifted from glance/common/utils.py +def bool_from_string(subject): + """ + Interpret a string as a boolean. + + Any string value in: + ('True', 'true', 'On', 'on', '1') + is interpreted as a boolean True. + + Useful for JSON-decoded stuff and config file parsing + """ + if isinstance(subject, bool): + return subject + elif isinstance(subject, int): + return subject == 1 + if hasattr(subject, 'startswith'): # str or unicode... + if subject.strip().lower() in ('true', 'on', '1'): + return True + return False + + +# lifted from keystoneclient/base.py +def getid(obj): + """ + Abstracts the common pattern of allowing both an object or an object's ID + (UUID) as a parameter when dealing with relationships. + """ + + # Try to return the object's UUID first, if we have a UUID. + try: + if obj.uuid: + return obj.uuid + except AttributeError: + pass + try: + return obj.id + except AttributeError: + return obj + + +def show_object(manager, id, fields=None): + """Check id, lookup object, display result fields""" + if not id: + print "no id specified" + return + obj = manager.get(id) + print_obj_fields(obj, fields) + + +def print_obj_fields(obj, fields=[]): + """Print specified object fields""" + # Select the fields to print, then passthrough to novaclient + a = {name: getattr(obj, name, '') for name in fields} + utils.print_dict(a) + + +def print_dict_fields(obj, fields=[]): + """Print specified object fields""" + # Select the fields to print, then passthrough to novaclient + a = {name: obj[name] for name in fields} + utils.print_dict(a) + + +def print_dict_list(objs, fields, formatters={}): + """Print list of dicts""" + mixed_case_fields = [] + 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: + if field in mixed_case_fields: + field_name = field.replace(' ', '_') + else: + field_name = field.lower().replace(' ', '_') + data = o[field_name] + row.append(data) + pt.add_row(row) + + pt.printt(sortby=fields[0]) + + +def print_list(objs, fields, formatters={}): + """Print list of objects""" + # Passthrough to novaclient + utils.print_list(objs, fields, formatters=formatters) + + +def expand_meta(objs, field): + """Expand metadata fields in an object""" + ret = [] + for oldobj in objs: + newobj = copy.deepcopy(oldobj) + ex = getattr(newobj, field, {}) + for f in ex.keys(): + setattr(newobj, f, ex[f]) + delattr(newobj, field) + ret.append(newobj) + return ret diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 000000000..c8d249666 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,51 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +from openstackclient import utils as os_utils +from tests import utils + +OBJ_LIST = [ + { + 'id': '123', + 'name': 'foo', + 'extra': { + 'desc': 'foo fu', + 'status': 'present', + } + }, + { + 'id': 'abc', + 'name': 'bar', + 'extra': { + 'desc': 'babar', + 'status': 'waiting', + } + } + ] + + +class Obj(object): + + def __init__(self): + pass + + +class UtilsTest(utils.TestCase): + + def setUp(self): + super(UtilsTest, self).setUp() + self.objs = [] + for o in OBJ_LIST: + obj = Obj() + for k in o.keys(): + setattr(obj, k, o.get(k)) + self.objs.append(obj) + + def tearDown(self): + super(UtilsTest, self).tearDown() + self.objs = [] + + def test_expand_meta(self): + ret = os_utils.expand_meta(self.objs, 'extra') + assert (getattr(ret[0], 'desc') == 'foo fu') + assert (getattr(ret[0], 'status') == 'present') + assert (getattr(ret[0], 'extra', 'qaz') == 'qaz') diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 000000000..25452d5cc --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,7 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import unittest + + +class TestCase(unittest.TestCase): + pass