Extensions can now modify resources.
Change-Id: I0d9c1050428d0ccf2e2b18053d75e0465463c08b
This commit is contained in:
parent
68d0abb061
commit
88bdfdd12f
@ -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
39
novaclient/extension.py
Normal 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
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user