Tests working again...merged in some work we did earlier.

This commit is contained in:
Brian Lamar 2011-08-03 17:41:33 -04:00
parent f8496672cc
commit 454daa2cba
52 changed files with 3172 additions and 3858 deletions

View File

@ -1,17 +0,0 @@
# Copyright 2010 Jacob Kaplan-Moss
# 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.
__version__ = '2.5'

235
novaclient/base.py Normal file
View File

@ -0,0 +1,235 @@
# Copyright 2010 Jacob Kaplan-Moss
# 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.
"""
Base utilities to build API operation managers and objects on top of.
"""
from novaclient import exceptions
# Python 2.4 compat
try:
all
except NameError:
def all(iterable):
return True not in (not x for x in iterable)
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
class Manager(object):
"""
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, api):
self.api = api
def _list(self, url, response_key, obj_class=None, body=None):
resp = None
if body:
resp, body = self.api.client.post(url, body=body)
else:
resp, body = self.api.client.get(url)
if obj_class is None:
obj_class = self.resource_class
return [obj_class(self, res)
for res in body[response_key] if res]
def _get(self, url, response_key):
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):
resp, body = self.api.client.post(url, body=body)
if return_raw:
return body[response_key]
return self.resource_class(self, body[response_key])
def _delete(self, url):
resp, body = self.api.client.delete(url)
def _update(self, url, body):
resp, body = self.api.client.put(url, body=body)
class ManagerWithFind(Manager):
"""
Like a `Manager`, but with additional `find()`/`findall()` methods.
"""
def find(self, **kwargs):
"""
Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
rl = self.findall(**kwargs)
try:
return rl[0]
except IndexError:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(404, msg)
def findall(self, **kwargs):
"""
Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class BootingManagerWithFind(ManagerWithFind):
"""Like a `ManagerWithFind`, but has the ability to boot servers."""
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):
"""
Create (boot) a new server.
:param name: Something to name the server.
:param image: The :class:`Image` to boot with.
:param flavor: The :class:`Flavor` to boot onto.
:param ipgroup: An initial :class:`IPGroup` for this server.
:param meta: A dict of arbitrary key/value metadata to store for this
server. A maximum of five entries is allowed, and both
keys and values must be 255 characters or less.
:param files: A dict of files to overrwrite on the server upon boot.
Keys are file names (i.e. ``/etc/passwd``) and values
are the file contents (either as a string or as a
file-like object). A maximum of five entries is allowed,
and each file must be 10k or less.
:param zone_blob: a single (encrypted) string which is used internally
by Nova for routing between Zones. Users cannot populate
this field.
:param reservation_id: a UUID for the set of servers being requested.
:param return_raw: If True, don't try to coearse the result into
a Resource object.
"""
body = {"server": {
"name": name,
"imageId": getid(image),
"flavorId": getid(flavor),
}}
if ipgroup:
body["server"]["sharedIpGroupId"] = getid(ipgroup)
if meta:
body["server"]["metadata"] = meta
if reservation_id:
body["server"]["reservation_id"] = reservation_id
if zone_blob:
body["server"]["zone_blob"] = zone_blob
if not min_count:
min_count = 1
if not max_count:
max_count = min_count
body["server"]["min_count"] = min_count
body["server"]["max_count"] = max_count
# Files are a slight bit tricky. They're passed in a "personality"
# list to the POST. Each item is a dict giving a file name and the
# base64-encoded contents of the file. We want to allow passing
# either an open file *or* some contents as files here.
if files:
personality = body['server']['personality'] = []
for filepath, file_or_string in files.items():
if hasattr(file_or_string, 'read'):
data = file_or_string.read()
else:
data = file_or_string
personality.append({
'path': filepath,
'contents': data.encode('base64'),
})
return self._create(resource_url, body, response_key,
return_raw=return_raw)
class Resource(object):
"""
A resource represents a particular instance of an object (server, flavor,
etc). This is pretty much just a bag for attributes.
"""
def __init__(self, manager, info):
self.manager = manager
self._info = info
self._add_details(info)
def _add_details(self, info):
for (k, v) in info.iteritems():
setattr(self, k, v)
def __getattr__(self, k):
self.get()
if k not in self.__dict__:
raise AttributeError(k)
else:
return self.__dict__[k]
def __repr__(self):
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
def get(self):
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info

157
novaclient/client.py Normal file
View File

@ -0,0 +1,157 @@
# Copyright 2010 Jacob Kaplan-Moss
"""
OpenStack Client interface. Handles the REST calls and responses.
"""
import time
import urlparse
import urllib
import httplib2
import logging
try:
import json
except ImportError:
import simplejson as json
# Python 2.5 compat fix
if not hasattr(urlparse, 'parse_qsl'):
import cgi
urlparse.parse_qsl = cgi.parse_qsl
from novaclient import exceptions
_logger = logging.getLogger(__name__)
class HTTPClient(httplib2.Http):
USER_AGENT = 'python-novaclient'
def __init__(self, user, apikey, projectid, auth_url, timeout=None):
super(HTTPClient, self).__init__(timeout=timeout)
self.user = user
self.apikey = apikey
self.projectid = projectid
self.auth_url = auth_url
self.version = 'v1.0'
self.management_url = None
self.auth_token = None
# httplib2 overrides
self.force_exception_to_status_code = True
def http_log(self, args, kwargs, resp, body):
if not _logger.isEnabledFor(logging.DEBUG):
return
string_parts = ['curl -i']
for element in args:
if element in ('GET', 'POST'):
string_parts.append(' -X %s' % element)
else:
string_parts.append(' %s' % element)
for element in kwargs['headers']:
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
string_parts.append(header)
_logger.debug("REQ: %s\n" % "".join(string_parts))
_logger.debug("RESP:%s %s\n", resp, body)
def request(self, *args, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers']['User-Agent'] = self.USER_AGENT
if 'body' in kwargs:
kwargs['headers']['Content-Type'] = 'application/json'
kwargs['body'] = json.dumps(kwargs['body'])
resp, body = super(HTTPClient, self).request(*args, **kwargs)
self.http_log(args, kwargs, resp, body)
if body:
try:
body = json.loads(body)
except ValueError, e:
pass
else:
body = None
if resp.status in (400, 401, 403, 404, 408, 413, 500, 501):
raise exceptions.from_response(resp, body)
return resp, body
def _cs_request(self, url, method, **kwargs):
if not self.management_url:
self.authenticate()
# Perform the request once. If we get a 401 back then it
# might be because the auth token expired, so try to
# re-authenticate and try again. If it still fails, bail.
try:
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
if self.projectid:
kwargs['headers']['X-Auth-Project-Id'] = self.projectid
resp, body = self.request(self.management_url + url, method,
**kwargs)
return resp, body
except exceptions.Unauthorized, ex:
try:
self.authenticate()
resp, body = self.request(self.management_url + url, method,
**kwargs)
return resp, body
except exceptions.Unauthorized:
raise ex
def get(self, url, **kwargs):
url = self._munge_get_url(url)
return self._cs_request(url, 'GET', **kwargs)
def post(self, url, **kwargs):
return self._cs_request(url, 'POST', **kwargs)
def put(self, url, **kwargs):
return self._cs_request(url, 'PUT', **kwargs)
def delete(self, url, **kwargs):
return self._cs_request(url, 'DELETE', **kwargs)
def authenticate(self):
scheme, netloc, path, query, frag = urlparse.urlsplit(
self.auth_url)
path_parts = path.split('/')
for part in path_parts:
if len(part) > 0 and part[0] == 'v':
self.version = part
break
headers = {'X-Auth-User': self.user,
'X-Auth-Key': self.apikey}
if self.projectid:
headers['X-Auth-Project-Id'] = self.projectid
resp, body = self.request(self.auth_url, 'GET', headers=headers)
self.management_url = resp['x-server-management-url']
self.auth_token = resp['x-auth-token']
def _munge_get_url(self, url):
"""
Munge GET URLs to always return uncached content.
The OpenStack Compute API caches data *very* agressively and doesn't
respect cache headers. To avoid stale data, then, we append a little
bit of nonsense onto GET parameters; this appears to force the data not
to be cached.
"""
scheme, netloc, path, query, frag = urlparse.urlsplit(url)
query = urlparse.parse_qsl(query)
query.append(('fresh', str(time.time())))
query = urllib.urlencode(query)
return urlparse.urlunsplit((scheme, netloc, path, query, frag))

View File

@ -3,7 +3,12 @@
Exception definitions.
"""
class OpenStackException(Exception):
class CommandError(Exception):
pass
class ClientException(Exception):
"""
The base exception class for all exceptions this library raises.
"""
@ -16,7 +21,7 @@ class OpenStackException(Exception):
return "%s (HTTP %s)" % (self.message, self.code)
class BadRequest(OpenStackException):
class BadRequest(ClientException):
"""
HTTP 400 - Bad request: you sent some malformed data.
"""
@ -24,7 +29,7 @@ class BadRequest(OpenStackException):
message = "Bad request"
class Unauthorized(OpenStackException):
class Unauthorized(ClientException):
"""
HTTP 401 - Unauthorized: bad credentials.
"""
@ -32,7 +37,7 @@ class Unauthorized(OpenStackException):
message = "Unauthorized"
class Forbidden(OpenStackException):
class Forbidden(ClientException):
"""
HTTP 403 - Forbidden: your credentials don't give you access to this
resource.
@ -41,7 +46,7 @@ class Forbidden(OpenStackException):
message = "Forbidden"
class NotFound(OpenStackException):
class NotFound(ClientException):
"""
HTTP 404 - Not found
"""
@ -49,7 +54,7 @@ class NotFound(OpenStackException):
message = "Not found"
class OverLimit(OpenStackException):
class OverLimit(ClientException):
"""
HTTP 413 - Over limit: you're over the API limits for this time period.
"""
@ -58,7 +63,7 @@ class OverLimit(OpenStackException):
# NotImplemented is a python keyword.
class HTTPNotImplemented(OpenStackException):
class HTTPNotImplemented(ClientException):
"""
HTTP 501 - Not Implemented: the server does not support this operation.
"""
@ -69,7 +74,7 @@ class HTTPNotImplemented(OpenStackException):
# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
# so we can do this:
# _code_map = dict((c.http_status, c)
# for c in OpenStackException.__subclasses__())
# for c in ClientException.__subclasses__())
#
# Instead, we have to hardcode it:
_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
@ -78,7 +83,7 @@ _code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
def from_response(response, body):
"""
Return an instance of an OpenStackException or subclass
Return an instance of an ClientException or subclass
based on an httplib2 response.
Usage::
@ -87,7 +92,7 @@ def from_response(response, body):
if resp.status != 200:
raise exception_from_response(resp, body)
"""
cls = _code_map.get(response.status, OpenStackException)
cls = _code_map.get(response.status, ClientException)
if body:
message = "n/a"
details = "n/a"

204
novaclient/shell.py Normal file
View File

@ -0,0 +1,204 @@
# Copyright 2010 Jacob Kaplan-Moss
# 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.
"""
Command-line interface to the OpenStack Nova API.
"""
import argparse
import httplib2
import os
import prettytable
import sys
from novaclient import exceptions
from novaclient import utils
from novaclient.v1_0 import shell as shell_v1_0
from novaclient.v1_1 import shell as shell_v1_1
def env(e):
return os.environ.get(e, '')
class OpenStackComputeShell(object):
# Hook for the test suite to inject a fake server.
_api_class = None
def get_base_parser(self):
parser = argparse.ArgumentParser(
prog='nova',
description=__doc__.strip(),
epilog='See "nova help COMMAND" '\
'for help on a specific command.',
add_help=False,
formatter_class=OpenStackHelpFormatter,
)
# Global arguments
parser.add_argument('-h', '--help',
action='help',
help=argparse.SUPPRESS,
)
parser.add_argument('--debug',
default=False,
action='store_true',
help=argparse.SUPPRESS)
parser.add_argument('--username',
default=env('NOVA_USERNAME'),
help='Defaults to env[NOVA_USERNAME].')
parser.add_argument('--apikey',
default=env('NOVA_API_KEY'),
help='Defaults to env[NOVA_API_KEY].')
parser.add_argument('--projectid',
default=env('NOVA_PROJECT_ID'),
help='Defaults to env[NOVA_PROJECT_ID].')
parser.add_argument('--url',
default=env('NOVA_URL'),
help='Defaults to env[NOVA_URL].')
parser.add_argument('--version',
default='1.1',
help='Accepts 1.0 or 1.1, defaults to 1.1')
return parser
def get_subcommand_parser(self, version):
parser = self.get_base_parser()
self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>')
actions_module = {
'1.0': shell_v1_0,
'1.1': shell_v1_1,
}[version]
self._find_actions(subparsers, actions_module)
self._find_actions(subparsers, self)
return parser
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.
command = attr[3:].replace('_', '-')
callback = getattr(actions_module, attr)
desc = callback.__doc__ or ''
help = desc.strip().split('\n')[0]
arguments = getattr(callback, '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)
def main(self, argv):
# Parse args once to find version
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
# build available subcommands based on version
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)
# Deal with global arguments
if args.debug:
httplib2.debuglevel = 1
# Short-circuit and deal with help right away.
if args.func == self.do_help:
self.do_help(args)
return 0
user, apikey, projectid, url = args.username, args.apikey, \
args.projectid, args.url
#FIXME(usrleon): Here should be restrict for project id same as
# for username or apikey but for compatibility it is not.
if not user:
raise exceptions.CommandError("You must provide a username, either via "
"--username or via env[NOVA_USERNAME]")
if not apikey:
raise exceptions.CommandError("You must provide an API key, either via "
"--apikey or via env[NOVA_API_KEY]")
self.cs = self.get_api_class()(user, apikey, projectid, url)
try:
self.cs.authenticate()
except exceptions.Unauthorized:
raise exceptions.CommandError("Invalid OpenStack Nova credentials.")
args.func(self.cs, args)
def get_api_class(self):
return self._api_class or shell_v1_0.CLIENT_CLASS
@utils.arg('command', metavar='<subcommand>', nargs='?',
help='Display help for <subcommand>')
def do_help(self, args):
"""
Display help about this program or one of its subcommands.
"""
if args.command:
if args.command in self.subcommands:
self.subcommands[args.command].print_help()
else:
raise exceptions.CommandError("'%s' is not a valid subcommand." %
args.command)
else:
self.parser.print_help()
# I'm picky about my shell help.
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:
OpenStackComputeShell().main(sys.argv[1:])
except Exception, e:
if httplib2.debuglevel == 1:
raise # dump stack.
else:
print >> sys.stderr, e
sys.exit(1)

43
novaclient/utils.py Normal file
View File

@ -0,0 +1,43 @@
import prettytable
# 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')

View File

@ -1,74 +0,0 @@
# Copyright 2010 Jacob Kaplan-Moss
# 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.v1_0 import accounts
from novaclient.v1_0 import backup_schedules
from novaclient.v1_0 import client
from novaclient.v1_0 import exceptions
from novaclient.v1_0 import flavors
from novaclient.v1_0 import images
from novaclient.v1_0 import ipgroups
from novaclient.v1_0 import servers
from novaclient.v1_0 import zones
class Client(object):
"""
Top-level object to access the OpenStack Compute v1.0 API.
Create an instance with your creds::
>>> os = novaclient.v1_0.Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
Then call methods on its managers::
>>> os.servers.list()
...
>>> os.flavors.list()
...
&c.
"""
def __init__(self, username, apikey, projectid, auth_url=None, timeout=None):
"""Initialize v1.0 Openstack Client."""
self.backup_schedules = backup_schedules.BackupScheduleManager(self)
self.flavors = flavors.FlavorManager(self)
self.images = images.ImageManager(self)
self.ipgroups = ipgroups.IPGroupManager(self)
self.servers = servers.ServerManager(self)
self.zones = zones.ZoneManager(self)
self.accounts = accounts.AccountManager(self)
auth_url = auth_url or "https://auth.api.rackspacecloud.com/v1.0"
self.client = client.HTTPClient(username,
apikey,
projectid,
auth_url,
timeout=timeout)
def authenticate(self):
"""
Authenticate against the server.
Normally this is called automatically when you first access the API,
but you can call this method to force authentication right now.
Returns on success; raises :exc:`novaclient.Unauthorized` if the
credentials are wrong.
"""
self.client.authenticate()

View File

@ -1,10 +1,13 @@
from novaclient.v1_0 import base
from novaclient import base
from novaclient.v1_0 import base as local_base
class Account(base.Resource):
pass
class AccountManager(base.BootingManagerWithFind):
class AccountManager(local_base.BootingManagerWithFind):
resource_class = Account
def create_instance_for(self, account_id, name, image, flavor,

View File

@ -3,7 +3,8 @@
Backup Schedule interface.
"""
from novaclient.v1_0 import base
from novaclient import base
BACKUP_WEEKLY_DISABLED = 'DISABLED'
BACKUP_WEEKLY_SUNDAY = 'SUNDAY'

