Extensions can now modify resources.

Change-Id: I0d9c1050428d0ccf2e2b18053d75e0465463c08b
This commit is contained in:
Rick Harris 2011-12-21 19:25:19 +00:00
parent 68d0abb061
commit 88bdfdd12f
9 changed files with 210 additions and 90 deletions

View File

@ -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):

39
novaclient/extension.py Normal file
View File

@ -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 "<Extension '%s'>" % self.name

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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):
"""

View File

@ -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='<name>', 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

View File

@ -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):
"""