diff --git a/novaclient/base.py b/novaclient/base.py index 1f73ffda1..a401dcd25 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -22,6 +22,7 @@ Base utilities to build API operation managers and objects on top of. import contextlib import os from novaclient import exceptions +from novaclient import utils # Python 2.4 compat @@ -50,7 +51,7 @@ def getid(obj): return obj -class Manager(object): +class Manager(utils.HookableMixin): """ Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. @@ -125,7 +126,8 @@ class Manager(object): resp, body = self.api.client.get(url) return self.resource_class(self, body[response_key]) - def _create(self, url, body, response_key, return_raw=False): + def _create(self, url, body, response_key, return_raw=False, **kwargs): + self.run_hooks('modify_body_for_create', body, **kwargs) resp, body = self.api.client.post(url, body=body) if return_raw: return body[response_key] @@ -136,7 +138,8 @@ class Manager(object): def _delete(self, url): resp, body = self.api.client.delete(url) - def _update(self, url, body): + def _update(self, url, body, **kwargs): + self.run_hooks('modify_body_for_update', body, **kwargs) resp, body = self.api.client.put(url, body=body) @@ -184,7 +187,7 @@ class BootingManagerWithFind(ManagerWithFind): def _boot(self, resource_url, response_key, name, image, flavor, ipgroup=None, meta=None, files=None, zone_blob=None, reservation_id=None, return_raw=False, min_count=None, - max_count=None): + max_count=None, **kwargs): """ Create (boot) a new server. @@ -245,7 +248,7 @@ class BootingManagerWithFind(ManagerWithFind): }) return self._create(resource_url, body, response_key, - return_raw=return_raw) + return_raw=return_raw, **kwargs) class Resource(object): diff --git a/novaclient/extension.py b/novaclient/extension.py new file mode 100644 index 000000000..7c91a8e89 --- /dev/null +++ b/novaclient/extension.py @@ -0,0 +1,39 @@ +# 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. + +from novaclient import base +from novaclient import utils + + +class Extension(utils.HookableMixin): + """Extension descriptor.""" + + SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') + + def __init__(self, name, module): + self.name = name + self.module = module + self._parse_extension_module() + + def _parse_extension_module(self): + self.manager_class = None + for attr_name, attr_value in self.module.__dict__.items(): + if attr_name in self.SUPPORTED_HOOKS: + self.add_hook(attr_name, attr_value) + elif utils.safe_issubclass(attr_value, base.Manager): + self.manager_class = attr_value + + def __repr__(self): + return "" % self.name diff --git a/novaclient/shell.py b/novaclient/shell.py index 4ebcdec01..58cea4066 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -27,6 +27,7 @@ import sys from novaclient import base from novaclient import exceptions as exc +import novaclient.extension from novaclient import utils from novaclient.v1_1 import shell as shell_v1_1 from novaclient.keystone import shell as shell_keystone @@ -123,7 +124,7 @@ class OpenStackComputeShell(object): return parser - def get_subcommand_parser(self, version, extensions): + def get_subcommand_parser(self, version): parser = self.get_base_parser() self.subcommands = {} @@ -141,8 +142,8 @@ class OpenStackComputeShell(object): self._find_actions(subparsers, shell_keystone) self._find_actions(subparsers, self) - for _, _, ext_module in extensions: - self._find_actions(subparsers, ext_module) + for extension in self.extensions: + self._find_actions(subparsers, extension.module) self._add_bash_completion_subparser(subparsers) @@ -161,22 +162,9 @@ class OpenStackComputeShell(object): if name == "__init__": continue - 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)) + module = imp.load_source(name, ext_path) + extension = novaclient.extension.Extension(name, module) + extensions.append(extension) return extensions @@ -218,13 +206,14 @@ class OpenStackComputeShell(object): (options, args) = parser.parse_known_args(argv) # build available subcommands based on version - extensions = self._discover_extensions(options.version) - subcommand_parser = self.get_subcommand_parser( - options.version, extensions) + self.extensions = self._discover_extensions(options.version) + self._run_extension_hooks('__pre_parse_args__') + + subcommand_parser = self.get_subcommand_parser(options.version) self.parser = subcommand_parser - # Parse args again and call whatever callback was selected args = subcommand_parser.parse_args(argv) + self._run_extension_hooks('__post_parse_args__', args) # Deal with global arguments if args.debug: @@ -287,7 +276,7 @@ class OpenStackComputeShell(object): projectid, url, insecure, region_name=region_name, endpoint_name=endpoint_name, - extensions=extensions) + extensions=self.extensions) try: if not utils.isunauthenticated(args.func): @@ -299,6 +288,11 @@ class OpenStackComputeShell(object): args.func(self.cs, args) + def _run_extension_hooks(self, hook_type, *args, **kwargs): + """Run hooks for all registered extensions.""" + for extension in self.extensions: + extension.run_hooks(hook_type, *args, **kwargs) + def get_api_class(self, version): try: return { diff --git a/novaclient/utils.py b/novaclient/utils.py index 9c6dbd079..8a3617ff7 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -5,16 +5,63 @@ import prettytable from novaclient import exceptions -# Decorator for cli-args def arg(*args, **kwargs): + """Decorator for CLI args.""" 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)) + add_arg(func, *args, **kwargs) return func return _decorator +def add_arg(f, *args, **kwargs): + """Bind CLI arguments to a shell.py `do_foo` function.""" + + if not hasattr(f, 'arguments'): + f.arguments = [] + + # NOTE(sirp): avoid dups that can occur when the module is shared across + # tests. + if (args, kwargs) not in f.arguments: + # Because of the sematics of decorator composition if we just append + # to the options list positional options will appear to be backwards. + f.arguments.insert(0, (args, kwargs)) + + +def add_resource_manager_extra_kwargs_hook(f, hook): + """Adds hook to bind CLI arguments to ResourceManager calls. + + The `do_foo` calls in shell.py will receive CLI args and then in turn pass + them through to the ResourceManager. Before passing through the args, the + hooks registered here will be called, giving us a chance to add extra + kwargs (taken from the command-line) to what's passed to the + ResourceManager. + """ + if not hasattr(f, 'resource_manager_kwargs_hooks'): + f.resource_manager_kwargs_hooks = [] + + names = [h.__name__ for h in f.resource_manager_kwargs_hooks] + if hook.__name__ not in names: + f.resource_manager_kwargs_hooks.append(hook) + + +def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): + """Return extra_kwargs by calling resource manager kwargs hooks.""" + hooks = getattr(f, "resource_manager_kwargs_hooks", []) + extra_kwargs = {} + for hook in hooks: + hook_name = hook.__name__ + hook_kwargs = hook(args) + + conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) + if conflicting_keys and not allow_conflicts: + raise Exception("Hook '%(hook_name)s' is attempting to redefine" + " attributes '%(conflicting_keys)s'" % locals()) + + extra_kwargs.update(hook_kwargs) + + return extra_kwargs + + def unauthenticated(f): """ Adds 'unauthenticated' attribute to decorated function. @@ -104,3 +151,33 @@ def _format_servers_list_networks(server): output.append(group) return '; '.join(output) + + +class HookableMixin(object): + """Mixin so classes can register and run hooks.""" + _hooks_map = {} + + @classmethod + def add_hook(cls, hook_type, hook_func): + if hook_type not in cls._hooks_map: + cls._hooks_map[hook_type] = [] + + cls._hooks_map[hook_type].append(hook_func) + + @classmethod + def run_hooks(cls, hook_type, *args, **kwargs): + hook_funcs = cls._hooks_map.get(hook_type) or [] + for hook_func in hook_funcs: + hook_func(*args, **kwargs) + + +def safe_issubclass(*args): + """Like issubclass, but will just return False if not a class.""" + + try: + if issubclass(*args): + return True + except TypeError: + pass + + return False diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py index 7c326c7f1..ced9d7412 100644 --- a/novaclient/v1_1/base.py +++ b/novaclient/v1_1/base.py @@ -20,13 +20,16 @@ import base64 from novaclient import base +# FIXME(sirp): Now that v1_0 has been removed, this can be merged with +# base.ManagerWithFind class BootingManagerWithFind(base.ManagerWithFind): """Like a `ManagerWithFind`, but has the ability to boot servers.""" def _boot(self, resource_url, response_key, name, image, flavor, meta=None, files=None, zone_blob=None, userdata=None, reservation_id=None, return_raw=False, min_count=None, max_count=None, security_groups=None, key_name=None, - availability_zone=None, block_device_mapping=None, nics=None): + availability_zone=None, block_device_mapping=None, nics=None, + **kwargs): """ Create (boot) a new server. @@ -144,4 +147,4 @@ class BootingManagerWithFind(base.ManagerWithFind): body['server']['networks'] = all_net_data return self._create(resource_url, body, response_key, - return_raw=return_raw) + return_raw=return_raw, **kwargs) diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 8f363f05d..5ead8ab3b 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -55,8 +55,10 @@ class Client(object): # Add in any extensions... if extensions: - for (ext_name, ext_manager_class, ext_module) in extensions: - setattr(self, ext_name, ext_manager_class(self)) + for extension in extensions: + if extension.manager_class: + setattr(self, extension.name, + extension.manager_class(self)) self.client = client.HTTPClient(username, password, diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 8291f8eda..7da3474c0 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -336,7 +336,7 @@ class ServerManager(local_base.BootingManagerWithFind): zone_blob=None, reservation_id=None, min_count=None, max_count=None, security_groups=None, userdata=None, key_name=None, availability_zone=None, - block_device_mapping=None, nics=None): + block_device_mapping=None, nics=None, **kwargs): # TODO: (anthony) indicate in doc string if param is an extension # and/or optional """ @@ -375,22 +375,26 @@ class ServerManager(local_base.BootingManagerWithFind): max_count = min_count if min_count > max_count: min_count = max_count + + boot_args = [name, image, flavor] + + boot_kwargs = dict( + meta=meta, files=files, userdata=userdata, zone_blob=zone_blob, + reservation_id=reservation_id, min_count=min_count, + max_count=max_count, security_groups=security_groups, + key_name=key_name, availability_zone=availability_zone, + **kwargs) + if block_device_mapping: - return self._boot("/os-volumes_boot", "server", - name, image, flavor, - meta=meta, files=files, userdata=userdata, - zone_blob=zone_blob, reservation_id=reservation_id, - min_count=min_count, max_count=max_count, - security_groups=security_groups, key_name=key_name, - availability_zone=availability_zone, - block_device_mapping=block_device_mapping) + resource_url = "/os-volumes_boot" + boot_kwargs['block_device_mapping'] = block_device_mapping else: - return self._boot("/servers", "server", name, image, flavor, - meta=meta, files=files, userdata=userdata, - zone_blob=zone_blob, reservation_id=reservation_id, - min_count=min_count, max_count=max_count, - security_groups=security_groups, key_name=key_name, - availability_zone=availability_zone, nics=nics) + resource_url = "/servers" + boot_kwargs['nics'] = nics + + response_key = "server" + return self._boot(resource_url, response_key, *boot_args, + **boot_kwargs) def update(self, server, name=None): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index cc8f7c90b..4813ec74c 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -51,7 +51,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): flavor = args.flavor image = args.image - metadata = dict(v.split('=') for v in args.meta) + meta = dict(v.split('=') for v in args.meta) files = {} for f in args.files: @@ -89,12 +89,12 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): if args.user_data: try: - user_data = open(args.user_data) + userdata = open(args.user_data) except IOError, e: raise exceptions.CommandError("Can't open '%s': %s" % \ (args.user_data, e)) else: - user_data = None + userdata = None if args.availability_zone: availability_zone = args.availability_zone @@ -119,9 +119,22 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): nic_info[k] = v nics.append(nic_info) - return (args.name, image, flavor, metadata, files, key_name, - reservation_id, min_count, max_count, user_data, \ - availability_zone, security_groups, block_device_mapping, nics) + boot_args = [args.name, image, flavor] + + boot_kwargs = dict( + meta=meta, + files=files, + key_name=key_name, + reservation_id=reservation_id, + min_count=min_count, + max_count=max_count, + userdata=userdata, + availability_zone=availability_zone, + security_groups=security_groups, + block_device_mapping=block_device_mapping, + nics=nics) + + return boot_args, boot_kwargs @utils.arg('--flavor', @@ -186,21 +199,12 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): "v4-fixed-ip: IPv4 fixed address for NIC (optional).") def do_boot(cs, args): """Boot a new server.""" - name, image, flavor, metadata, files, key_name, reservation_id, \ - min_count, max_count, user_data, availability_zone, \ - security_groups, block_device_mapping, nics = _boot(cs, args) + boot_args, boot_kwargs = _boot(cs, args) - server = cs.servers.create(args.name, image, flavor, - meta=metadata, - files=files, - min_count=min_count, - max_count=max_count, - userdata=user_data, - availability_zone=availability_zone, - security_groups=security_groups, - key_name=key_name, - block_device_mapping=block_device_mapping, - nics=nics) + extra_boot_kwargs = utils.get_resource_manager_extra_kwargs(do_boot, args) + boot_kwargs.update(extra_boot_kwargs) + + server = cs.servers.create(*boot_args, **boot_kwargs) # Keep any information (like adminPass) returned by create info = server._info @@ -269,23 +273,17 @@ def do_boot(cs, args): @utils.arg('name', metavar='', help='Name for the new server') def do_zone_boot(cs, args): """Boot a new server, potentially across Zones.""" - reservation_id = args.reservation_id - min_count = args.min_instances - max_count = args.max_instances - name, image, flavor, metadata, \ - files, reservation_id, min_count, max_count,\ - user_data, availability_zone, security_groups = \ - _boot(cs, args, - reservation_id=reservation_id, - min_count=min_count, - max_count=max_count) + boot_args, boot_kwargs = _boot(cs, + args, + reservation_id=args.reservation_id, + min_count=args.min_instances, + max_count=args.max_instances) - reservation_id = cs.zones.boot(args.name, image, flavor, - meta=metadata, - files=files, - reservation_id=reservation_id, - min_count=min_count, - max_count=max_count) + extra_boot_kwargs = utils.get_resource_manager_extra_kwargs( + do_zone_boot, args) + boot_kwargs.update(extra_boot_kwargs) + + reservation_id = cs.zones.boot(*boot_args, **boot_kwargs) print "Reservation ID=", reservation_id diff --git a/novaclient/v1_1/zones.py b/novaclient/v1_1/zones.py index c8308d489..1bc7cfd1a 100644 --- a/novaclient/v1_1/zones.py +++ b/novaclient/v1_1/zones.py @@ -120,7 +120,7 @@ class ZoneManager(local_base.BootingManagerWithFind): def boot(self, name, image, flavor, meta=None, files=None, zone_blob=None, reservation_id=None, min_count=None, - max_count=None): + max_count=None, **kwargs): """ Create (boot) a new server while being aware of Zones. @@ -150,7 +150,7 @@ class ZoneManager(local_base.BootingManagerWithFind): meta=meta, files=files, zone_blob=zone_blob, reservation_id=reservation_id, return_raw=True, min_count=min_count, - max_count=max_count) + max_count=max_count, **kwargs) def select(self, *args, **kwargs): """