View File

@ -19,7 +19,9 @@
Base utilities to build API operation managers and objects on top of.
"""
from novaclient.v1_0 import exceptions
from novaclient import base
from novaclient import exceptions
# Python 2.4 compat
try:
@ -29,103 +31,7 @@ except NameError:
return True not in (not x for x in iterable)
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
class Manager(object):
"""
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, api):
self.api = api
def _list(self, url, response_key, obj_class=None, body=None):
resp = None
if body:
resp, body = self.api.client.post(url, body=body)
else:
resp, body = self.api.client.get(url)
if obj_class is None:
obj_class = self.resource_class
return [obj_class(self, res)
for res in body[response_key] if res]
def _get(self, url, response_key):
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):
resp, body = self.api.client.post(url, body=body)
if return_raw:
return body[response_key]
return self.resource_class(self, body[response_key])
def _delete(self, url):
resp, body = self.api.client.delete(url)
def _update(self, url, body):
resp, body = self.api.client.put(url, body=body)
class ManagerWithFind(Manager):
"""
Like a `Manager`, but with additional `find()`/`findall()` methods.
"""
def find(self, **kwargs):
"""
Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
rl = self.findall(**kwargs)
try:
return rl[0]
except IndexError:
raise exceptions.NotFound(404, "No %s matching %s." %
(self.resource_class.__name__, kwargs))
def findall(self, **kwargs):
"""
Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class BootingManagerWithFind(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,
ipgroup=None, meta=None, files=None, zone_blob=None,
@ -155,11 +61,11 @@ class BootingManagerWithFind(ManagerWithFind):
"""
body = {"server": {
"name": name,
"imageId": getid(image),
"flavorId": getid(flavor),
"imageId": base.getid(image),
"flavorId": base.getid(flavor),
}}
if ipgroup:
body["server"]["sharedIpGroupId"] = getid(ipgroup)
body["server"]["sharedIpGroupId"] = base.getid(ipgroup)
if meta:
body["server"]["metadata"] = meta
if reservation_id:
@ -192,43 +98,3 @@ class BootingManagerWithFind(ManagerWithFind):
return self._create(resource_url, body, response_key,
return_raw=return_raw)
class Resource(object):
"""
A resource represents a particular instance of an object (server, flavor,
etc). This is pretty much just a bag for attributes.
"""
def __init__(self, manager, info):
self.manager = manager
self._info = info
self._add_details(info)
def _add_details(self, info):
for (k, v) in info.iteritems():
setattr(self, k, v)
def __getattr__(self, k):
self.get()
if k not in self.__dict__:
raise AttributeError(k)
else:
return self.__dict__[k]
def __repr__(self):
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
def get(self):
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info

View File

@ -1,154 +1,60 @@
# Copyright 2010 Jacob Kaplan-Moss
"""
OpenStack Client interface. Handles the REST calls and responses.
"""
import time
import urlparse
import urllib
import httplib2
import logging
try:
import json
except ImportError:
import simplejson as json
from novaclient import client
from novaclient.v1_0 import accounts
from novaclient.v1_0 import backup_schedules
from novaclient.v1_0 import flavors
from novaclient.v1_0 import images
from novaclient.v1_0 import ipgroups
from novaclient.v1_0 import servers
from novaclient.v1_0 import zones
# Python 2.5 compat fix
if not hasattr(urlparse, 'parse_qsl'):
import cgi
urlparse.parse_qsl = cgi.parse_qsl
import novaclient
from novaclient.v1_0 import exceptions
_logger = logging.getLogger(__name__)
class Client(object):
"""
Top-level object to access the OpenStack Compute API.
class HTTPClient(httplib2.Http):
Create an instance with your creds::
USER_AGENT = 'python-novaclient/%s' % novaclient.__version__
>>> client = Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
def __init__(self, user, apikey, projectid, auth_url, timeout=None):
super(HTTPClient, self).__init__(timeout=timeout)
self.user = user
self.apikey = apikey
self.projectid = projectid
self.auth_url = auth_url
self.version = 'v1.0'
Then call methods on its managers::
self.management_url = None
self.auth_token = None
>>> client.servers.list()
...
>>> client.flavors.list()
...
# httplib2 overrides
self.force_exception_to_status_code = True
"""
def http_log(self, args, kwargs, resp, body):
if not _logger.isEnabledFor(logging.DEBUG):
return
def __init__(self, username, api_key, project_id, auth_url=None,
timeout=None):
string_parts = ['curl -i']
for element in args:
if element in ('GET','POST'):
string_parts.append(' -X %s' % element)
else:
string_parts.append(' %s' % element)
self.accounts = accounts.AccountManager(self)
self.backup_schedules = backup_schedules.BackupScheduleManager(self)
self.flavors = flavors.FlavorManager(self)
self.images = images.ImageManager(self)
self.ipgroups = ipgroups.IPGroupManager(self)
self.servers = servers.ServerManager(self)
self.zones = zones.ZoneManager(self)
for element in kwargs['headers']:
string_parts.append(' -H "%s: %s"' % (element,kwargs['headers'][element]))
_auth_url = auth_url or 'https://auth.api.rackspacecloud.com/v1.0'
_logger.debug("REQ: %s\n" % "".join(string_parts))
_logger.debug("RESP:%s %s\n", resp,body)
def request(self, *args, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers']['User-Agent'] = self.USER_AGENT
if 'body' in kwargs:
kwargs['headers']['Content-Type'] = 'application/json'
kwargs['body'] = json.dumps(kwargs['body'])
resp, body = super(HTTPClient, self).request(*args, **kwargs)
self.http_log(args, kwargs, resp, body)
if body:
try:
body = json.loads(body)
except ValueError, e:
pass
else:
body = None
if resp.status in (400, 401, 403, 404, 408, 413, 500, 501):
raise exceptions.from_response(resp, body)
return resp, body
def _cs_request(self, url, method, **kwargs):
if not self.management_url:
self.authenticate()
# Perform the request once. If we get a 401 back then it
# might be because the auth token expired, so try to
# re-authenticate and try again. If it still fails, bail.
try:
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
if self.projectid:
kwargs['headers']['X-Auth-Project-Id'] = self.projectid
resp, body = self.request(self.management_url + url, method,
**kwargs)
return resp, body
except exceptions.Unauthorized, ex:
try:
self.authenticate()
resp, body = self.request(self.management_url + url, method,
**kwargs)
return resp, body
except exceptions.Unauthorized:
raise ex
def get(self, url, **kwargs):
url = self._munge_get_url(url)
return self._cs_request(url, 'GET', **kwargs)
def post(self, url, **kwargs):
return self._cs_request(url, 'POST', **kwargs)
def put(self, url, **kwargs):
return self._cs_request(url, 'PUT', **kwargs)
def delete(self, url, **kwargs):
return self._cs_request(url, 'DELETE', **kwargs)
self.client = client.HTTPClient(username,
api_key,
project_id,
_auth_url,
timeout=timeout)
def authenticate(self):
scheme, netloc, path, query, frag = urlparse.urlsplit(
self.auth_url)
path_parts = path.split('/')
for part in path_parts:
if len(part) > 0 and part[0] == 'v':
self.version = part
break
headers = {'X-Auth-User': self.user,
'X-Auth-Key': self.apikey}
if self.projectid:
headers['X-Auth-Project-Id'] = self.projectid
resp, body = self.request(self.auth_url, 'GET', headers=headers)
self.management_url = resp['x-server-management-url']
self.auth_token = resp['x-auth-token']
def _munge_get_url(self, url):
"""
Munge GET URLs to always return uncached content.
Authenticate against the server.
The OpenStack Nova API caches data *very* agressively and doesn't
respect cache headers. To avoid stale data, then, we append a little
bit of nonsense onto GET parameters; this appears to force the data not
to be cached.
Normally this is called automatically when you first access the API,
but you can call this method to force authentication right now.
Returns on success; raises :exc:`exceptions.Unauthorized` if the
credentials are wrong.
"""
scheme, netloc, path, query, frag = urlparse.urlsplit(url)
query = urlparse.parse_qsl(query)
query.append(('fresh', str(time.time())))
query = urllib.urlencode(query)
return urlparse.urlunsplit((scheme, netloc, path, query, frag))
self.client.authenticate()

View File

@ -3,7 +3,7 @@
Flavor interface.
"""
from novaclient.v1_0 import base
from novaclient import base
class Flavor(base.Resource):

View File

@ -3,7 +3,7 @@
Image interface.
"""
from novaclient.v1_0 import base
from novaclient import base
class Image(base.Resource):
@ -46,8 +46,7 @@ class ImageManager(base.ManagerWithFind):
detail = "/detail"
return self._list("/images%s" % detail, "images")
def create(self, server, name, image_type=None, backup_type=None, rotation=None):
def create(self, server, name):
"""
Create a new image by snapshotting a running :class:`Server`
@ -55,23 +54,7 @@ class ImageManager(base.ManagerWithFind):
:param server: The :class:`Server` (or its ID) to make a snapshot of.
:rtype: :class:`Image`
"""
if image_type is None:
image_type = "snapshot"
if image_type not in ("backup", "snapshot"):
raise Exception("Invalid image_type: must be backup or snapshot")
if image_type == "backup":
if not rotation:
raise Exception("rotation is required for backups")
elif not backup_type:
raise Exception("backup_type required for backups")
elif backup_type not in ("daily", "weekly"):
raise Exception("Invalid backup_type: must be daily or weekly")
data = {"image": {"serverId": base.getid(server), "name": name,
"image_type": image_type, "backup_type": backup_type,
"rotation": rotation}}
data = {"image": {"serverId": base.getid(server), "name": name}}
return self._create("/images", data, "image")
def delete(self, image):

View File

@ -3,7 +3,7 @@
IP Group interface.
"""
from novaclient.v1_0 import base
from novaclient import base
class IPGroup(base.Resource):

View File

@ -20,7 +20,10 @@ Server interface.
"""
import urllib
from novaclient.v1_0 import base
from novaclient import base
from novaclient.v1_0 import base as local_base
REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
@ -154,6 +157,18 @@ class Server(base.Resource):
"""
self.manager.resize(self, flavor)
def backup(self, image_name, backup_type, rotation):
"""
Create a server backup.
:param server: The :class:`Server` (or its ID).
:param image_name: The name to assign the newly create image.
:param backup_type: 'daily' or 'weekly'
:param rotation: number of backups of type 'backup_type' to keep
:returns Newly created :class:`Image` object
"""
return self.manager.backup(self, image_name, backup_type, rotation)
def confirm_resize(self):
"""
Confirm that the resize worked, thus removing the original server.
@ -198,7 +213,7 @@ class Server(base.Resource):
return self.addresses['private']
class ServerManager(base.BootingManagerWithFind):
class ServerManager(local_base.BootingManagerWithFind):
resource_class = Server
def get(self, server):
@ -370,6 +385,31 @@ class ServerManager(base.BootingManagerWithFind):
"""
self._action('resize', server, {'flavorId': base.getid(flavor)})
def backup(self, server, image_name, backup_type, rotation):
"""
Create a server backup.
:param server: The :class:`Server` (or its ID).
:param image_name: The name to assign the newly create image.
:param backup_type: 'daily' or 'weekly'
:param rotation: number of backups of type 'backup_type' to keep
:returns Newly created :class:`Image` object
"""
if not rotation:
raise Exception("rotation is required for backups")
elif not backup_type:
raise Exception("backup_type required for backups")
elif backup_type not in ("daily", "weekly"):
raise Exception("Invalid backup_type: must be daily or weekly")
data = {
"name": image_name,
"rotation": rotation,
<