From bb879dd10b37a884a7e724d7a7cbd204041061e0 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Thu, 15 Dec 2011 19:39:33 +0000 Subject: [PATCH] Adding extension framework. Change-Id: If882f7a822ef6b1e58666b3af6f7166ab0a230fe --- README.rst | 2 +- novaclient/base.py | 5 ++- novaclient/shell.py | 72 ++++++++++++++++++++++++++++++++++++-- novaclient/utils.py | 12 +++++++ novaclient/v1_0/client.py | 7 +++- novaclient/v1_1/client.py | 7 +++- novaclient/v1_1/shell.py | 14 +------- run_tests.sh | 5 ++- tests/v1_1/fakes.py | 14 ++++++++ tools/nova.bash_completion | 11 ++---- 10 files changed, 117 insertions(+), 32 deletions(-) diff --git a/README.rst b/README.rst index a1cecf1c0..19a042ca7 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ pull requests. .. _Gerrit: http://wiki.openstack.org/GerritWorkflow This code a fork of `Jacobian's python-cloudservers`__ If you need API support -for the Rackspace API soley or the BSD license, you should use that repository. +for the Rackspace API solely or the BSD license, you should use that repository. python-client is licensed under the Apache License like the rest of OpenStack. __ http://github.com/jacobian/python-cloudservers diff --git a/novaclient/base.py b/novaclient/base.py index 9b35ffed5..1f73ffda1 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -74,7 +74,10 @@ class Manager(object): # NOTE(ja): keystone returns values as list as {'values': [ ... ]} # unlike other services which just return the list... if type(data) is dict: - data = data['values'] + try: + data = data['values'] + except KeyError: + pass with self.uuid_cache(obj_class, mode="w"): return [obj_class(self, res, loaded=True) for res in data if res] diff --git a/novaclient/shell.py b/novaclient/shell.py index cee4e1d78..5c91b890b 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -20,10 +20,13 @@ Command-line interface to the OpenStack Nova API. """ import argparse +import glob import httplib2 +import imp import os import sys +from novaclient import base from novaclient import exceptions as exc from novaclient import utils from novaclient.v1_0 import shell as shell_v1_0 @@ -96,7 +99,7 @@ class OpenStackComputeShell(object): return parser - def get_subcommand_parser(self, version): + def get_subcommand_parser(self, version, extensions): parser = self.get_base_parser() self.subcommands = {} @@ -115,8 +118,49 @@ class OpenStackComputeShell(object): self._find_actions(subparsers, actions_module) self._find_actions(subparsers, self) + for _, _, ext_module in extensions: + self._find_actions(subparsers, ext_module) + + self._add_bash_completion_subparser(subparsers) + return parser + def _discover_extensions(self, version): + module_path = os.path.dirname(os.path.abspath(__file__)) + version_str = "v%s" % version.replace('.', '_') + ext_path = os.path.join(module_path, version_str, 'contrib') + ext_glob = os.path.join(ext_path, "*.py") + + extensions = [] + for ext_path in glob.iglob(ext_glob): + name = os.path.basename(ext_path)[:-3] + ext_module = imp.load_source(name, ext_path) + + # Extract Manager class + ext_manager_class = None + for attr_value in ext_module.__dict__.values(): + try: + if issubclass(attr_value, base.Manager): + ext_manager_class = attr_value + break + except TypeError: + continue # in case attr_value isn't a class... + + if not ext_manager_class: + raise Exception("Could not find Manager class in extension.") + + extensions.append((name, ext_manager_class, ext_module)) + + return extensions + + def _add_bash_completion_subparser(self, subparsers): + subparser = subparsers.add_parser('bash_completion', + add_help=False, + formatter_class=OpenStackHelpFormatter + ) + self.subcommands['bash_completion'] = subparser + subparser.set_defaults(func=self.do_bash_completion) + def _find_actions(self, subparsers, 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. @@ -147,7 +191,9 @@ class OpenStackComputeShell(object): (options, args) = parser.parse_known_args(argv) # build available subcommands based on version - subcommand_parser = self.get_subcommand_parser(options.version) + extensions = self._discover_extensions(options.version) + subcommand_parser = self.get_subcommand_parser( + options.version, extensions) self.parser = subcommand_parser # Parse args again and call whatever callback was selected @@ -161,6 +207,9 @@ class OpenStackComputeShell(object): if args.func == self.do_help: self.do_help(args) return 0 + elif args.func == self.do_bash_completion: + self.do_bash_completion(args) + return 0 (user, apikey, password, projectid, url, region_name, endpoint_name, insecure) = (args.username, args.apikey, @@ -199,7 +248,8 @@ class OpenStackComputeShell(object): self.cs = self.get_api_class(options.version)(user, password, projectid, url, insecure, region_name=region_name, - endpoint_name=endpoint_name) + endpoint_name=endpoint_name, + extensions=extensions) try: self.cs.authenticate() @@ -221,6 +271,22 @@ class OpenStackComputeShell(object): except KeyError: return shell_v1_0.CLIENT_CLASS + def do_bash_completion(self, args): + """ + Prints all of the commands and options to stdout so that the + nova.bash_completion script doesn't have to hard code them. + """ + commands = set() + options = set() + for sc_str, sc in self.subcommands.items(): + commands.add(sc_str) + for option in sc._optionals._option_string_actions.keys(): + options.add(option) + + commands.remove('bash-completion') + commands.remove('bash_completion') + print ' '.join(commands | options) + @utils.arg('command', metavar='', nargs='?', help='Display help for ') def do_help(self, args): diff --git a/novaclient/utils.py b/novaclient/utils.py index 582829d11..083620add 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -71,3 +71,15 @@ def find_resource(manager, name_or_id): msg = "No %s with a name or ID of '%s' exists." % \ (manager.resource_class.__name__.lower(), name_or_id) raise exceptions.CommandError(msg) + + +def _format_servers_list_networks(server): + output = [] + for (network, addresses) in server.networks.items(): + if len(addresses) == 0: + continue + addresses_csv = ', '.join(addresses) + group = "%s=%s" % (network, addresses_csv) + output.append(group) + + return '; '.join(output) diff --git a/novaclient/v1_0/client.py b/novaclient/v1_0/client.py index 7e326cd3f..16594bae1 100644 --- a/novaclient/v1_0/client.py +++ b/novaclient/v1_0/client.py @@ -27,7 +27,7 @@ class Client(object): def __init__(self, username, api_key, project_id, auth_url=None, insecure=False, timeout=None, token=None, region_name=None, - endpoint_name='publicURL'): + endpoint_name='publicURL', extensions=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument @@ -40,6 +40,11 @@ class Client(object): self.servers = servers.ServerManager(self) self.zones = zones.ZoneManager(self) + # Add in any extensions... + if extensions: + for (ext_name, ext_manager_class, ext_module) in extensions: + setattr(self, ext_name, ext_manager_class(self)) + _auth_url = auth_url or 'https://auth.api.rackspacecloud.com/v1.0' self.client = client.HTTPClient(username, diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index cf82b1cfc..8f363f05d 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -33,7 +33,7 @@ class Client(object): # FIXME(jesse): project_id isn't required to authenticate def __init__(self, username, api_key, project_id, auth_url, insecure=False, timeout=None, token=None, region_name=None, - endpoint_name='publicURL'): + endpoint_name='publicURL', extensions=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument password = api_key @@ -53,6 +53,11 @@ class Client(object): self.security_group_rules = \ security_group_rules.SecurityGroupRuleManager(self) + # Add in any extensions... + if extensions: + for (ext_name, ext_manager_class, ext_module) in extensions: + setattr(self, ext_name, ext_manager_class(self)) + self.client = client.HTTPClient(username, password, project_id, diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 45398b3c0..cc8f7c90b 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -481,23 +481,11 @@ def do_list(cs, args): id_col = 'ID' columns = [id_col, 'Name', 'Status', 'Networks'] - formatters = {'Networks': _format_servers_list_networks} + formatters = {'Networks': utils._format_servers_list_networks} utils.print_list(cs.servers.list(search_opts=search_opts), columns, formatters) -def _format_servers_list_networks(server): - output = [] - for (network, addresses) in server.networks.items(): - if len(addresses) == 0: - continue - addresses_csv = ', '.join(addresses) - group = "%s=%s" % (network, addresses_csv) - output.append(group) - - return '; '.join(output) - - @utils.arg('--hard', dest='reboot_type', action='store_const', diff --git a/run_tests.sh b/run_tests.sh index 94ff3a14a..0a4bbf872 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -92,9 +92,8 @@ function run_pep8 { # other than what the PEP8 tool claims. It is deprecated in Python 3, so, # perhaps the mistake was thinking that the deprecation applied to Python 2 # as well. - ${wrapper} pep8 --repeat --show-pep8 --show-source \ - --ignore=E202,W602 \ - ${srcfiles} + pep8_opts="--ignore=E202,W602 --repeat" + ${wrapper} pep8 ${pep8_opts} ${srcfiles} } NOSETESTS="nosetests $noseopts $noseargs" diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index ccfb64358..8ec2929f6 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1,3 +1,17 @@ +# Copyright 2011 OpenStack, LLC +# +# 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 httplib2 from novaclient import client as base_client diff --git a/tools/nova.bash_completion b/tools/nova.bash_completion index 0bcd0afdf..6bbbc268d 100644 --- a/tools/nova.bash_completion +++ b/tools/nova.bash_completion @@ -4,15 +4,8 @@ _nova() COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts="add-fixed-ip backup backup-schedule backup-schedule-delete boot - boot-for-account delete delete diagnostics flavor-list image-create - image-delete image-list ip-share ip-unshare ipgroup-create - ipgroup-delete ipgroup-list ipgroup-show list migrate pause reboot - rebuild remove-fixed-ip rename rescue resize resize-confirm - resize-revert resume root-password show suspend unpause unrescue - zone zone-add zone-boot zone-delete zone-info zone-list help - --debug --endpoint_name --password --projectid --region_name --url - --username --version" + + opts="$(nova bash_completion)" UUID_CACHE=~/.novaclient_cached_*_uuids opts+=" "$(cat $UUID_CACHE 2> /dev/null | tr '\n' ' ')