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,
"backup_type": backup_type,
}
self._action('createBackup', server, data)
def pause(self, server):
"""
Pause the server.

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,8 @@
Zone interface.
"""
from novaclient.v1_0 import base
from novaclient import base
from novaclient.v1_0 import base as local_base
class Weighting(base.Resource):
@ -64,7 +65,7 @@ class Zone(base.Resource):
weight_offset, weight_scale)
class ZoneManager(base.BootingManagerWithFind):
class ZoneManager(local_base.BootingManagerWithFind):
resource_class = Zone
def info(self):

View File

@ -1,66 +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_1 import client
from novaclient.v1_1 import exceptions
from novaclient.v1_1 import flavors
from novaclient.v1_1 import images
from novaclient.v1_1 import servers
class Client(object):
"""
Top-level object to access the OpenStack Compute v1.0 API.
Create an instance with your creds::
>>> os = novaclient.v1_1.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.flavors = flavors.FlavorManager(self)
self.images = images.ImageManager(self)
self.servers = servers.ServerManager(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

@ -19,7 +19,7 @@
Base utilities to build API operation managers and objects on top of.
"""
from novaclient.v1_1 import exceptions
from novaclient import exceptions
# Python 2.4 compat
try:

View File

@ -1,154 +1,48 @@
# 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_1 import flavors
from novaclient.v1_1 import images
from novaclient.v1_1 import servers
# Python 2.5 compat fix
if not hasattr(urlparse, 'parse_qsl'):
import cgi
urlparse.parse_qsl = cgi.parse_qsl
import novaclient
from novaclient.v1_1 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, timeout=None):
self.flavors = flavors.FlavorManager(self)
self.images = images.ImageManager(self)
self.servers = servers.ServerManager(self)
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']:
string_parts.append(' -H "%s: %s"' % (element,kwargs['headers'][element]))
_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

@ -1,100 +0,0 @@
# Copyright 2010 Jacob Kaplan-Moss
"""
Exception definitions.
"""
class OpenStackException(Exception):
"""
The base exception class for all exceptions this library raises.
"""
def __init__(self, code, message=None, details=None):
self.code = code
self.message = message or self.__class__.message
self.details = details
def __str__(self):
return "%s (HTTP %s)" % (self.message, self.code)
class BadRequest(OpenStackException):
"""
HTTP 400 - Bad request: you sent some malformed data.
"""
http_status = 400
message = "Bad request"
class Unauthorized(OpenStackException):
"""
HTTP 401 - Unauthorized: bad credentials.
"""
http_status = 401
message = "Unauthorized"
class Forbidden(OpenStackException):
"""
HTTP 403 - Forbidden: your credentials don't give you access to this
resource.
"""
http_status = 403
message = "Forbidden"
class NotFound(OpenStackException):
"""
HTTP 404 - Not found
"""
http_status = 404
message = "Not found"
class OverLimit(OpenStackException):
"""
HTTP 413 - Over limit: you're over the API limits for this time period.
"""
http_status = 413
message = "Over limit"
# NotImplemented is a python keyword.
class HTTPNotImplemented(OpenStackException):
"""
HTTP 501 - Not Implemented: the server does not support this operation.
"""
http_status = 501
message = "Not Implemented"
# 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__())
#
# Instead, we have to hardcode it:
_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
Forbidden, NotFound, OverLimit, HTTPNotImplemented])
def from_response(response, body):
"""
Return an instance of an OpenStackException or subclass
based on an httplib2 response.
Usage::
resp, body = http.request(...)
if resp.status != 200:
raise exception_from_response(resp, body)
"""
cls = _code_map.get(response.status, OpenStackException)
if body:
message = "n/a"
details = "n/a"
if hasattr(body, 'keys'):
error = body[body.keys()[0]]
message = error.get('message', None)
details = error.get('details', None)
return cls(code=response.status, message=message, details=details)
else:
return cls(code=response.status)

View File

@ -3,7 +3,7 @@
Flavor interface.
"""
from novaclient.v1_1 import base
from novaclient import base
class Flavor(base.Resource):
@ -26,10 +26,10 @@ class FlavorManager(base.ManagerWithFind):
:rtype: list of :class:`Flavor`.
"""
detail = ""
if detailed:
detail = "/detail"
return self._list("/flavors%s" % detail, "flavors")
if detailed is True:
return self._list("/flavors/detail", "flavors")
else:
return self._list("/flavors", "flavors")
def get(self, flavor):
"""

View File

@ -3,7 +3,7 @@
Image interface.
"""
from novaclient.v1_1 import base
from novaclient import base
class Image(base.Resource):
@ -41,38 +41,10 @@ class ImageManager(base.ManagerWithFind):
:rtype: list of :class:`Image`
"""
detail = ""
if detailed:
detail = "/detail"
return self._list("/images%s" % detail, "images")
def create(self, server, name, image_type=None, backup_type=None, rotation=None):
"""
Create a new image by snapshotting a running :class:`Server`
:param name: An (arbitrary) name for the new image.
: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}}
return self._create("/images", data, "image")
if detailed is True:
return self._list("/images/detail", "images")
else:
return self._list("/images", "images")
def delete(self, image):
"""

View File

@ -20,7 +20,10 @@ Server interface.
"""
import urllib
from novaclient.v1_1 import base
from novaclient import base
from novaclient.v1_0 import base as local_base
REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
@ -42,22 +45,11 @@ class Server(base.Resource):
:param name: Update the server's name.
:param password: Update the root password.
"""
self.manager.update(self, name)
def create_image(self, name, metadata=None):
"""
Create an image based on this server.
:param name: The name of the image to create
:param metadata: The metadata to associated with the image.
"""
self.manager.create_image(self, name, metadata)
self.manager.update(self, name=name)
def change_password(self, password):
"""
Update the root password on this server.
:param password: The password to set.
Update the password for a server.
"""
self.manager.change_password(self, password)
@ -91,6 +83,15 @@ class Server(base.Resource):
"""
self.manager.resize(self, flavor)
def create_image(self, image_name, metadata):
"""
Create an image based on this server.
:param image_name: The name to assign the newly create image.
:param metadata: Metadata to assign to the image.
"""
self.manager.create_image(self, image_name, metadata)
def confirm_resize(self):
"""
Confirm that the resize worked, thus removing the original server.
@ -121,24 +122,8 @@ class Server(base.Resource):
return ""
return self.addresses['private']
@property
def image_id(self):
"""
Shortcut to get the image identifier.
"""
return self.image["id"]
@property
def flavor_id(self):
"""
Shortcut to get the flavor identifier.
"""
return self.flavor["id"]
class ServerManager(base.BootingManagerWithFind):
class ServerManager(local_base.BootingManagerWithFind):
resource_class = Server
def get(self, server):
@ -161,8 +146,9 @@ class ServerManager(base.BootingManagerWithFind):
"""
if search_opts is None:
search_opts = {}
qparams = {}
# only use values in query string if they are set
for opt, val in search_opts.iteritems():
if val:
qparams[opt] = val
@ -190,8 +176,30 @@ class ServerManager(base.BootingManagerWithFind):
file-like object). A maximum of five entries is allowed,
and each file must be 10k or less.
"""
return self._boot("/servers", "server", name, image, flavor,
meta=meta, files=files)
personality = []
for file_path, filelike in files.items():
try:
data = filelike.read()
except AttributeError:
data = str(filelike)
personality.append({
"path": file_path,
"contents": data.encode("base64"),
})
body = {
"server": {
"name": name,
"imageRef": base.getid(image),
"flavorRef": base.getid(flavor),
"metadata": meta or {},
"personality": personality,
},
}
return self._create("/servers", body, "server", return_raw=False)
def update(self, server, name=None):
"""
@ -211,6 +219,12 @@ class ServerManager(base.BootingManagerWithFind):
self._update("/servers/%s" % base.getid(server), body)
def change_password(self, server, password):
"""
Update the password for a server.
"""
self._action("changePassword", server, {"adminPass": password})
def delete(self, server):
"""
Delete (i.e. shut down and delete the image) this server.
@ -227,32 +241,6 @@ class ServerManager(base.BootingManagerWithFind):
"""
self._action('reboot', server, {'type': type})
def create_image(self, server, name, metadata=None):
"""
Create an image based on this server.
:param server: The :class:`Server` (or its ID) to create image from.
:param name: The name of the image to create
:param metadata: The metadata to associated with the image.
"""
body = {
"name": name,
"metadata": metadata or {},
}
self._action('createImage', server, body)
def change_password(self, server, password):
"""
Update the root password on a server.
:param server: The :class:`Server` (or its ID) to share onto.
:param password: The password to set.
"""
body = {
"adminPass": password,
}
self._action('changePassword', server, body)
def rebuild(self, server, image):
"""
Rebuild -- shut down and then re-image -- a server.
@ -292,6 +280,17 @@ class ServerManager(base.BootingManagerWithFind):
"""
self._action('revertResize', server)
def create_image(self, server, image_name, metadata=None):
"""
Snapshot a server.
:param server: The :class:`Server` (or its ID) to share onto.
:param image_name: Name to give the snapshot image
:param meta: Metadata to give newly-created image entity
"""
self._action('createImage', server,
{'name': image_name, 'metadata': metadata or {}})
def _action(self, action, server, info=None):
"""
Perform a server "action" -- reboot/rebuild/resize/etc.

View File

@ -15,570 +15,316 @@
# License for the specific language governing permissions and limitations
# under the License.
"""
Command-line interface to the OpenStack Nova API.
"""
import argparse
import getpass
import httplib2
import os
import prettytable
import sys
import textwrap
import uuid
import novaclient.v1_1
from novaclient.v1_1 import exceptions
from novaclient import exceptions
from novaclient import utils
from novaclient.v1_1 import client
from novaclient.v1_1 import servers
def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)
CLIENT_CLASS = client.Client
# Sentinal for boot --key
AUTO_KEY = object()
# Decorator for 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 _translate_flavor_keys(collection):
convert = [('ram', 'memory_mb'), ('disk', 'local_gb')]
for item in collection:
keys = item.__dict__.keys()
for from_key, to_key in convert:
if from_key in keys and to_key not in keys:
setattr(item, to_key, item._info[from_key])
@utils.arg('--flavor',
default=None,
metavar='<flavor>',
help="Flavor ID (see 'osc flavors'). "\
"Defaults to 256MB RAM instance.")
@utils.arg('--image',
default=None,
metavar='<image>',
help="Image ID (see 'osc images'). "\
"Defaults to Ubuntu 10.04 LTS.")
@utils.arg('--meta',
metavar="<key=value>",
action='append',
default=[],
help="Record arbitrary key/value metadata. "\
"May be give multiple times.")
@utils.arg('--file',
metavar="<dst-path=src-path>",
action='append',
dest='files',
default=[],
help="Store arbitrary files from <src-path> locally to <dst-path> "\
"on the new server. You may store up to 5 files.")
@utils.arg('--key',
metavar='<path>',
nargs='?',
const=AUTO_KEY,
help="Key the server with an SSH keypair. "\
"Looks in ~/.ssh for a key, "\
"or takes an explicit <path> to one.")
@utils.arg('name', metavar='<name>', help='Name for the new server')
def do_boot(cs, args):
"""Boot a new server."""
name, image, flavor, metadata, files = _boot(cs, args)
server = cs.servers.create(args.name, image, flavor,
meta=metadata, files=files)
utils.print_dict(server._info)
class CommandError(Exception):
pass
def _boot(cs, args):
"""Boot a new server."""
flavor = args.flavor or cs.flavors.find(ram=256)
image = args.image or cs.images.find(name="Ubuntu 10.04 LTS "\
"(lucid)")
def env(e):
return os.environ.get(e, '')
metadata = dict(v.split('=') for v in args.meta)
class OpenStackShell(object):
# Hook for the test suite to inject a fake server.
_api_class = novaclient.v1_1.Client
def __init__(self):
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
self.parser.add_argument('-h', '--help',
action='help',
help=argparse.SUPPRESS,
)
self.parser.add_argument('--debug',
default=False,
action='store_true',
help=argparse.SUPPRESS)
self.parser.add_argument('--username',
default=env('NOVA_USERNAME'),
help='Defaults to env[NOVA_USERNAME].')
self.parser.add_argument('--apikey',
default=env('NOVA_API_KEY'),
help='Defaults to env[NOVA_API_KEY].')
self.parser.add_argument('--projectid',
default=env('NOVA_PROJECT_ID'),
help='Defaults to env[NOVA_PROJECT_ID].')
auth_url = env('NOVA_URL')
if auth_url == '':
auth_url = 'https://auth.api.rackspacecloud.com/v1.0'
self.parser.add_argument('--url',
default=auth_url,
help='Defaults to env[NOVA_URL].')
# Subcommands
subparsers = self.parser.add_subparsers(metavar='<subcommand>')
self.subcommands = {}
# Everything that's do_* is a subcommand.
for attr in (a for a in dir(self) if a.startswith('do_')):
# I prefer to be hypen-separated instead of underscores.
command = attr[3:].replace('_', '-')
callback = getattr(self, 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 and call whatever callback was selected
args = self.parser.parse_args(argv)
# Short-circuit and deal with help right away.
if args.func == self.do_help:
self.do_help(args)
return 0
# Deal with global arguments
if args.debug:
httplib2.debuglevel = 1
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 CommandError("You must provide a username, either via "
"--username or via env[NOVA_USERNAME]")
if not apikey:
raise CommandError("You must provide an API key, either via "
"--apikey or via env[NOVA_API_KEY]")
self.cs = self._api_class(user, apikey, projectid, url)
files = {}
for f in args.files:
dst, src = f.split('=', 1)
try:
self.cs.authenticate()
except exceptions.Unauthorized:
raise CommandError("Invalid OpenStack Nova credentials.")
files[dst] = open(src)
except IOError, e:
raise exceptions.CommandError("Can't open '%s': %s" % (src, e))
args.func(args)
@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 CommandError("'%s' is not a valid subcommand." %
args.command)
if args.key is AUTO_KEY:
possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k)
for k in ('id_dsa.pub', 'id_rsa.pub')]
for k in possible_keys:
if os.path.exists(k):
keyfile = k
break
else:
self.parser.print_help()
raise exceptions.CommandError("Couldn't find a key file: tried "
"~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub")
elif args.key:
keyfile = args.key
else:
keyfile = None
def _boot(self, args, reservation_id=None, min_count=None, max_count=None):
"""Boot a new server."""
flavor = args.flavor or self.cs.flavors.find(ram=256)
image = args.image or self.cs.images.find(name="Ubuntu 10.04 LTS "\
"(lucid)")
metadata = dict(v.split('=') for v in args.meta)
files = {}
for f in args.files:
dst, src = f.split('=', 1)
try:
files[dst] = open(src)
except IOError, e:
raise CommandError("Can't open '%s': %s" % (src, e))
if args.key is AUTO_KEY:
possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k)
for k in ('id_dsa.pub', 'id_rsa.pub')]
for k in possible_keys:
if os.path.exists(k):
keyfile = k
break
else:
raise CommandError("Couldn't find a key file: tried "
"~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub")
elif args.key:
keyfile = args.key
else:
keyfile = None
if keyfile:
try:
files['/root/.ssh/authorized_keys2'] = open(keyfile)
except IOError, e:
raise CommandError("Can't open '%s': %s" % (keyfile, e))
return (args.name, image, flavor, metadata, files)
@arg('--flavor',
default=None,
metavar='<flavor>',
help="Flavor ID (see 'novaclient flavors'). "\
"Defaults to 256MB RAM instance.")
@arg('--image',
default=None,
metavar='<image>',
help="Image ID (see 'novaclient images'). "\
"Defaults to Ubuntu 10.04 LTS.")
@arg('--meta',
metavar="<key=value>",
action='append',
default=[],
help="Record arbitrary key/value metadata. "\
"May be give multiple times.")
@arg('--file',
metavar="<dst-path=src-path>",
action='append',
dest='files',
default=[],
help="Store arbitrary files from <src-path> locally to <dst-path> "\
"on the new server. You may store up to 5 files.")
@arg('--key',
metavar='<path>',
nargs='?',
const=AUTO_KEY,
help="Key the server with an SSH keypair. "\
"Looks in ~/.ssh for a key, "\
"or takes an explicit <path> to one.")
@arg('name', metavar='<name>', help='Name for the new server')
def do_boot(self, args):
"""Boot a new server."""
name, image, flavor, metadata, files = self._boot(args)
server = self.cs.servers.create(args.name, image, flavor,
meta=metadata,
files=files)
print_dict(server._info)
def _translate_flavor_keys(self, collection):
convert = [('ram', 'memory_mb'), ('disk', 'local_gb')]
for item in collection:
keys = item.__dict__.keys()
for from_key, to_key in convert:
if from_key in keys and to_key not in keys:
setattr(item, to_key, item._info[from_key])
@arg('--fixed_ip',
dest='fixed_ip',
metavar='<fixed_ip>',
default=None,
help='Only match against fixed IP.')
@arg('--reservation_id',
dest='reservation_id',
metavar='<reservation_id>',
default=None,
help='Only return instances that match reservation_id.')
@arg('--recurse_zones',
dest='recurse_zones',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Recurse through all zones if set.')
@arg('--ip',
dest='ip',
metavar='<ip_regexp>',
default=None,
help='Search with regular expression match by IP address')
@arg('--ip6',
dest='ip6',
metavar='<ip6_regexp>',
default=None,
help='Search with regular expression match by IPv6 address')
@arg('--server_name',
dest='server_name',
metavar='<name_regexp>',
default=None,
help='Search with regular expression match by server name')
@arg('--name',
dest='display_name',
metavar='<name_regexp>',
default=None,
help='Search with regular expression match by display name')
@arg('--instance_name',
dest='name',
metavar='<name_regexp>',
default=None,
help='Search with regular expression match by instance name')
def do_list(self, args):
"""List active servers."""
recurse_zones = args.recurse_zones
search_opts = {
'reservation_id': args.reservation_id,
'fixed_ip': args.fixed_ip,
'recurse_zones': recurse_zones,
'ip': args.ip,
'ip6': args.ip6,
'name': args.name,
'server_name': args.server_name,
'display_name': args.display_name}
if recurse_zones:
to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP']
else:
to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP']
print_list(self.cs.servers.list(search_opts=search_opts),
to_print)
def do_flavor_list(self, args):
"""Print a list of available 'flavors' (sizes of servers)."""
flavors = self.cs.flavors.list()
self._translate_flavor_keys(flavors)
print_list(flavors, [
'ID',
'Name',
'Memory_MB',
'Swap',
'Local_GB',
'VCPUs',
'RXTX_Quota',
'RXTX_Cap'])
def do_image_list(self, args):
"""Print a list of available images to boot from."""
print_list(self.cs.images.list(), ['ID', 'Name', 'Status'])
@arg('server', metavar='<server>', help='Name or ID of server.')
@arg('name', metavar='<name>', help='Name of backup or snapshot.')
@arg('--image-type',
metavar='<backup|snapshot>',
default='snapshot',
help='type of image (default: snapshot)')
@arg('--backup-type',
metavar='<daily|weekly>',
default=None,
help='type of backup')
@arg('--rotation',
default=None,
type=int,
metavar='<rotation>',
help="Number of backups to retain. Used for backup image_type.")
def do_create_image(self, args):
"""Create a new image by taking a snapshot of a running server."""
server = self._find_server(args.server)
server.create_image(args.name)
@arg('image', metavar='<image>', help='Name or ID of image.')
def do_image_delete(self, args):
"""
Delete an image.
It should go without saying, but you can only delete images you
created.
"""
image = self._find_image(args.image)
image.delete()
@arg('--hard',
dest='reboot_type',
action='store_const',
const=servers.REBOOT_HARD,
default=servers.REBOOT_SOFT,
help='Perform a hard reboot (instead of a soft one).')
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_reboot(self, args):
"""Reboot a server."""
self._find_server(args.server).reboot(args.reboot_type)
@arg('server', metavar='<server>', help='Name or ID of server.')
@arg('image', metavar='<image>', help="Name or ID of new image.")
def do_rebuild(self, args):
"""Shutdown, re-image, and re-boot a server."""
server = self._find_server(args.server)
image = self._find_image(args.image)
server.rebuild(image)
@arg('server', metavar='<server>', help='Name (old name) or ID of server.')
@arg('name', metavar='<name>', help='New name for the server.')
def do_rename(self, args):
"""Rename a server."""
self._find_server(args.server).update(name=args.name)
@arg('server', metavar='<server>', help='Name or ID of server.')
@arg('flavor', metavar='<flavor>', help="Name or ID of new flavor.")
def do_resize(self, args):
"""Resize a server."""
server = self._find_server(args.server)
flavor = self._find_flavor(args.flavor)
server.resize(flavor)
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_migrate(self, args):
"""Migrate a server."""
self._find_server(args.server).migrate()
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_pause(self, args):
"""Pause a server."""
self._find_server(args.server).pause()
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_unpause(self, args):
"""Unpause a server."""
self._find_server(args.server).unpause()
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_suspend(self, args):
"""Suspend a server."""
self._find_server(args.server).suspend()
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_resume(self, args):
"""Resume a server."""
self._find_server(args.server).resume()
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_rescue(self, args):
"""Rescue a server."""
self._find_server(args.server).rescue()
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_unrescue(self, args):
"""Unrescue a server."""
self._find_server(args.server).unrescue()
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_diagnostics(self, args):
"""Retrieve server diagnostics."""
print_dict(self.cs.servers.diagnostics(args.server)[1])
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_actions(self, args):
"""Retrieve server actions."""
print_list(
self.cs.servers.actions(args.server),
["Created_At", "Action", "Error"])
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_resize_confirm(self, args):
"""Confirm a previous resize."""
self._find_server(args.server).confirm_resize()
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_resize_revert(self, args):
"""Revert a previous resize (and return to the previous VM)."""
self._find_server(args.server).revert_resize()
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_root_password(self, args):
"""
Change the root password for a server.
"""
server = self._find_server(args.server)
p1 = getpass.getpass('New password: ')
p2 = getpass.getpass('Again: ')
if p1 != p2:
raise CommandError("Passwords do not match.")
server.change_password(p1)
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_show(self, args):
"""Show details about the given server."""
s = self._find_server(args.server)
info = s._info.copy()
addresses = info.pop('addresses')
for network_name in addresses.keys():
ips = map(lambda x: x["addr"], addresses[network_name])
info['%s ip' % network_name] = ', '.join(ips)
flavor = info.get('flavor', {})
flavor_id = flavor.get('id')
if flavor_id:
info['flavor'] = self._find_flavor(flavor_id).name
image = info.get('image', {})
image_id = image.get('id')
if image_id:
info['image'] = self._find_image(image_id).name
print_dict(info)
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_delete(self, args):
"""Immediately shut down and delete a server."""
self._find_server(args.server).delete()
def _find_server(self, server):
"""Get a server by name or ID."""
return self._find_resource(self.cs.servers, server)
def _find_image(self, image):
"""Get an image by name or ID."""
return self._find_resource(self.cs.images, image)
def _find_flavor(self, flavor):
"""Get a flavor by name, ID, or RAM size."""
if keyfile:
try:
return self._find_resource(self.cs.flavors, flavor)
except exceptions.NotFound:
return self.cs.flavors.find(ram=flavor)
files['/root/.ssh/authorized_keys2'] = open(keyfile)
except IOError, e:
raise exceptions.CommandError("Can't open '%s': %s" % (keyfile, e))
def _find_resource(self, manager, name_or_id):
"""Helper for the _find_* methods."""
try:
if isinstance(name_or_id, int) or name_or_id.isdigit():
return manager.get(int(name_or_id))
try:
uuid.UUID(name_or_id)
return manager.get(name_or_id)
except ValueError:
return manager.find(name=name_or_id)
except exceptions.NotFound:
raise CommandError("No %s with a name or ID of '%s' exists." %
(manager.resource_class.__name__.lower(), name_or_id))
return (args.name, image, flavor, metadata, files)
# 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 do_flavor_list(cs, args):
"""Print a list of available 'flavors' (sizes of servers)."""
flavors = cs.flavors.list()
_translate_flavor_keys(flavors)
utils.print_list(flavors, [
'ID',
'Name',
'Memory_MB',
'Swap',
'Local_GB',
'VCPUs',
'RXTX_Quota',
'RXTX_Cap'])
def do_image_list(cs, args):
"""Print a list of available images to boot from."""
utils.print_list(cs.images.list(), ['ID', 'Name', 'Status'])
# Helpers
def print_list(objs, fields, formatters={}):
pt = prettytable.PrettyTable([f for f in fields], caching=False)
pt.aligns = ['l' for f in fields]
@utils.arg('image', metavar='<image>', help='Name or ID of image.')
def do_image_delete(cs, args):
"""
Delete an image.
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)
It should go without saying, but you can only delete images you
created.
"""
image = _find_image(cs, args.image)
image.delete()
pt.printt(sortby=fields[0])
@utils.arg('--fixed_ip',
dest='fixed_ip',
metavar='<fixed_ip>',
default=None,
help='Only match against fixed IP.')
@utils.arg('--reservation_id',
dest='reservation_id',
metavar='<reservation_id>',
default=None,
help='Only return instances that match reservation_id.')
@utils.arg('--recurse_zones',
dest='recurse_zones',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Recurse through all zones if set.')
@utils.arg('--ip',
dest='ip',
metavar='<ip_regexp>',
default=None,
help='Search with regular expression match by IP address')
@utils.arg('--ip6',
dest='ip6',
metavar='<ip6_regexp>',
default=None,
help='Search with regular expression match by IPv6 address')
@utils.arg('--server_name',
dest='server_name',
metavar='<name_regexp>',
default=None,
help='Search with regular expression match by server name')
@utils.arg('--name',
dest='display_name',
metavar='<name_regexp>',
default=None,
help='Search with regular expression match by display name')
@utils.arg('--instance_name',
dest='name',
metavar='<name_regexp>',
default=None,
help='Search with regular expression match by instance name')
def do_list(cs, args):
"""List active servers."""
recurse_zones = args.recurse_zones
search_opts = {
'reservation_id': args.reservation_id,
'fixed_ip': args.fixed_ip,
'recurse_zones': recurse_zones,
'ip': args.ip,
'ip6': args.ip6,
'name': args.name,
'server_name': args.server_name,
'display_name': args.display_name}
if recurse_zones:
to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP']
else:
to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP']
utils.print_list(cs.servers.list(search_opts=search_opts),
to_print)
@utils.arg('--hard',
dest='reboot_type',
action='store_const',
const=servers.REBOOT_HARD,
default=servers.REBOOT_SOFT,
help='Perform a hard reboot (instead of a soft one).')
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
def do_reboot(cs, args):
"""Reboot a server."""
_find_server(cs, args.server).reboot(args.reboot_type)
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')
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
@utils.arg('image', metavar='<image>', help="Name or ID of new image.")
def do_rebuild(cs, args):
"""Shutdown, re-image, and re-boot a server."""
server = _find_server(cs, args.server)
image = _find_image(cs, args.image)
server.rebuild(image)
@utils.arg('server', metavar='<server>', help='Name (old name) or ID of server.')
@utils.arg('name', metavar='<name>', help='New name for the server.')
def do_rename(cs, args):
"""Rename a server."""
_find_server(cs, args.server).update(name=args.name)
def main():
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
@utils.arg('flavor', metavar='<flavor>', help="Name or ID of new flavor.")
def do_resize(cs, args):
"""Resize a server."""
server = _find_server(cs, args.server)
flavor = _find_flavor(cs, args.flavor)
server.resize(flavor)
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
def do_resize_confirm(cs, args):
"""Confirm a previous resize."""
_find_server(cs, args.server).confirm_resize()
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
def do_resize_revert(cs, args):
"""Revert a previous resize (and return to the previous VM)."""
_find_server(cs, args.server).revert_resize()
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
def do_root_password(cs, args):
"""
Change the root password for a server.
"""
server = _find_server(cs, args.server)
p1 = getpass.getpass('New password: ')
p2 = getpass.getpass('Again: ')
if p1 != p2:
raise exceptions.CommandError("Passwords do not match.")
server.change_password(p1)
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
@utils.arg('name', metavar='<name>', help='Name of snapshot.')
def do_create_image(cs, args):
"""Create a new image by taking a snapshot of a running server."""
server = _find_server(cs, args.server)
cs.servers.create_image(server, args.name)
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
def do_show(cs, args):
"""Show details about the given server."""
s = _find_server(cs, args.server)
info = s._info.copy()
addresses = info.pop('addresses', [])
for addrtype in addresses:
ips = map(lambda x: x['addr'], addresses[addrtype])
info['%s ip' % addrtype] = ', '.join(ips)
flavor = info.get('flavor', {})
flavor_id = flavor.get('id', '')
info['flavor'] = _find_flavor(cs, flavor_id).name
image = info.get('image', {})
image_id = image.get('id', '')
info['image'] = _find_image(cs, image_id).name
utils.print_dict(info)
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
def do_delete(cs, args):
"""Immediately shut down and delete a server."""
_find_server(cs, args.server).delete()
def _find_server(cs, server):
"""Get a server by name or ID."""
return _find_resource(cs.servers, server)
def _find_image(cs, image):
"""Get an image by name or ID."""
return _find_resource(cs.images, image)
def _find_flavor(cs, flavor):
"""Get a flavor by name, ID, or RAM size."""
try:
OpenStackShell().main(sys.argv[1:])
return _find_resource(cs.flavors, flavor)
except exceptions.NotFound:
return cs.flavors.find(ram=flavor)
def _find_resource(manager, name_or_id):
"""Helper for the _find_* methods."""
try:
if isinstance(name_or_id, int) or name_or_id.isdigit():
return manager.get(int(name_or_id))
try:
uuid.UUID(name_or_id)
return manager.get(name_or_id)
except ValueError:
return manager.find(name=name_or_id)
except exceptions.NotFound:
raise exceptions.CommandError("No %s with a name or ID of '%s' exists." %
(manager.resource_class.__name__.lower(), name_or_id))
except Exception, e:
if httplib2.debuglevel == 1:
raise # dump stack.
else:
print >> sys.stderr, e
sys.exit(1)

66
tests/fakes.py Normal file
View File

@ -0,0 +1,66 @@
"""
A fake server that "responds" to API methods with pre-canned responses.
All of these responses come from the spec, so if for some reason the spec's
wrong the tests might raise AssertionError. I've indicated in comments the places where actual
behavior differs from the spec.
"""
import novaclient.client
def assert_has_keys(dict, required=[], optional=[]):
keys = dict.keys()
for k in required:
assert k in keys
allowed_keys = set(required) | set(optional)
extra_keys = set(keys).difference(set(required + optional))
if extra_keys:
raise AssertionError("found unexpected keys: %s" % list(extra_keys))
class FakeClient(object):
def assert_called(self, method, url, body=None):
"""
Assert than an API method was just called.
"""
expected = (method, url)
called = self.client.callstack[-1][0:2]
assert self.client.callstack, \
"Expected %s %s but no calls were made." % expected
assert expected == called, 'Expected %s %s; got %s %s' % \
(expected + called)
if body is not None:
assert self.client.callstack[-1][2] == body
self.client.callstack = []
def assert_called_anytime(self, method, url, body=None):
"""
Assert than an API method was called anytime in the test.
"""
expected = (method, url)
assert self.client.callstack, \
"Expected %s %s but no calls were made." % expected
found = False
for entry in self.client.callstack:
called = entry[0:2]
if expected == entry[0:2]:
found = True
break
assert found, 'Expected %s %s; got %s' % \
(expected, self.client.callstack)
if body is not None:
assert entry[2] == body
self.client.callstack = []
def authenticate(self):
pass

60
tests/test_base.py Normal file
View File

@ -0,0 +1,60 @@
import mock
from novaclient import base
from novaclient import exceptions
from novaclient.v1_0 import flavors
from tests.v1_0 import fakes
from tests import utils
cs = fakes.FakeClient()
class BaseTest(utils.TestCase):
def test_resource_repr(self):
r = base.Resource(None, dict(foo="bar", baz="spam"))
self.assertEqual(repr(r), "<Resource baz=spam, foo=bar>")
def test_getid(self):
self.assertEqual(base.getid(4), 4)
class TmpObject(object):
id = 4
self.assertEqual(base.getid(TmpObject), 4)
def test_resource_lazy_getattr(self):
f = flavors.Flavor(cs.flavors, {'id': 1})
self.assertEqual(f.name, '256 MB Server')
cs.assert_called('GET', '/flavors/1')
# Missing stuff still fails after a second get
self.assertRaises(AttributeError, getattr, f, 'blahblah')
cs.assert_called('GET', '/flavors/1')
def test_eq(self):
# Two resources of the same type with the same id: equal
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
self.assertEqual(r1, r2)
# Two resoruces of different types: never equal
r1 = base.Resource(None, {'id': 1})
r2 = flavors.Flavor(None, {'id': 1})
self.assertNotEqual(r1, r2)
# Two resources with no ID: equal if their info is equal
r1 = base.Resource(None, {'name': 'joe', 'age': 12})
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
self.assertEqual(r1, r2)
def test_findall_invalid_attribute(self):
# Make sure findall with an invalid attribute doesn't cause errors.
# The following should not raise an exception.
cs.flavors.findall(vegetable='carrot')
# However, find() should raise an error
self.assertRaises(exceptions.NotFound,
cs.flavors.find,
vegetable='carrot')

58
tests/test_http.py Normal file
View File

@ -0,0 +1,58 @@
import httplib2
import mock
from novaclient import client
from tests import utils
fake_response = httplib2.Response({"status": 200})
fake_body = '{"hi": "there"}'
mock_request = mock.Mock(return_value=(fake_response, fake_body))
def get_client():
cl = client.HTTPClient("username", "apikey",
"project_id", "auth_test")
cl.management_url = "http://example.com"
cl.auth_token = "token"
return cl
class ClientTest(utils.TestCase):
def test_get(self):
cl = get_client()
@mock.patch.object(httplib2.Http, "request", mock_request)
@mock.patch('time.time', mock.Mock(return_value=1234))
def test_get_call():
resp, body = cl.get("/hi")
headers={"X-Auth-Token": "token",
"X-Auth-Project-Id": "project_id",
"User-Agent": cl.USER_AGENT,
}
mock_request.assert_called_with("http://example.com/hi?fresh=1234",
"GET", headers=headers)
# Automatic JSON parsing
self.assertEqual(body, {"hi": "there"})
test_get_call()
def test_post(self):
cl = get_client()
@mock.patch.object(httplib2.Http, "request", mock_request)
def test_post_call():
cl.post("/hi", body=[1, 2, 3])
headers={
"X-Auth-Token": "token",
"X-Auth-Project-Id": "project_id",
"Content-Type": "application/json",
"User-Agent": cl.USER_AGENT
}
mock_request.assert_called_with("http://example.com/hi", "POST",
headers=headers, body='[1, 2, 3]')
test_post_call()

39
tests/test_shell.py Normal file
View File

@ -0,0 +1,39 @@
import os
import mock
import httplib2
from novaclient.shell import OpenStackComputeShell
from novaclient import exceptions
from tests import utils
class ShellTest(utils.TestCase):
# Patch os.environ to avoid required auth info.
def setUp(self):
global _old_env
fake_env = {
'NOVA_USERNAME': 'username',
'NOVA_API_KEY': 'password',
'NOVA_PROJECT_ID': 'project_id'
}
_old_env, os.environ = os.environ, fake_env.copy()
# Make a fake shell object, a helping wrapper to call it, and a quick way
# of asserting that certain API calls were made.
global shell, _shell, assert_called, assert_called_anytime
_shell = OpenStackComputeShell()
shell = lambda cmd: _shell.main(cmd.split())
def tearDown(self):
global _old_env
os.environ = _old_env
def test_help_unknown_command(self):
self.assertRaises(exceptions.CommandError, shell, 'help foofoo')
def test_debug(self):
httplib2.debuglevel = 0
shell('--debug help')
assert httplib2.debuglevel == 1

5
tests/utils.py Normal file
View File

@ -0,0 +1,5 @@
import unittest
class TestCase(unittest.TestCase):
pass

View File

@ -1,82 +1,22 @@
"""
A fake server that "responds" to API methods with pre-canned responses.
All of these responses come from the spec, so if for some reason the spec's
wrong the tests might fail. I've indicated in comments the places where actual
behavior differs from the spec.
"""
from __future__ import absolute_import
import urlparse
import urllib
import httplib2
import urllib
import urlparse
from novaclient.v1_0 import Client
from novaclient.v1_0.client import HTTPClient
from .utils import fail, assert_in, assert_not_in, assert_has_keys
from novaclient import client as base_client
from novaclient.v1_0 import client
from tests import fakes
def assert_equal(value_one, value_two):
assert value_one == value_two
class FakeClient(fakes.FakeClient, client.Client):
def __init__(self, *args, **kwargs):
client.Client.__init__(self, 'username', 'apikey',
'project_id', 'auth_url')
self.client = FakeHTTPClient(**kwargs)
class FakeClient(Client):
def __init__(self, username=None, password=None, project_id=None,
auth_url=None):
super(FakeClient, self).__init__('username', 'apikey',
'project_id', 'auth_url')
self.client = FakeHTTPClient()
def assert_called(self, method, url, body=None):
"""
Assert than an API method was just called.
"""
expected = (method, url)
called = self.client.callstack[-1][0:2]
assert self.client.callstack, \
"Expected %s %s but no calls were made." % expected
assert expected == called, 'Expected %s %s; got %s %s' % \
(expected + called)
if body is not None:
assert_equal(self.client.callstack[-1][2], body)
self.client.callstack = []
def assert_called_anytime(self, method, url, body=None):
"""
Assert than an API method was called anytime in the test.
"""
expected = (method, url)
assert self.client.callstack, \
"Expected %s %s but no calls were made." % expected
found = False
for entry in self.client.callstack:
called = entry[0:2]
if expected == entry[0:2]:
found = True
break
assert found, 'Expected %s; got %s' % \
(expected, self.client.callstack)
if body is not None:
assert_equal(entry[2], body)
self.client.callstack = []
def authenticate(self):
pass
class FakeHTTPClient(HTTPClient):
def __init__(self):
class FakeHTTPClient(base_client.HTTPClient):
def __init__(self, **kwargs):
self.username = 'username'
self.apikey = 'apikey'
self.auth_url = 'auth_url'
@ -85,15 +25,15 @@ class FakeHTTPClient(HTTPClient):
def _cs_request(self, url, method, **kwargs):
# Check that certain things are called correctly
if method in ['GET', 'DELETE']:
assert_not_in('body', kwargs)
assert 'body' not in kwargs
elif method in ['PUT', 'POST']:
assert_in('body', kwargs)
assert 'body' in kwargs
# Call the method
munged_url = url.strip('/').replace('/', '_').replace('.', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
fail('Called unknown API method: %s %s' % (method, url))
raise AssertionError('Called unknown API method: %s %s' % (method, url))
# Note the call
self.callstack.append((method, url, kwargs.get('body', None)))
@ -210,14 +150,14 @@ class FakeHTTPClient(HTTPClient):
]})
def post_servers(self, body, **kw):
assert_equal(body.keys(), ['server'])
assert_has_keys(body['server'],
required=['name', 'imageId', 'flavorId'],
optional=['sharedIpGroupId', 'metadata',
'personality', 'min_count', 'max_count'])
assert body.keys() == ['server']
fakes.assert_has_keys(body['server'],
required=['name', 'imageId', 'flavorId'],
optional=['sharedIpGroupId', 'metadata',
'personality', 'min_count', 'max_count'])
if 'personality' in body['server']:
for pfile in body['server']['personality']:
assert_has_keys(pfile, required=['path', 'contents'])
fakes.assert_has_keys(pfile, required=['path', 'contents'])
return (202, self.get_servers_1234()[1])
def get_servers_1234(self, **kw):
@ -229,8 +169,8 @@ class FakeHTTPClient(HTTPClient):
return (200, r)
def put_servers_1234(self, body, **kw):
assert_equal(body.keys(), ['server'])
assert_has_keys(body['server'], optional=['name', 'adminPass'])
assert body.keys() == ['server']
fakes.assert_has_keys(body['server'], optional=['name', 'adminPass'])
return (204, None)
def delete_servers_1234(self, **kw):
@ -253,8 +193,8 @@ class FakeHTTPClient(HTTPClient):
self.get_servers_1234_ips()[1]['addresses']['private']})
def put_servers_1234_ips_public_1_2_3_4(self, body, **kw):
assert_equal(body.keys(), ['shareIp'])
assert_has_keys(body['shareIp'], required=['sharedIpGroupId',
assert body.keys() == ['shareIp']
fakes.assert_has_keys(body['shareIp'], required=['sharedIpGroupId',
'configureServer'])
return (202, None)
@ -266,29 +206,32 @@ class FakeHTTPClient(HTTPClient):
#
def post_servers_1234_action(self, body, **kw):
assert_equal(len(body.keys()), 1)
assert len(body.keys()) == 1
action = body.keys()[0]
if action == 'reboot':
assert_equal(body[action].keys(), ['type'])
assert_in(body[action]['type'], ['HARD', 'SOFT'])
assert body[action].keys() == ['type']
assert body[action]['type'] in ['HARD', 'SOFT']
elif action == 'rebuild':
assert_equal(body[action].keys(), ['imageId'])
assert body[action].keys() == ['imageId']
elif action == 'resize':
assert_equal(body[action].keys(), ['flavorId'])
assert body[action].keys() == ['flavorId']
elif action == 'createBackup':
assert set(body[action].keys()) == \
set(['name', 'rotation', 'backup_type'])
elif action == 'confirmResize':
assert_equal(body[action], None)
assert body[action] is None
# This one method returns a different response code
return (204, None)
elif action == 'revertResize':
assert_equal(body[action], None)
assert body[action] is None
elif action == 'migrate':
assert_equal(body[action], None)
assert body[action] is None
elif action == 'addFixedIp':
assert_equal(body[action].keys(), ['networkId'])
assert body[action].keys() == ['networkId']
elif action == 'removeFixedIp':
assert_equal(body[action].keys(), ['address'])
assert body[action].keys() == ['address']
else:
fail("Unexpected server action: %s" % action)
raise AssertionError("Unexpected server action: %s" % action)
return (202, None)
#
@ -349,8 +292,8 @@ class FakeHTTPClient(HTTPClient):
return (200, {'image': self.get_images_detail()[1]['images'][1]})
def post_images(self, body, **kw):
assert_equal(body.keys(), ['image'])
assert_has_keys(body['image'], required=['serverId', 'name', 'image_type', 'backup_type', 'rotation'])
assert body.keys() == ['image']
fakes.assert_has_keys(body['image'], required=['serverId', 'name'])
return (202, self.get_images_1()[1])
def delete_images_1(self, **kw):
@ -367,8 +310,8 @@ class FakeHTTPClient(HTTPClient):
}})
def post_servers_1234_backup_schedule(self, body, **kw):
assert_equal(body.keys(), ['backupSchedule'])
assert_has_keys(body['backupSchedule'], required=['enabled'],
assert body.keys() == ['backupSchedule']
fakes.assert_has_keys(body['backupSchedule'], required=['enabled'],
optional=['weekly', 'daily'])
return (204, None)
@ -395,8 +338,8 @@ class FakeHTTPClient(HTTPClient):
self.get_shared_ip_groups_detail()[1]['sharedIpGroups'][0]})
def post_shared_ip_groups(self, body, **kw):
assert_equal(body.keys(), ['sharedIpGroup'])
assert_has_keys(body['sharedIpGroup'], required=['name'],
assert body.keys() == ['sharedIpGroup']
fakes.assert_has_keys(body['sharedIpGroup'], required=['name'],
optional=['server'])
return (201, {'sharedIpGroup': {
'id': 10101,
@ -417,7 +360,6 @@ class FakeHTTPClient(HTTPClient):
{'id': 2, 'api_url': 'http://foo.com', 'username': 'alice'},
]})
def get_zones_detail(self, **kw):
return (200, {'zones': [
{'id': 1, 'api_url': 'http://foo.com', 'username': 'bob',
@ -435,16 +377,16 @@ class FakeHTTPClient(HTTPClient):
return (200, r)
def post_zones(self, body, **kw):
assert_equal(body.keys(), ['zone'])
assert_has_keys(body['zone'],
assert body.keys() == ['zone']
fakes.assert_has_keys(body['zone'],
required=['api_url', 'username', 'password'],
optional=['weight_offset', 'weight_scale'])
return (202, self.get_zones_1()[1])
def put_zones_1(self, body, **kw):
assert_equal(body.keys(), ['zone'])
assert_has_keys(body['zone'], optional=['api_url', 'username',
assert body.keys() == ['zone']
fakes.assert_has_keys(body['zone'], optional=['api_url', 'username',
'password',
'weight_offset',
'weight_scale'])
@ -457,12 +399,14 @@ class FakeHTTPClient(HTTPClient):
# Accounts
#
def post_accounts_test_account_create_instance(self, body, **kw):
assert_equal(body.keys(), ['server'])
assert_has_keys(body['server'],
assert body.keys() == ['server']
fakes.assert_has_keys(body['server'],
required=['name', 'imageId', 'flavorId'],
optional=['sharedIpGroupId', 'metadata',
'personality', 'min_count', 'max_count'])
if 'personality' in body['server']:
for pfile in body['server']['personality']:
assert_has_keys(pfile, required=['path', 'contents'])
fakes.assert_has_keys(pfile, required=['path', 'contents'])
return (202, self.get_servers_1234()[1])

View File

@ -1,21 +1,25 @@
from __future__ import absolute_import
import StringIO
from .fakes import FakeClient
from tests.v1_0 import fakes
from tests import utils
os = FakeClient()
def test_instance_creation_for_account():
s = os.accounts.create_instance_for(
account_id='test_account',
name="My server",
image=1,
flavor=1,
meta={'foo': 'bar'},
ipgroup=1,
files={
'/etc/passwd': 'some data', # a file
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
})
os.assert_called('POST', '/accounts/test_account/create_instance')
cs = fakes.FakeClient()
class AccountsTest(utils.TestCase):
def test_instance_creation_for_account(self):
s = cs.accounts.create_instance_for(
account_id='test_account',
name="My server",
image=1,
flavor=1,
meta={'foo': 'bar'},
ipgroup=1,
files={
'/etc/passwd': 'some data', # a file
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
})
cs.assert_called('POST', '/accounts/test_account/create_instance')

View File

@ -1,71 +1,74 @@
import mock
import httplib2
from nose.tools import assert_raises, assert_equal
import mock
import novaclient.v1_0
from novaclient.v1_0 import exceptions
from novaclient.v1_0 import client
from novaclient import exceptions
from tests import utils
def test_authenticate_success():
cs = novaclient.v1_0.Client("username", "apikey", "project_id")
auth_response = httplib2.Response({
'status': 204,
'x-server-management-url':
'https://servers.api.rackspacecloud.com/v1.0/443470',
'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
})
mock_request = mock.Mock(return_value=(auth_response, None))
class AuthenticationTests(utils.TestCase):
@mock.patch.object(httplib2.Http, "request", mock_request)
def test_auth_call():
cs.client.authenticate()
mock_request.assert_called_with(cs.client.auth_url, 'GET',
def test_authenticate_success(self):
cs = client.Client("username", "apikey", "project_id")
management_url = 'https://servers.api.rackspacecloud.com/v1.0/443470'
auth_response = httplib2.Response({
'status': 204,
'x-server-management-url': management_url,
'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
})
mock_request = mock.Mock(return_value=(auth_response, None))
@mock.patch.object(httplib2.Http, "request", mock_request)
def test_auth_call():
cs.client.authenticate()
headers={
'X-Auth-User': 'username',
'X-Auth-Key': 'apikey',
'X-Auth-Project-Id': 'project_id',
'User-Agent': cs.client.USER_AGENT
})
assert_equal(cs.client.management_url,
auth_response['x-server-management-url'])
assert_equal(cs.client.auth_token, auth_response['x-auth-token'])
}
mock_request.assert_called_with(cs.client.auth_url, 'GET',
headers=headers)
self.assertEqual(cs.client.management_url,
auth_response['x-server-management-url'])
self.assertEqual(cs.client.auth_token,
auth_response['x-auth-token'])
test_auth_call()
test_auth_call()
def test_authenticate_failure(self):
cs = client.Client("username", "apikey", "project_id")
auth_response = httplib2.Response({'status': 401})
mock_request = mock.Mock(return_value=(auth_response, None))
def test_authenticate_failure():
cs = novaclient.v1_0.Client("username", "apikey", "project_id")
auth_response = httplib2.Response({'status': 401})
mock_request = mock.Mock(return_value=(auth_response, None))
@mock.patch.object(httplib2.Http, "request", mock_request)
def test_auth_call():
self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
@mock.patch.object(httplib2.Http, "request", mock_request)
def test_auth_call():
assert_raises(exceptions.Unauthorized, cs.client.authenticate)
test_auth_call()
test_auth_call()
def test_auth_automatic(self):
cs = client.Client("username", "apikey", "project_id")
http_client = cs.client
http_client.management_url = ''
mock_request = mock.Mock(return_value=(None, None))
@mock.patch.object(http_client, 'request', mock_request)
@mock.patch.object(http_client, 'authenticate')
def test_auth_call(m):
http_client.get('/')
m.assert_called()
mock_request.assert_called()
def test_auth_automatic():
client = novaclient.v1_0.Client("username", "apikey", "project_id").client
client.management_url = ''
mock_request = mock.Mock(return_value=(None, None))
test_auth_call()
@mock.patch.object(client, 'request', mock_request)
@mock.patch.object(client, 'authenticate')
def test_auth_call(m):
client.get('/')
m.assert_called()
mock_request.assert_called()
def test_auth_manual(self):
cs = client.Client("username", "apikey", "project_id")
test_auth_call()
@mock.patch.object(cs.client, 'authenticate')
def test_auth_call(m):
cs.authenticate()
m.assert_called()
def test_auth_manual():
cs = novaclient.v1_0.Client("username", "apikey", "project_id")
@mock.patch.object(cs.client, 'authenticate')
def test_auth_call(m):
cs.authenticate()
m.assert_called()
test_auth_call()
test_auth_call()

View File

@ -1,60 +1,60 @@
from __future__ import absolute_import
from novaclient.v1_0 import backup_schedules
from .fakes import FakeClient
from .utils import assert_isinstance
os = FakeClient()
from tests.v1_0 import fakes
from tests import utils
def test_get_backup_schedule():
s = os.servers.get(1234)
# access via manager
b = os.backup_schedules.get(server=s)
assert_isinstance(b, backup_schedules.BackupSchedule)
os.assert_called('GET', '/servers/1234/backup_schedule')
b = os.backup_schedules.get(server=1234)
assert_isinstance(b, backup_schedules.BackupSchedule)
os.assert_called('GET', '/servers/1234/backup_schedule')
# access via instance
assert_isinstance(s.backup_schedule, backup_schedules.BackupSchedule)
os.assert_called('GET', '/servers/1234/backup_schedule')
# Just for coverage's sake
b = s.backup_schedule.get()
os.assert_called('GET', '/servers/1234/backup_schedule')
cs = fakes.FakeClient()
def test_create_update_backup_schedule():
s = os.servers.get(1234)
class BackupSchedulesTest(utils.TestCase):
# create/update via manager
os.backup_schedules.update(
server=s,
enabled=True,
weekly=backup_schedules.BACKUP_WEEKLY_THURSDAY,
daily=backup_schedules.BACKUP_DAILY_H_1000_1200
)
os.assert_called('POST', '/servers/1234/backup_schedule')
def test_get_backup_schedule(self):
s = cs.servers.get(1234)
# and via instance
s.backup_schedule.update(enabled=False)
os.assert_called('POST', '/servers/1234/backup_schedule')
# access via manager
b = cs.backup_schedules.get(server=s)
self.assertTrue(isinstance(b, backup_schedules.BackupSchedule))
cs.assert_called('GET', '/servers/1234/backup_schedule')
b = cs.backup_schedules.get(server=1234)
self.assertTrue(isinstance(b, backup_schedules.BackupSchedule))
cs.assert_called('GET', '/servers/1234/backup_schedule')
def test_delete_backup_schedule():
s = os.servers.get(1234)
# access via instance
self.assertTrue(isinstance(s.backup_schedule,
backup_schedules.BackupSchedule))
cs.assert_called('GET', '/servers/1234/backup_schedule')
# delete via manager
os.backup_schedules.delete(s)
os.assert_called('DELETE', '/servers/1234/backup_schedule')
os.backup_schedules.delete(1234)
os.assert_called('DELETE', '/servers/1234/backup_schedule')
# Just for coverage's sake
b = s.backup_schedule.get()
cs.assert_called('GET', '/servers/1234/backup_schedule')
# and via instance
s.backup_schedule.delete()
os.assert_called('DELETE', '/servers/1234/backup_schedule')
def test_create_update_backup_schedule(self):
s = cs.servers.get(1234)
# create/update via manager
cs.backup_schedules.update(
server=s,
enabled=True,
weekly=backup_schedules.BACKUP_WEEKLY_THURSDAY,
daily=backup_schedules.BACKUP_DAILY_H_1000_1200
)
cs.assert_called('POST', '/servers/1234/backup_schedule')
# and via instance
s.backup_schedule.update(enabled=False)
cs.assert_called('POST', '/servers/1234/backup_schedule')
def test_delete_backup_schedule(self):
s = cs.servers.get(1234)
# delete via manager
cs.backup_schedules.delete(s)
cs.assert_called('DELETE', '/servers/1234/backup_schedule')
cs.backup_schedules.delete(1234)
cs.assert_called('DELETE', '/servers/1234/backup_schedule')
# and via instance
s.backup_schedule.delete()
cs.assert_called('DELETE', '/servers/1234/backup_schedule')

View File

@ -1,61 +0,0 @@
from __future__ import absolute_import
import mock
from nose.tools import assert_equal, assert_not_equal, assert_raises
from novaclient.v1_0 import flavors
from novaclient.v1_0 import exceptions
from novaclient.v1_0 import base
from .fakes import FakeClient
os = FakeClient()
def test_resource_repr():
r = base.Resource(None, dict(foo="bar", baz="spam"))
assert_equal(repr(r), "<Resource baz=spam, foo=bar>")
def test_getid():
assert_equal(base.getid(4), 4)
class O(object):
id = 4
assert_equal(base.getid(O), 4)
def test_resource_lazy_getattr():
f = flavors.Flavor(os.flavors, {'id': 1})
assert_equal(f.name, '256 MB Server')
os.assert_called('GET', '/flavors/1')
# Missing stuff still fails after a second get
assert_raises(AttributeError, getattr, f, 'blahblah')
os.assert_called('GET', '/flavors/1')
def test_eq():
# Two resources of the same type with the same id: equal
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
assert_equal(r1, r2)
# Two resoruces of different types: never equal
r1 = base.Resource(None, {'id': 1})
r2 = flavors.Flavor(None, {'id': 1})
assert_not_equal(r1, r2)
# Two resources with no ID: equal if their info is equal
r1 = base.Resource(None, {'name': 'joe', 'age': 12})
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
assert_equal(r1, r2)
def test_findall_invalid_attribute():
# Make sure findall with an invalid attribute doesn't cause errors.
# The following should not raise an exception.
os.flavors.findall(vegetable='carrot')
# However, find() should raise an error
assert_raises(exceptions.NotFound, os.flavors.find, vegetable='carrot')

View File

@ -1,52 +0,0 @@
import mock
import httplib2
from novaclient.v1_0 import client
from nose.tools import assert_equal
fake_response = httplib2.Response({"status": 200})
fake_body = '{"hi": "there"}'
mock_request = mock.Mock(return_value=(fake_response, fake_body))
def get_client():
cl = client.HTTPClient("username", "apikey", "project_id", "auth_test")
cl.management_url = "http://example.com"
cl.auth_token = "token"
return cl
def test_get():
cl = get_client()
@mock.patch.object(httplib2.Http, "request", mock_request)
@mock.patch('time.time', mock.Mock(return_value=1234))
def test_get_call():
resp, body = cl.get("/hi")
mock_request.assert_called_with("http://example.com/hi?fresh=1234",
"GET",
headers={"X-Auth-Token": "token",
"X-Auth-Project-Id": "project_id",
"User-Agent": cl.USER_AGENT})
# Automatic JSON parsing
assert_equal(body, {"hi": "there"})
test_get_call()
def test_post():
cl = get_client()
@mock.patch.object(httplib2.Http, "request", mock_request)
def test_post_call():
cl.post("/hi", body=[1, 2, 3])
mock_request.assert_called_with("http://example.com/hi", "POST",
headers={
"X-Auth-Token": "token",
"X-Auth-Project-Id": "project_id",
"Content-Type": "application/json",
"User-Agent": cl.USER_AGENT},
body='[1, 2, 3]'
)
test_post_call()

View File

@ -1,42 +1,38 @@
from __future__ import absolute_import
from nose.tools import assert_raises, assert_equal
from novaclient import exceptions
from novaclient.v1_0 import flavors
from novaclient.v1_0 import exceptions
from .fakes import FakeClient
from .utils import assert_isinstance
os = FakeClient()
from tests.v1_0 import fakes
from tests import utils
def test_list_flavors():
fl = os.flavors.list()
os.assert_called('GET', '/flavors/detail')
[assert_isinstance(f, flavors.Flavor) for f in fl]
cs = fakes.FakeClient()
def test_list_flavors_undetailed():
fl = os.flavors.list(detailed=False)
os.assert_called('GET', '/flavors')
[assert_isinstance(f, flavors.Flavor) for f in fl]
class FlavorsTest(utils.TestCase):
def test_list_flavors(self):
fl = cs.flavors.list()
cs.assert_called('GET', '/flavors/detail')
[self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl]
def test_get_flavor_details():
f = os.flavors.get(1)
os.assert_called('GET', '/flavors/1')
assert_isinstance(f, flavors.Flavor)
assert_equal(f.ram, 256)
assert_equal(f.disk, 10)
def test_list_flavors_undetailed(self):
fl = cs.flavors.list(detailed=False)
cs.assert_called('GET', '/flavors')
[self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl]
def test_get_flavor_details(self):
f = cs.flavors.get(1)
cs.assert_called('GET', '/flavors/1')
self.assertTrue(isinstance(f, flavors.Flavor))
self.assertEqual(f.ram, 256)
self.assertEqual(f.disk, 10)
def test_find():
f = os.flavors.find(ram=256)
os.assert_called('GET', '/flavors/detail')
assert_equal(f.name, '256 MB Server')
def test_find(self):
f = cs.flavors.find(ram=256)
cs.assert_called('GET', '/flavors/detail')
self.assertEqual(f.name, '256 MB Server')
f = os.flavors.find(disk=20)
assert_equal(f.name, '512 MB Server')
f = cs.flavors.find(disk=20)
self.assertEqual(f.name, '512 MB Server')
assert_raises(exceptions.NotFound, os.flavors.find, disk=12345)
self.assertRaises(exceptions.NotFound, cs.flavors.find, disk=12345)

View File

@ -1,51 +1,45 @@
from __future__ import absolute_import
from nose.tools import assert_equal
from novaclient.v1_0 import images
from .fakes import FakeClient
from .utils import assert_isinstance
os = FakeClient()
from tests.v1_0 import fakes
from tests import utils
def test_list_images():
il = os.images.list()
os.assert_called('GET', '/images/detail')
[assert_isinstance(i, images.Image) for i in il]
cs = fakes.FakeClient()
def test_list_images_undetailed():
il = os.images.list(detailed=False)
os.assert_called('GET', '/images')
[assert_isinstance(i, images.Image) for i in il]
class ImagesTest(utils.TestCase):
def test_list_images(self):
il = cs.images.list()
cs.assert_called('GET', '/images/detail')
[self.assertTrue(isinstance(i, images.Image)) for i in il]
def test_get_image_details():
i = os.images.get(1)
os.assert_called('GET', '/images/1')
assert_isinstance(i, images.Image)
assert_equal(i.id, 1)
assert_equal(i.name, 'CentOS 5.2')
def test_list_images_undetailed(self):
il = cs.images.list(detailed=False)
cs.assert_called('GET', '/images')
[self.assertTrue(isinstance(i, images.Image)) for i in il]
def test_get_image_details(self):
i = cs.images.get(1)
cs.assert_called('GET', '/images/1')
self.assertTrue(isinstance(i, images.Image))
self.assertEqual(i.id, 1)
self.assertEqual(i.name, 'CentOS 5.2')
def test_create_image():
i = os.images.create(server=1234, name="Just in case")
os.assert_called('POST', '/images')
assert_isinstance(i, images.Image)
def test_create_image(self):
i = cs.images.create(server=1234, name="Just in case")
cs.assert_called('POST', '/images')
self.assertTrue(isinstance(i, images.Image))
def test_delete_image(self):
cs.images.delete(1)
cs.assert_called('DELETE', '/images/1')
def test_delete_image():
os.images.delete(1)
os.assert_called('DELETE', '/images/1')
def test_find(self):
i = cs.images.find(name="CentOS 5.2")
self.assertEqual(i.id, 1)
cs.assert_called('GET', '/images/detail')
def test_find():
i = os.images.find(name="CentOS 5.2")
assert_equal(i.id, 1)
os.assert_called('GET', '/images/detail')
iml = os.images.findall(status='SAVING')
assert_equal(len(iml), 1)
assert_equal(iml[0].name, 'My Server Backup')
iml = cs.images.findall(status='SAVING')
self.assertEqual(len(iml), 1)
self.assertEqual(iml[0].name, 'My Server Backup')

View File

@ -1,52 +1,48 @@
from __future__ import absolute_import
from nose.tools import assert_equal
from novaclient.v1_0 import ipgroups
from .fakes import FakeClient
from .utils import assert_isinstance
os = FakeClient()
from tests.v1_0 import fakes
from tests import utils
def test_list_ipgroups():
ipl = os.ipgroups.list()
os.assert_called('GET', '/shared_ip_groups/detail')
[assert_isinstance(ipg, ipgroups.IPGroup) for ipg in ipl]
cs = fakes.FakeClient()
def test_list_ipgroups_undetailed():
ipl = os.ipgroups.list(detailed=False)
os.assert_called('GET', '/shared_ip_groups')
[assert_isinstance(ipg, ipgroups.IPGroup) for ipg in ipl]
class IPGroupTest(utils.TestCase):
def test_list_ipgroups(self):
ipl = cs.ipgroups.list()
cs.assert_called('GET', '/shared_ip_groups/detail')
[self.assertTrue(isinstance(ipg, ipgroups.IPGroup)) \
for ipg in ipl]
def test_get_ipgroup():
ipg = os.ipgroups.get(1)
os.assert_called('GET', '/shared_ip_groups/1')
assert_isinstance(ipg, ipgroups.IPGroup)
def test_list_ipgroups_undetailed(self):
ipl = cs.ipgroups.list(detailed=False)
cs.assert_called('GET', '/shared_ip_groups')
[self.assertTrue(isinstance(ipg, ipgroups.IPGroup)) \
for ipg in ipl]
def test_get_ipgroup(self):
ipg = cs.ipgroups.get(1)
cs.assert_called('GET', '/shared_ip_groups/1')
self.assertTrue(isinstance(ipg, ipgroups.IPGroup))
def test_create_ipgroup():
ipg = os.ipgroups.create("My group", 1234)
os.assert_called('POST', '/shared_ip_groups')
assert_isinstance(ipg, ipgroups.IPGroup)
def test_create_ipgroup(self):
ipg = cs.ipgroups.create("My group", 1234)
cs.assert_called('POST', '/shared_ip_groups')
self.assertTrue(isinstance(ipg, ipgroups.IPGroup))
def test_delete_ipgroup(self):
ipg = cs.ipgroups.get(1)
ipg.delete()
cs.assert_called('DELETE', '/shared_ip_groups/1')
cs.ipgroups.delete(ipg)
cs.assert_called('DELETE', '/shared_ip_groups/1')
cs.ipgroups.delete(1)
cs.assert_called('DELETE', '/shared_ip_groups/1')
def test_delete_ipgroup():
ipg = os.ipgroups.get(1)
ipg.delete()
os.assert_called('DELETE', '/shared_ip_groups/1')
os.ipgroups.delete(ipg)
os.assert_called('DELETE', '/shared_ip_groups/1')
os.ipgroups.delete(1)
os.assert_called('DELETE', '/shared_ip_groups/1')
def test_find():
ipg = os.ipgroups.find(name='group1')
os.assert_called('GET', '/shared_ip_groups/detail')
assert_equal(ipg.name, 'group1')
ipgl = os.ipgroups.findall(id=1)
assert_equal(ipgl, [ipgroups.IPGroup(None, {'id': 1})])
def test_find(self):
ipg = cs.ipgroups.find(name='group1')
cs.assert_called('GET', '/shared_ip_groups/detail')
self.assertEqual(ipg.name, 'group1')
ipgl = cs.ipgroups.findall(id=1)
self.assertEqual(ipgl, [ipgroups.IPGroup(None, {'id': 1})])

View File

@ -1,180 +1,169 @@
from __future__ import absolute_import
import StringIO
from nose.tools import assert_equal
from novaclient.v1_0 import servers
from .fakes import FakeClient
from .utils import assert_isinstance
from tests.v1_0 import fakes
from tests import utils
os = FakeClient()
cs = fakes.FakeClient()
def test_list_servers():
sl = os.servers.list()
os.assert_called('GET', '/servers/detail')
[assert_isinstance(s, servers.Server) for s in sl]
class ServersTest(utils.TestCase):
def test_list_servers(self):
sl = cs.servers.list()
cs.assert_called('GET', '/servers/detail')
[self.assertTrue(isinstance(s, servers.Server)) for s in sl]
def test_list_servers_undetailed():
sl = os.servers.list(detailed=False)
os.assert_called('GET', '/servers')
[assert_isinstance(s, servers.Server) for s in sl]
def test_list_servers_undetailed(self):
sl = cs.servers.list(detailed=False)
cs.assert_called('GET', '/servers')
[self.assertTrue(isinstance(s, servers.Server)) for s in sl]
def test_get_server_details(self):
s = cs.servers.get(1234)
cs.assert_called('GET', '/servers/1234')
self.assertTrue(isinstance(s, servers.Server))
self.assertEqual(s.id, 1234)
self.assertEqual(s.status, 'BUILD')
def test_get_server_details():
s = os.servers.get(1234)
os.assert_called('GET', '/servers/1234')
assert_isinstance(s, servers.Server)
assert_equal(s.id, 1234)
assert_equal(s.status, 'BUILD')
def test_create_server(self):
s = cs.servers.create(
name="My server",
image=1,
flavor=1,
meta={'foo': 'bar'},
ipgroup=1,
files={
'/etc/passwd': 'some data', # a file
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
}
)
cs.assert_called('POST', '/servers')
self.assertTrue(isinstance(s, servers.Server))
def test_update_server(self):
s = cs.servers.get(1234)
def test_create_server():
s = os.servers.create(
name="My server",
image=1,
flavor=1,
meta={'foo': 'bar'},
ipgroup=1,
files={
'/etc/passwd': 'some data', # a file
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
}
)
os.assert_called('POST', '/servers')
assert_isinstance(s, servers.Server)
# Update via instance
s.update(name='hi')
cs.assert_called('PUT', '/servers/1234')
s.update(name='hi', password='there')
cs.assert_called('PUT', '/servers/1234')
# Silly, but not an error
s.update()
def test_update_server():
s = os.servers.get(1234)
# Update via manager
cs.servers.update(s, name='hi')
cs.assert_called('PUT', '/servers/1234')
cs.servers.update(1234, password='there')
cs.assert_called('PUT', '/servers/1234')
cs.servers.update(s, name='hi', password='there')
cs.assert_called('PUT', '/servers/1234')
# Update via instance
s.update(name='hi')
os.assert_called('PUT', '/servers/1234')
s.update(name='hi', password='there')
os.assert_called('PUT', '/servers/1234')
def test_delete_server(self):
s = cs.servers.get(1234)
s.delete()
cs.assert_called('DELETE', '/servers/1234')
cs.servers.delete(1234)
cs.assert_called('DELETE', '/servers/1234')
cs.servers.delete(s)
cs.assert_called('DELETE', '/servers/1234')
# Silly, but not an error
s.update()
def test_find(self):
s = cs.servers.find(name='sample-server')
cs.assert_called('GET', '/servers/detail')
self.assertEqual(s.name, 'sample-server')
# Update via manager
os.servers.update(s, name='hi')
os.assert_called('PUT', '/servers/1234')
os.servers.update(1234, password='there')
os.assert_called('PUT', '/servers/1234')
os.servers.update(s, name='hi', password='there')
os.assert_called('PUT', '/servers/1234')
# Find with multiple results arbitraility returns the first item
s = cs.servers.find(flavorId=1)
sl = cs.servers.findall(flavorId=1)
self.assertEqual(sl[0], s)
self.assertEqual([s.id for s in sl], [1234, 5678])
def test_share_ip(self):
s = cs.servers.get(1234)
def test_delete_server():
s = os.servers.get(1234)
s.delete()
os.assert_called('DELETE', '/servers/1234')
os.servers.delete(1234)
os.assert_called('DELETE', '/servers/1234')
os.servers.delete(s)
os.assert_called('DELETE', '/servers/1234')
# Share via instance
s.share_ip(ipgroup=1, address='1.2.3.4')
cs.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4')
# Share via manager
cs.servers.share_ip(s, ipgroup=1, address='1.2.3.4', configure=False)
cs.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4')
def test_find():
s = os.servers.find(name='sample-server')
os.assert_called('GET', '/servers/detail')
assert_equal(s.name, 'sample-server')
def test_unshare_ip(self):
s = cs.servers.get(1234)
# Find with multiple results arbitraility returns the first item
s = os.servers.find(flavorId=1)
sl = os.servers.findall(flavorId=1)
assert_equal(sl[0], s)
assert_equal([s.id for s in sl], [1234, 5678])
# Unshare via instance
s.unshare_ip('1.2.3.4')
cs.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
# Unshare via manager
cs.servers.unshare_ip(s, '1.2.3.4')
cs.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
def test_share_ip():
s = os.servers.get(1234)
def test_reboot_server(self):
s = cs.servers.get(1234)
s.reboot()
cs.assert_called('POST', '/servers/1234/action')
cs.servers.reboot(s, type='HARD')
cs.assert_called('POST', '/servers/1234/action')
# Share via instance
s.share_ip(ipgroup=1, address='1.2.3.4')
os.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4')
def test_rebuild_server(self):
s = cs.servers.get(1234)
s.rebuild(image=1)
cs.assert_called('POST', '/servers/1234/action')
cs.servers.rebuild(s, image=1)
cs.assert_called('POST', '/servers/1234/action')
# Share via manager
os.servers.share_ip(s, ipgroup=1, address='1.2.3.4', configure=False)
os.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4')
def test_resize_server(self):
s = cs.servers.get(1234)
s.resize(flavor=1)
cs.assert_called('POST', '/servers/1234/action')
cs.servers.resize(s, flavor=1)
cs.assert_called('POST', '/servers/1234/action')
def test_confirm_resized_server(self):
s = cs.servers.get(1234)
s.confirm_resize()
cs.assert_called('POST', '/servers/1234/action')
cs.servers.confirm_resize(s)
cs.assert_called('POST', '/servers/1234/action')
def test_unshare_ip():
s = os.servers.get(1234)
def test_revert_resized_server(self):
s = cs.servers.get(1234)
s.revert_resize()
cs.assert_called('POST', '/servers/1234/action')
cs.servers.revert_resize(s)
cs.assert_called('POST', '/servers/1234/action')
# Unshare via instance
s.unshare_ip('1.2.3.4')
os.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
def test_backup_server(self):
s = cs.servers.get(1234)
s.backup("ImageName", "daily", 10)
cs.assert_called('POST', '/servers/1234/action')
cs.servers.backup(s, "ImageName", "daily", 10)
cs.assert_called('POST', '/servers/1234/action')
# Unshare via manager
os.servers.unshare_ip(s, '1.2.3.4')
os.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
def test_migrate_server(self):
s = cs.servers.get(1234)
s.migrate()
cs.assert_called('POST', '/servers/1234/action')
cs.servers.migrate(s)
cs.assert_called('POST', '/servers/1234/action')
def test_add_fixed_ip(self):
s = cs.servers.get(1234)
s.add_fixed_ip(1)
cs.assert_called('POST', '/servers/1234/action')
cs.servers.add_fixed_ip(s, 1)
cs.assert_called('POST', '/servers/1234/action')
def test_reboot_server():
s = os.servers.get(1234)
s.reboot()
os.assert_called('POST', '/servers/1234/action')
os.servers.reboot(s, type='HARD')
os.assert_called('POST', '/servers/1234/action')
def test_rebuild_server():
s = os.servers.get(1234)
s.rebuild(image=1)
os.assert_called('POST', '/servers/1234/action')
os.servers.rebuild(s, image=1)
os.assert_called('POST', '/servers/1234/action')
def test_resize_server():
s = os.servers.get(1234)
s.resize(flavor=1)
os.assert_called('POST', '/servers/1234/action')
os.servers.resize(s, flavor=1)
os.assert_called('POST', '/servers/1234/action')
def test_confirm_resized_server():
s = os.servers.get(1234)
s.confirm_resize()
os.assert_called('POST', '/servers/1234/action')
os.servers.confirm_resize(s)
os.assert_called('POST', '/servers/1234/action')
def test_revert_resized_server():
s = os.servers.get(1234)
s.revert_resize()
os.assert_called('POST', '/servers/1234/action')
os.servers.revert_resize(s)
os.assert_called('POST', '/servers/1234/action')
def test_migrate_server():
s = os.servers.get(1234)
s.migrate()
os.assert_called('POST', '/servers/1234/action')
os.servers.migrate(s)
os.assert_called('POST', '/servers/1234/action')
def test_add_fixed_ip():
s = os.servers.get(1234)
s.add_fixed_ip(1)
os.assert_called('POST', '/servers/1234/action')
os.servers.add_fixed_ip(s, 1)
os.assert_called('POST', '/servers/1234/action')
def test_remove_fixed_ip():
s = os.servers.get(1234)
s.remove_fixed_ip('10.0.0.1')
os.assert_called('POST', '/servers/1234/action')
os.servers.remove_fixed_ip(s, '10.0.0.1')
os.assert_called('POST', '/servers/1234/action')
def test_remove_fixed_ip(self):
s = cs.servers.get(1234)
s.remove_fixed_ip('10.0.0.1')
cs.assert_called('POST', '/servers/1234/action')
cs.servers.remove_fixed_ip(s, '10.0.0.1')
cs.assert_called('POST', '/servers/1234/action')

View File

@ -1,374 +1,319 @@
from __future__ import absolute_import
import os
import mock
import httplib2
from nose.tools import assert_raises, assert_equal
from novaclient.v1_0.shell import OpenStackShell, CommandError
from .fakes import FakeClient
from .utils import assert_in
from novaclient.shell import OpenStackComputeShell
from novaclient import exceptions
from tests.v1_0 import fakes
from tests import utils
# Patch os.environ to avoid required auth info.
def setup():
global _old_env
fake_env = {
'NOVA_USERNAME': 'username',
'NOVA_API_KEY': 'password',
'NOVA_PROJECT_ID': 'project_id'
}
_old_env, os.environ = os.environ, fake_env.copy()
class ShellTest(utils.TestCase):
# Make a fake shell object, a helping wrapper to call it, and a quick way
# of asserting that certain API calls were made.
global shell, _shell, assert_called, assert_called_anytime
_shell = OpenStackShell()
_shell._api_class = FakeClient
assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b)
assert_called_anytime = lambda m, u, b=None: \
_shell.cs.assert_called_anytime(m, u, b)
shell = lambda cmd: _shell.main(cmd.split())
def teardown():
global _old_env
os.environ = _old_env
def test_backup_schedule():
shell('backup-schedule 1234')
assert_called('GET', '/servers/1234/backup_schedule')
shell('backup-schedule sample-server --weekly monday')
assert_called(
'POST', '/servers/1234/backup_schedule',
{'backupSchedule': {'enabled': True, 'daily': 'DISABLED',
'weekly': 'MONDAY'}}
)
shell('backup-schedule sample-server '
'--weekly disabled --daily h_0000_0200')
assert_called(
'POST', '/servers/1234/backup_schedule',
{'backupSchedule': {'enabled': True, 'daily': 'H_0000_0200',
'weekly': 'DISABLED'}}
)
shell('backup-schedule sample-server --disable')
assert_called(
'POST', '/servers/1234/backup_schedule',
{'backupSchedule': {'enabled': False, 'daily': 'DISABLED',
'weekly': 'DISABLED'}}
)
def test_backup_schedule_delete():
shell('backup-schedule-delete 1234')
assert_called('DELETE', '/servers/1234/backup_schedule')
def test_boot():
shell('boot --image 1 some-server')
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'min_count': 1, 'max_count': 1}}
)
shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'min_count': 1, 'max_count': 1,
'metadata': {'foo': 'bar', 'spam': 'eggs'}}}
)
def test_boot_files():
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
expected_file_data = open(testfile).read().encode('base64')
shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' %
(testfile, testfile))
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'min_count': 1, 'max_count': 1,
'personality': [
{'path': '/tmp/bar', 'contents': expected_file_data},
{'path': '/tmp/foo', 'contents': expected_file_data}
]}
# Patch os.environ to avoid required auth info.
def setUp(self):
global _old_env
fake_env = {
'NOVA_USERNAME': 'username',
'NOVA_API_KEY': 'password',
'NOVA_PROJECT_ID': 'project_id'
}
)
_old_env, os.environ = os.environ, fake_env.copy()
# Make a fake shell object, a helping wrapper to call it, and a quick way
# of asserting that certain API calls were made.
global shell, _shell, assert_called, assert_called_anytime
_shell = OpenStackComputeShell()
_shell._api_class = fakes.FakeClient
assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b)
assert_called_anytime = lambda m, u, b=None: \
_shell.cs.assert_called_anytime(m, u, b)
def test_boot_invalid_file():
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
assert_raises(CommandError, shell, 'boot some-server --image 1 '
'--file /foo=%s' % invalid_file)
def shell(cmd):
command = ['--version=1.0',]
command.extend(cmd.split())
_shell.main(command)
def tearDown(self):
global _old_env
os.environ = _old_env
def test_boot_key_auto():
mock_exists = mock.Mock(return_value=True)
mock_open = mock.Mock()
mock_open.return_value = mock.Mock()
mock_open.return_value.read = mock.Mock(return_value='SSHKEY')
def test_backup_schedule(self):
shell('backup-schedule 1234')
assert_called('GET', '/servers/1234/backup_schedule')
@mock.patch('os.path.exists', mock_exists)
@mock.patch('__builtin__.open', mock_open)
def test_shell_call():
shell('boot some-server --image 1 --key')
shell('backup-schedule sample-server --weekly monday')
assert_called(
'POST', '/servers/1234/backup_schedule',
{'backupSchedule': {'enabled': True, 'daily': 'DISABLED',
'weekly': 'MONDAY'}}
)
shell('backup-schedule sample-server '
'--weekly disabled --daily h_0000_0200')
assert_called(
'POST', '/servers/1234/backup_schedule',
{'backupSchedule': {'enabled': True, 'daily': 'H_0000_0200',
'weekly': 'DISABLED'}}
)
shell('backup-schedule sample-server --disable')
assert_called(
'POST', '/servers/1234/backup_schedule',
{'backupSchedule': {'enabled': False, 'daily': 'DISABLED',
'weekly': 'DISABLED'}}
)
def test_backup_schedule_delete(self):
shell('backup-schedule-delete 1234')
assert_called('DELETE', '/servers/1234/backup_schedule')
def test_boot(self):
shell('boot --image 1 some-server')
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'min_count': 1, 'max_count': 1}}
)
shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'min_count': 1, 'max_count': 1,
'personality': [{
'path': '/root/.ssh/authorized_keys2',
'contents': ('SSHKEY').encode('base64')},
'metadata': {'foo': 'bar', 'spam': 'eggs'}}}
)
def test_boot_files(self):
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
expected_file_data = open(testfile).read().encode('base64')
shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' %
(testfile, testfile))
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'min_count': 1, 'max_count': 1,
'personality': [
{'path': '/tmp/bar', 'contents': expected_file_data},
{'path': '/tmp/foo', 'contents': expected_file_data}
]}
}
)
test_shell_call()
def test_boot_key_auto_no_keys():
mock_exists = mock.Mock(return_value=False)
@mock.patch('os.path.exists', mock_exists)
def test_shell_call():
assert_raises(CommandError, shell, 'boot some-server --image 1 --key')
test_shell_call()
def test_boot_key_file():
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
expected_file_data = open(testfile).read().encode('base64')
shell('boot some-server --image 1 --key %s' % testfile)
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'min_count': 1, 'max_count': 1,
'personality': [
{'path': '/root/.ssh/authorized_keys2', 'contents':
expected_file_data},
]}
}
)
def test_boot_invalid_keyfile():
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
assert_raises(CommandError, shell, 'boot some-server '
'--image 1 --key %s' % invalid_file)
def test_boot_ipgroup():
shell('boot --image 1 --ipgroup 1 some-server')
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}}
)
def test_boot_ipgroup_name():
shell('boot --image 1 --ipgroup group1 some-server')
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}}
)
def test_flavor_list():
shell('flavor-list')
assert_called_anytime('GET', '/flavors/detail')
def test_image_list():
shell('image-list')
assert_called('GET', '/images/detail')
def test_snapshot_create():
shell('image-create sample-server mysnapshot')
assert_called(
'POST', '/images',
{'image': {'name': 'mysnapshot', 'serverId': 1234, 'image_type': 'snapshot', 'backup_type': None, 'rotation': None}}
)
def test_backup_create():
shell('image-create sample-server mybackup --image-type backup --backup-type daily --rotation 1')
assert_called(
'POST', '/images',
{'image': {'name': 'mybackup', 'serverId': 1234, 'image_type': 'backup', 'backup_type': 'daily', 'rotation': 1}}
)
def test_image_delete():
shell('image-delete 1')
assert_called('DELETE', '/images/1')
def test_ip_share():
shell('ip-share sample-server 1 1.2.3.4')
assert_called(
'PUT', '/servers/1234/ips/public/1.2.3.4',
{'shareIp': {'sharedIpGroupId': 1, 'configureServer': True}}
)
def test_ip_unshare():
shell('ip-unshare sample-server 1.2.3.4')
assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
def test_ipgroup_list():
shell('ipgroup-list')
assert_in(('GET', '/shared_ip_groups/detail', None),
_shell.cs.client.callstack)
assert_called('GET', '/servers/5678')
def test_ipgroup_show():
shell('ipgroup-show 1')
assert_called('GET', '/shared_ip_groups/1')
shell('ipgroup-show group2')
# does a search, not a direct GET
assert_called('GET', '/shared_ip_groups/detail')
def test_ipgroup_create():
shell('ipgroup-create a-group')
assert_called(
'POST', '/shared_ip_groups',
{'sharedIpGroup': {'name': 'a-group'}}
)
shell('ipgroup-create a-group sample-server')
assert_called(
'POST', '/shared_ip_groups',
{'sharedIpGroup': {'name': 'a-group', 'server': 1234}}
)
def test_ipgroup_delete():
shell('ipgroup-delete group1')
assert_called('DELETE', '/shared_ip_groups/1')
def test_list():
shell('list')
assert_called('GET', '/servers/detail')
def test_reboot():
shell('reboot sample-server')
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
shell('reboot sample-server --hard')
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
def test_rebuild():
shell('rebuild sample-server 1')
assert_called('POST', '/servers/1234/action', {'rebuild': {'imageId': 1}})
def test_rename():
shell('rename sample-server newname')
assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
def test_resize():
shell('resize sample-server 1')
assert_called('POST', '/servers/1234/action', {'resize': {'flavorId': 1}})
def test_resize_confirm():
shell('resize-confirm sample-server')
assert_called('POST', '/servers/1234/action', {'confirmResize': None})
def test_resize_revert():
shell('resize-revert sample-server')
assert_called('POST', '/servers/1234/action', {'revertResize': None})
@mock.patch('getpass.getpass', mock.Mock(return_value='p'))
def test_root_password():
shell('root-password sample-server')
assert_called('PUT', '/servers/1234', {'server': {'adminPass': 'p'}})
def test_show():
shell('show 1234')
# XXX need a way to test multiple calls
# assert_called('GET', '/servers/1234')
assert_called('GET', '/images/2')
def test_delete():
shell('delete 1234')
assert_called('DELETE', '/servers/1234')
shell('delete sample-server')
assert_called('DELETE', '/servers/1234')
def test_zone():
shell('zone 1')
assert_called('GET', '/zones/1')
shell('zone 1 --api_url=http://zzz --zone_username=frank --password=xxx')
assert_called(
'PUT', '/zones/1',
{'zone': {'api_url': 'http://zzz', 'username': 'frank',
'password': 'xxx'}}
)
def test_zone_add():
shell('zone-add http://zzz frank xxx 0.0 1.0')
assert_called(
'POST', '/zones',
{'zone': {'api_url': 'http://zzz', 'username': 'frank',
'password': 'xxx',
'weight_offset': '0.0', 'weight_scale': '1.0'}}
)
def test_zone_delete():
shell('zone-delete 1')
assert_called('DELETE', '/zones/1')
def test_zone_list():
shell('zone-list')
assert_in(('GET', '/zones/detail', None),
_shell.cs.client.callstack)
def test_help():
@mock.patch.object(_shell.parser, 'print_help')
def test_help(m):
shell('help')
m.assert_called()
@mock.patch.object(_shell.subcommands['delete'], 'print_help')
def test_help_delete(m):
shell('help delete')
m.assert_called()
test_help()
test_help_delete()
assert_raises(CommandError, shell, 'help foofoo')
def test_debug():
httplib2.debuglevel = 0
shell('--debug list')
assert httplib2.debuglevel == 1
def test_boot_invalid_file(self):
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
self.assertRaises(exceptions.CommandError, shell, 'boot some-server --image 1 '
'--file /foo=%s' % invalid_file)
def test_boot_key_auto(self):
mock_exists = mock.Mock(return_value=True)
mock_open = mock.Mock()
mock_open.return_value = mock.Mock()
mock_open.return_value.read = mock.Mock(return_value='SSHKEY')
@mock.patch('os.path.exists', mock_exists)
@mock.patch('__builtin__.open', mock_open)
def test_shell_call():
shell('boot some-server --image 1 --key')
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'min_count': 1, 'max_count': 1,
'personality': [{
'path': '/root/.ssh/authorized_keys2',
'contents': ('SSHKEY').encode('base64')},
]}
}
)
test_shell_call()
def test_boot_key_auto_no_keys(self):
mock_exists = mock.Mock(return_value=False)
@mock.patch('os.path.exists', mock_exists)
def test_shell_call():
self.assertRaises(exceptions.CommandError, shell,
'boot some-server --image 1 --key')
test_shell_call()
def test_boot_key_file(self):
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
expected_file_data = open(testfile).read().encode('base64')
shell('boot some-server --image 1 --key %s' % testfile)
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'min_count': 1, 'max_count': 1,
'personality': [
{'path': '/root/.ssh/authorized_keys2', 'contents':
expected_file_data},
]}
}
)
def test_boot_invalid_keyfile(self):
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
self.assertRaises(exceptions.CommandError, shell, 'boot some-server '
'--image 1 --key %s' % invalid_file)
def test_boot_ipgroup(self):
shell('boot --image 1 --ipgroup 1 some-server')
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}}
)
def test_boot_ipgroup_name(self):
shell('boot --image 1 --ipgroup group1 some-server')
assert_called(
'POST', '/servers',
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}}
)
def test_flavor_list(self):
shell('flavor-list')
assert_called_anytime('GET', '/flavors/detail')
def test_image_list(self):
shell('image-list')
assert_called('GET', '/images/detail')
def test_snapshot_create(self):
shell('image-create sample-server mysnapshot')
assert_called(
'POST', '/images',
{'image': {'name': 'mysnapshot', 'serverId': 1234}}
)
def test_image_delete(self):
shell('image-delete 1')
assert_called('DELETE', '/images/1')
def test_ip_share(self):
shell('ip-share sample-server 1 1.2.3.4')
assert_called(
'PUT', '/servers/1234/ips/public/1.2.3.4',
{'shareIp': {'sharedIpGroupId': 1, 'configureServer': True}}
)
def test_ip_unshare(self):
shell('ip-unshare sample-server 1.2.3.4')
assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
def test_ipgroup_list(self):
shell('ipgroup-list')
assert ('GET', '/shared_ip_groups/detail', None) in \
_shell.cs.client.callstack
assert_called('GET', '/servers/5678')
def test_ipgroup_show(self):
shell('ipgroup-show 1')
assert_called('GET', '/shared_ip_groups/1')
shell('ipgroup-show group2')
# does a search, not a direct GET
assert_called('GET', '/shared_ip_groups/detail')
def test_ipgroup_create(self):
shell('ipgroup-create a-group')
assert_called(
'POST', '/shared_ip_groups',
{'sharedIpGroup': {'name': 'a-group'}}
)
shell('ipgroup-create a-group sample-server')
assert_called(
'POST', '/shared_ip_groups',
{'sharedIpGroup': {'name': 'a-group', 'server': 1234}}
)
def test_ipgroup_delete(self):
shell('ipgroup-delete group1')
assert_called('DELETE', '/shared_ip_groups/1')
def test_list(self):
shell('list')
assert_called('GET', '/servers/detail')
def test_reboot(self):
shell('reboot sample-server')
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
shell('reboot sample-server --hard')
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
def test_rebuild(self):
shell('rebuild sample-server 1')
assert_called('POST', '/servers/1234/action', {'rebuild': {'imageId': 1}})
def test_rename(self):
shell('rename sample-server newname')
assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
def test_resize(self):
shell('resize sample-server 1')
assert_called('POST', '/servers/1234/action', {'resize': {'flavorId': 1}})
def test_resize_confirm(self):
shell('resize-confirm sample-server')
assert_called('POST', '/servers/1234/action', {'confirmResize': None})
def test_resize_revert(self):
shell('resize-revert sample-server')
assert_called('POST', '/servers/1234/action', {'revertResize': None})
def test_backup(self):
shell('backup sample-server mybackup daily 1')
assert_called(
'POST', '/servers/1234/action',
{'createBackup': {'name': 'mybackup', 'backup_type': 'daily',
'rotation': 1}}
)
@mock.patch('getpass.getpass', mock.Mock(return_value='p'))
def test_root_password(self):
shell('root-password sample-server')
assert_called('PUT', '/servers/1234', {'server': {'adminPass': 'p'}})
def test_show(self):
shell('show 1234')
# XXX need a way to test multiple calls
# assert_called('GET', '/servers/1234')
assert_called('GET', '/images/2')
def test_delete(self):
shell('delete 1234')
assert_called('DELETE', '/servers/1234')
shell('delete sample-server')
assert_called('DELETE', '/servers/1234')
def test_zone(self):
shell('zone 1')
assert_called('GET', '/zones/1')
shell('zone 1 --api_url=http://zzz --zone_username=frank --password=xxx')
assert_called(
'PUT', '/zones/1',
{'zone': {'api_url': 'http://zzz', 'username': 'frank',
'password': 'xxx'}}
)
def test_zone_add(self):
shell('zone-add http://zzz frank xxx 0.0 1.0')
assert_called(
'POST', '/zones',
{'zone': {'api_url': 'http://zzz', 'username': 'frank',
'password': 'xxx',
'weight_offset': '0.0', 'weight_scale': '1.0'}}
)
def test_zone_delete(self):
shell('zone-delete 1')
assert_called('DELETE', '/zones/1')
def test_zone_list(self):
shell('zone-list')
assert ('GET', '/zones/detail', None) in _shell.cs.client.callstack

View File

@ -1,83 +1,76 @@
from __future__ import absolute_import
import StringIO
from nose.tools import assert_equal
from novaclient.v1_0 import zones
from .fakes import FakeClient
from .utils import assert_isinstance
os = FakeClient()
from tests.v1_0 import fakes
from tests import utils
def test_list_zones():
sl = os.zones.list()
os.assert_called('GET', '/zones/detail')
[assert_isinstance(s, zones.Zone) for s in sl]
os = fakes.FakeClient()
def test_list_zones_undetailed():
sl = os.zones.list(detailed=False)
os.assert_called('GET', '/zones')
[assert_isinstance(s, zones.Zone) for s in sl]
class ZonesTest(utils.TestCase):
def test_list_zones(self):
sl = os.zones.list()
os.assert_called('GET', '/zones/detail')
[self.assertTrue(isinstance(s, zones.Zone)) for s in sl]
def test_get_zone_details():
s = os.zones.get(1)
os.assert_called('GET', '/zones/1')
assert_isinstance(s, zones.Zone)
assert_equal(s.id, 1)
assert_equal(s.api_url, 'http://foo.com')
def test_list_zones_undetailed(self):
sl = os.zones.list(detailed=False)
os.assert_called('GET', '/zones')
[self.assertTrue(isinstance(s, zones.Zone)) for s in sl]
def test_get_zone_details(self):
s = os.zones.get(1)
os.assert_called('GET', '/zones/1')
self.assertTrue(isinstance(s, zones.Zone))
self.assertEqual(s.id, 1)
self.assertEqual(s.api_url, 'http://foo.com')
def test_create_zone():
s = os.zones.create(api_url="http://foo.com", username='bob',
password='xxx')
os.assert_called('POST', '/zones')
assert_isinstance(s, zones.Zone)
def test_create_zone(self):
s = os.zones.create(api_url="http://foo.com", username='bob',
password='xxx')
os.assert_called('POST', '/zones')
self.assertTrue(isinstance(s, zones.Zone))
def test_update_zone(self):
s = os.zones.get(1)
def test_update_zone():
s = os.zones.get(1)
# Update via instance
s.update(api_url='http://blah.com')
os.assert_called('PUT', '/zones/1')
s.update(api_url='http://blah.com', username='alice', password='xxx')
os.assert_called('PUT', '/zones/1')
# Update via instance
s.update(api_url='http://blah.com')
os.assert_called('PUT', '/zones/1')
s.update(api_url='http://blah.com', username='alice', password='xxx')
os.assert_called('PUT', '/zones/1')
# Silly, but not an error
s.update()
# Silly, but not an error
s.update()
# Update via manager
os.zones.update(s, api_url='http://blah.com')
os.assert_called('PUT', '/zones/1')
os.zones.update(1, api_url='http://blah.com')
os.assert_called('PUT', '/zones/1')
os.zones.update(s, api_url='http://blah.com', username='fred',
password='zip')
os.assert_called('PUT', '/zones/1')
# Update via manager
os.zones.update(s, api_url='http://blah.com')
os.assert_called('PUT', '/zones/1')
os.zones.update(1, api_url= 'http://blah.com')
os.assert_called('PUT', '/zones/1')
os.zones.update(s, api_url='http://blah.com', username='fred',
password='zip')
os.assert_called('PUT', '/zones/1')
def test_delete_zone(self):
s = os.zones.get(1)
s.delete()
os.assert_called('DELETE', '/zones/1')
os.zones.delete(1)
os.assert_called('DELETE', '/zones/1')
os.zones.delete(s)
os.assert_called('DELETE', '/zones/1')
def test_find_zone(self):
s = os.zones.find(password='qwerty')
os.assert_called('GET', '/zones/detail')
self.assertEqual(s.username, 'bob')
def test_delete_zone():
s = os.zones.get(1)
s.delete()
os.assert_called('DELETE', '/zones/1')
os.zones.delete(1)
os.assert_called('DELETE', '/zones/1')
os.zones.delete(s)
os.assert_called('DELETE', '/zones/1')
def test_find_zone():
s = os.zones.find(password='qwerty')
os.assert_called('GET', '/zones/detail')
assert_equal(s.username, 'bob')
# Find with multiple results returns the first item
s = os.zones.find(api_url='http://foo.com')
sl = os.zones.findall(api_url='http://foo.com')
assert_equal(sl[0], s)
assert_equal([s.id for s in sl], [1, 2])
# Find with multiple results returns the first item
s = os.zones.find(api_url='http://foo.com')
sl = os.zones.findall(api_url='http://foo.com')
self.assertEqual(sl[0], s)
self.assertEqual([s.id for s in sl], [1, 2])

View File

@ -1 +1 @@
OH HAI!
BLAH

View File

@ -1,86 +1,23 @@
"""
A fake server that "responds" to API methods with pre-canned responses.
All of these responses come from the spec, so if for some reason the spec's
wrong the tests might fail. I've indicated in comments the places where actual
behavior differs from the spec.
"""
from __future__ import absolute_import
import urlparse
import urllib
import httplib2
import urllib
import urlparse
from novaclient.v1_1 import Client
from novaclient.v1_1.client import HTTPClient
from .utils import fail, assert_in, assert_not_in, assert_has_keys
from novaclient import client as base_client
from novaclient.v1_1 import client
from tests import fakes
def assert_equal(value_one, value_two):
try:
assert value_one == value_two
except AssertionError:
print "%(value_one)s does not equal %(value_two)s" % locals()
raise
class FakeClient(fakes.FakeClient, client.Client):
def __init__(self, *args, **kwargs):
client.Client.__init__(self, 'username', 'apikey',
'project_id', 'auth_url')
self.client = FakeHTTPClient(**kwargs)
class FakeClient(Client):
def __init__(self, username=None, password=None, project_id=None,
auth_url=None):
super(FakeClient, self).__init__('username', 'apikey',
'project_id', 'auth_url')
self.client = FakeHTTPClient()
class FakeHTTPClient(base_client.HTTPClient):
def assert_called(self, method, url, body=None):
"""
Assert than an API method was just called.
"""
expected = (method, url)
called = self.client.callstack[-1][0:2]
assert self.client.callstack, \
"Expected %s %s but no calls were made." % expected
assert expected == called, 'Expected %s %s; got %s %s' % \
(expected + called)
if body is not None:
assert_equal(self.client.callstack[-1][2], body)
self.client.callstack = []
def assert_called_anytime(self, method, url, body=None):
"""
Assert than an API method was called anytime in the test.
"""
expected = (method, url)
assert self.client.callstack, \
"Expected %s %s but no calls were made." % expected
found = False
for entry in self.client.callstack:
called = entry[0:2]
if expected == entry[0:2]:
found = True
break
assert found, 'Expected %s; got %s' % \
(expected, self.client.callstack)
if body is not None:
assert_equal(entry[2], body)
self.client.callstack = []
def authenticate(self):
pass
class FakeHTTPClient(HTTPClient):
def __init__(self):
def __init__(self, **kwargs):
self.username = 'username'
self.apikey = 'apikey'
self.auth_url = 'auth_url'
@ -89,15 +26,15 @@ class FakeHTTPClient(HTTPClient):
def _cs_request(self, url, method, **kwargs):
# Check that certain things are called correctly
if method in ['GET', 'DELETE']:
assert_not_in('body', kwargs)
assert 'body' not in kwargs
elif method in ['PUT', 'POST']:
assert_in('body', kwargs)
assert 'body' in kwargs
# Call the method
munged_url = url.strip('/').replace('/', '_').replace('.', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
fail('Called unknown API method: %s %s' % (method, url))
raise AssertionError('Called unknown API method: %s %s' % (method, url))
# Note the call
self.callstack.append((method, url, kwargs.get('body', None)))
@ -184,25 +121,27 @@ class FakeHTTPClient(HTTPClient):
"name": "sample-server",
"image": {
"id": 2,
"name": "sample image",
},
"flavor": {
"id": 1,
"name": "256 MB Server",
},
"hostId": "e4d909c290d0fb1ca068ffaddf22cbd0",
"status": "BUILD",
"progress": 60,
"addresses": {
"public": [{
"addr": "1.2.3.4",
"version": 4,
"addr": "1.2.3.4",
},
{
"addr": "5.6.7.8",
"version": 4,
"addr": "5.6.7.8",
}],
"private": [{
"addr": "10.11.12.13",
"version": 4,
"addr": "10.11.12.13",
}],
},
"metadata": {
@ -215,24 +154,26 @@ class FakeHTTPClient(HTTPClient):
"name": "sample-server2",
"image": {
"id": 2,
"name": "sample image",
},
"flavor": {
"id": 1,
"name": "256 MB Server",
},
"hostId": "9e107d9d372bb6826bd81d3542a419d6",
"status": "ACTIVE",
"addresses": {
"public": [{
"addr": "1.2.3.5",
"version": 4,
"addr": "4.5.6.7",
},
{
"addr": "5.6.7.9",
"version": 4,
"addr": "5.6.9.8",
}],
"private": [{
"addr": "10.13.12.13",
"version": 4,
"addr": "10.13.12.13",
}],
},
"metadata": {
@ -242,13 +183,13 @@ class FakeHTTPClient(HTTPClient):
]})
def post_servers(self, body, **kw):
assert_equal(body.keys(), ['server'])
assert_has_keys(body['server'],
assert body.keys() == ['server']
fakes.assert_has_keys(body['server'],
required=['name', 'imageRef', 'flavorRef'],
optional=['metadata', 'personality'])
if 'personality' in body['server']:
for pfile in body['server']['personality']:
assert_has_keys(pfile, required=['path', 'contents'])
fakes.assert_has_keys(pfile, required=['path', 'contents'])
return (202, self.get_servers_1234()[1])
def get_servers_1234(self, **kw):
@ -260,8 +201,8 @@ class FakeHTTPClient(HTTPClient):
return (200, r)
def put_servers_1234(self, body, **kw):
assert_equal(body.keys(), ['server'])
assert_has_keys(body['server'], optional=['name', 'adminPass'])
assert body.keys() == ['server']
fakes.assert_has_keys(body['server'], optional=['name', 'adminPass'])
return (204, None)
def delete_servers_1234(self, **kw):
@ -283,12 +224,6 @@ class FakeHTTPClient(HTTPClient):
return (200, {'private':
self.get_servers_1234_ips()[1]['addresses']['private']})
def put_servers_1234_ips_public_1_2_3_4(self, body, **kw):
assert_equal(body.keys(), ['shareIp'])
assert_has_keys(body['shareIp'], required=['sharedIpGroupId',
'configureServer'])
return (202, None)
def delete_servers_1234_ips_public_1_2_3_4(self, **kw):
return (202, None)
@ -297,27 +232,27 @@ class FakeHTTPClient(HTTPClient):
#
def post_servers_1234_action(self, body, **kw):
assert_equal(len(body.keys()), 1)
assert len(body.keys()) == 1
action = body.keys()[0]
if action == 'reboot':
assert_equal(body[action].keys(), ['type'])
assert_in(body[action]['type'], ['HARD', 'SOFT'])
assert body[action].keys() == ['type']
assert body[action]['type'] in ['HARD', 'SOFT']
elif action == 'rebuild':
assert_equal(body[action].keys(), ['imageRef'])
assert body[action].keys() == ['imageRef']
elif action == 'resize':
assert_equal(body[action].keys(), ['flavorRef'])
assert body[action].keys() == ['flavorRef']
elif action == 'confirmResize':
assert_equal(body[action], None)
assert body[action] is None
# This one method returns a different response code
return (204, None)
elif action == 'revertResize':
assert_equal(body[action], None)
elif action == 'changePassword':
assert_equal(body[action].keys(), ["adminPass"])
assert body[action] is None
elif action == 'createImage':
assert_equal(body[action].keys(), ["name", "metadata"])
assert set(body[action].keys()) == set(['name', 'metadata'])
elif action == 'changePassword':
assert body[action].keys() == ['adminPass']
else:
fail("Unexpected server action: %s" % action)
raise AssertionError("Unexpected server action: %s" % action)
return (202, None)
#
@ -378,119 +313,9 @@ class FakeHTTPClient(HTTPClient):
return (200, {'image': self.get_images_detail()[1]['images'][1]})
def post_images(self, body, **kw):
assert_equal(body.keys(), ['image'])
assert_has_keys(body['image'], required=['serverId', 'name', 'image_type', 'backup_type', 'rotation'])
assert body.keys() == ['image']
fakes.assert_has_keys(body['image'], required=['serverId', 'name'])
return (202, self.get_images_1()[1])
def delete_images_1(self, **kw):
return (204, None)
#
# Backup schedules
#
def get_servers_1234_backup_schedule(self, **kw):
return (200, {"backupSchedule": {
"enabled": True,
"weekly": "THURSDAY",
"daily": "H_0400_0600"
}})
def post_servers_1234_backup_schedule(self, body, **kw):
assert_equal(body.keys(), ['backupSchedule'])
assert_has_keys(body['backupSchedule'], required=['enabled'],
optional=['weekly', 'daily'])
return (204, None)
def delete_servers_1234_backup_schedule(self, **kw):
return (204, None)
#
# Shared IP groups
#
def get_shared_ip_groups(self, **kw):
return (200, {'sharedIpGroups': [
{'id': 1, 'name': 'group1'},
{'id': 2, 'name': 'group2'},
]})
def get_shared_ip_groups_detail(self, **kw):
return (200, {'sharedIpGroups': [
{'id': 1, 'name': 'group1', 'servers': [1234]},
{'id': 2, 'name': 'group2', 'servers': [5678]},
]})
def get_shared_ip_groups_1(self, **kw):
return (200, {'sharedIpGroup':
self.get_shared_ip_groups_detail()[1]['sharedIpGroups'][0]})
def post_shared_ip_groups(self, body, **kw):
assert_equal(body.keys(), ['sharedIpGroup'])
assert_has_keys(body['sharedIpGroup'], required=['name'],
optional=['server'])
return (201, {'sharedIpGroup': {
'id': 10101,
'name': body['sharedIpGroup']['name'],
'servers': 'server' in body['sharedIpGroup'] and \
[body['sharedIpGroup']['server']] or None
}})
def delete_shared_ip_groups_1(self, **kw):
return (204, None)
#
# Zones
#
def get_zones(self, **kw):
return (200, {'zones': [
{'id': 1, 'api_url': 'http://foo.com', 'username': 'bob'},
{'id': 2, 'api_url': 'http://foo.com', 'username': 'alice'},
]})
def get_zones_detail(self, **kw):
return (200, {'zones': [
{'id': 1, 'api_url': 'http://foo.com', 'username': 'bob',
'password': 'qwerty'},
{'id': 2, 'api_url': 'http://foo.com', 'username': 'alice',
'password': 'password'}
]})
def get_zones_1(self, **kw):
r = {'zone': self.get_zones_detail()[1]['zones'][0]}
return (200, r)
def get_zones_2(self, **kw):
r = {'zone': self.get_zones_detail()[1]['zones'][1]}
return (200, r)
def post_zones(self, body, **kw):
assert_equal(body.keys(), ['zone'])
assert_has_keys(body['zone'],
required=['api_url', 'username', 'password'],
optional=['weight_offset', 'weight_scale'])
return (202, self.get_zones_1()[1])
def put_zones_1(self, body, **kw):
assert_equal(body.keys(), ['zone'])
assert_has_keys(body['zone'], optional=['api_url', 'username',
'password',
'weight_offset',
'weight_scale'])
return (204, None)
def delete_zones_1(self, **kw):
return (202, None)
#
# Accounts
#
def post_accounts_test_account_create_instance(self, body, **kw):
assert_equal(body.keys(), ['server'])
assert_has_keys(body['server'],
required=['name', 'imageRef', 'flavorRef'],
optional=['metadata', 'personality'])
if 'personality' in body['server']:
for pfile in body['server']['personality']:
assert_has_keys(pfile, required=['path', 'contents'])
return (202, self.get_servers_1234()[1])

74
tests/v1_1/test_auth.py Normal file
View File

@ -0,0 +1,74 @@
import httplib2
import mock
from novaclient.v1_1 import client
from novaclient import exceptions
from tests import utils
class AuthenticationTests(utils.TestCase):
def test_authenticate_success(self):
cs = client.Client("username", "apikey", "project_id", "auth_url")
management_url = 'https://servers.api.rackspacecloud.com/v1.1/443470'
auth_response = httplib2.Response({
'status': 204,
'x-server-management-url': management_url,
'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
})
mock_request = mock.Mock(return_value=(auth_response, None))
@mock.patch.object(httplib2.Http, "request", mock_request)
def test_auth_call():
cs.client.authenticate()
headers={
'X-Auth-User': 'username',
'X-Auth-Key': 'apikey',
'X-Auth-Project-Id': 'project_id',
'User-Agent': cs.client.USER_AGENT
}
mock_request.assert_called_with(cs.client.auth_url, 'GET',
headers=headers)
self.assertEqual(cs.client.management_url,
auth_response['x-server-management-url'])
self.assertEqual(cs.client.auth_token,
auth_response['x-auth-token'])
test_auth_call()
def test_authenticate_failure(self):
cs = client.Client("username", "apikey", "project_id", "auth_url")
auth_response = httplib2.Response({'status': 401})
mock_request = mock.Mock(return_value=(auth_response, None))
@mock.patch.object(httplib2.Http, "request", mock_request)
def test_auth_call():
self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
test_auth_call()
def test_auth_automatic(self):
cs = client.Client("username", "apikey", "project_id", "auth_url")
http_client = cs.client
http_client.management_url = ''
mock_request = mock.Mock(return_value=(None, None))
@mock.patch.object(http_client, 'request', mock_request)
@mock.patch.object(http_client, 'authenticate')
def test_auth_call(m):
http_client.get('/')
m.assert_called()
mock_request.assert_called()
test_auth_call()
def test_auth_manual(self):
cs = client.Client("username", "apikey", "project_id", "auth_url")
@mock.patch.object(cs.client, 'authenticate')
def test_auth_call(m):
cs.authenticate()
m.assert_called()
test_auth_call()

View File

@ -1,61 +0,0 @@
from __future__ import absolute_import
import mock
from nose.tools import assert_equal, assert_not_equal, assert_raises
from novaclient.v1_1 import flavors
from novaclient.v1_1 import exceptions
from novaclient.v1_1 import base
from .fakes import FakeClient
os = FakeClient()
def test_resource_repr():
r = base.Resource(None, dict(foo="bar", baz="spam"))
assert_equal(repr(r), "<Resource baz=spam, foo=bar>")
def test_getid():
assert_equal(base.getid(4), 4)
class O(object):
id = 4
assert_equal(base.getid(O), 4)
def test_resource_lazy_getattr():
f = flavors.Flavor(os.flavors, {'id': 1})
assert_equal(f.name, '256 MB Server')
os.assert_called('GET', '/flavors/1')
# Missing stuff still fails after a second get
assert_raises(AttributeError, getattr, f, 'blahblah')
os.assert_called('GET', '/flavors/1')
def test_eq():
# Two resources of the same type with the same id: equal
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
assert_equal(r1, r2)
# Two resoruces of different types: never equal
r1 = base.Resource(None, {'id': 1})
r2 = flavors.Flavor(None, {'id': 1})
assert_not_equal(r1, r2)
# Two resources with no ID: equal if their info is equal
r1 = base.Resource(None, {'name': 'joe', 'age': 12})
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
assert_equal(r1, r2)
def test_findall_invalid_attribute():
# Make sure findall with an invalid attribute doesn't cause errors.
# The following should not raise an exception.
os.flavors.findall(vegetable='carrot')
# However, find() should raise an error
assert_raises(exceptions.NotFound, os.flavors.find, vegetable='carrot')

View File

@ -1,52 +0,0 @@
import mock
import httplib2
from novaclient.v1_1 import client
from nose.tools import assert_equal
fake_response = httplib2.Response({"status": 200})
fake_body = '{"hi": "there"}'
mock_request = mock.Mock(return_value=(fake_response, fake_body))
def get_client():
cl = client.HTTPClient("username", "apikey", "project_id", "auth_test")
cl.management_url = "http://example.com"
cl.auth_token = "token"
return cl
def test_get():
cl = get_client()
@mock.patch.object(httplib2.Http, "request", mock_request)
@mock.patch('time.time', mock.Mock(return_value=1234))
def test_get_call():
resp, body = cl.get("/hi")
mock_request.assert_called_with("http://example.com/hi?fresh=1234",
"GET",
headers={"X-Auth-Token": "token",
"X-Auth-Project-Id": "project_id",
"User-Agent": cl.USER_AGENT})
# Automatic JSON parsing
assert_equal(body, {"hi": "there"})
test_get_call()
def test_post():
cl = get_client()
@mock.patch.object(httplib2.Http, "request", mock_request)
def test_post_call():
cl.post("/hi", body=[1, 2, 3])
mock_request.assert_called_with("http://example.com/hi", "POST",
headers={
"X-Auth-Token": "token",
"X-Auth-Project-Id": "project_id",
"Content-Type": "application/json",
"User-Agent": cl.USER_AGENT},
body='[1, 2, 3]'
)
test_post_call()

View File

@ -1,42 +1,38 @@
from __future__ import absolute_import
from nose.tools import assert_raises, assert_equal
from novaclient import exceptions
from novaclient.v1_1 import flavors
from novaclient.v1_1 import exceptions
from .fakes import FakeClient
from .utils import assert_isinstance
os = FakeClient()
from tests.v1_1 import fakes
from tests import utils
def test_list_flavors():
fl = os.flavors.list()
os.assert_called('GET', '/flavors/detail')
[assert_isinstance(f, flavors.Flavor) for f in fl]
cs = fakes.FakeClient()
def test_list_flavors_undetailed():
fl = os.flavors.list(detailed=False)
os.assert_called('GET', '/flavors')
[assert_isinstance(f, flavors.Flavor) for f in fl]
class FlavorsTest(utils.TestCase):
def test_list_flavors(self):
fl = cs.flavors.list()
cs.assert_called('GET', '/flavors/detail')
[self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl]
def test_get_flavor_details():
f = os.flavors.get(1)
os.assert_called('GET', '/flavors/1')
assert_isinstance(f, flavors.Flavor)
assert_equal(f.ram, 256)
assert_equal(f.disk, 10)
def test_list_flavors_undetailed(self):
fl = cs.flavors.list(detailed=False)
cs.assert_called('GET', '/flavors')
[self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl]
def test_get_flavor_details(self):
f = cs.flavors.get(1)
cs.assert_called('GET', '/flavors/1')
self.assertTrue(isinstance(f, flavors.Flavor))
self.assertEqual(f.ram, 256)
self.assertEqual(f.disk, 10)
def test_find():
f = os.flavors.find(ram=256)
os.assert_called('GET', '/flavors/detail')
assert_equal(f.name, '256 MB Server')
def test_find(self):
f = cs.flavors.find(ram=256)
cs.assert_called('GET', '/flavors/detail')
self.assertEqual(f.name, '256 MB Server')
f = os.flavors.find(disk=20)
assert_equal(f.name, '512 MB Server')
f = cs.flavors.find(disk=20)
self.assertEqual(f.name, '512 MB Server')
assert_raises(exceptions.NotFound, os.flavors.find, disk=12345)
self.assertRaises(exceptions.NotFound, cs.flavors.find, disk=12345)

View File

@ -1,51 +1,40 @@
from __future__ import absolute_import
from nose.tools import assert_equal
from novaclient.v1_1 import images
from .fakes import FakeClient
from .utils import assert_isinstance
os = FakeClient()
from tests.v1_1 import fakes
from tests import utils
def test_list_images():
il = os.images.list()
os.assert_called('GET', '/images/detail')
[assert_isinstance(i, images.Image) for i in il]
cs = fakes.FakeClient()
def test_list_images_undetailed():
il = os.images.list(detailed=False)
os.assert_called('GET', '/images')
[assert_isinstance(i, images.Image) for i in il]
class ImagesTest(utils.TestCase):
def test_list_images(self):
il = cs.images.list()
cs.assert_called('GET', '/images/detail')
[self.assertTrue(isinstance(i, images.Image)) for i in il]
def test_get_image_details():
i = os.images.get(1)
os.assert_called('GET', '/images/1')
assert_isinstance(i, images.Image)
assert_equal(i.id, 1)
assert_equal(i.name, 'CentOS 5.2')
def test_list_images_undetailed(self):
il = cs.images.list(detailed=False)
cs.assert_called('GET', '/images')
[self.assertTrue(isinstance(i, images.Image)) for i in il]
def test_get_image_details(self):
i = cs.images.get(1)
cs.assert_called('GET', '/images/1')
self.assertTrue(isinstance(i, images.Image))
self.assertEqual(i.id, 1)
self.assertEqual(i.name, 'CentOS 5.2')
def test_create_image():
i = os.images.create(server=1234, name="Just in case")
os.assert_called('POST', '/images')
assert_isinstance(i, images.Image)
def test_delete_image(self):
cs.images.delete(1)
cs.assert_called('DELETE', '/images/1')
def test_find(self):
i = cs.images.find(name="CentOS 5.2")
self.assertEqual(i.id, 1)
cs.assert_called('GET', '/images/detail')
def test_delete_image():
os.images.delete(1)
os.assert_called('DELETE', '/images/1')
def test_find():
i = os.images.find(name="CentOS 5.2")
assert_equal(i.id, 1)
os.assert_called('GET', '/images/detail')
iml = os.images.findall(status='SAVING')
assert_equal(len(iml), 1)
assert_equal(iml[0].name, 'My Server Backup')
iml = cs.images.findall(status='SAVING')
self.assertEqual(len(iml), 1)
self.assertEqual(iml[0].name, 'My Server Backup')

View File

@ -1,125 +1,114 @@
from __future__ import absolute_import
import StringIO
from nose.tools import assert_equal
from novaclient.v1_1 import servers
from .fakes import FakeClient
from .utils import assert_isinstance
from tests.v1_1 import fakes
from tests import utils
os = FakeClient()
cs = fakes.FakeClient()
def test_list_servers():
sl = os.servers.list()
os.assert_called('GET', '/servers/detail')
[assert_isinstance(s, servers.Server) for s in sl]
class ServersTest(utils.TestCase):
def test_list_servers(self):
sl = cs.servers.list()
cs.assert_called('GET', '/servers/detail')
[self.assertTrue(isinstance(s, servers.Server)) for s in sl]
def test_list_servers_undetailed():
sl = os.servers.list(detailed=False)
os.assert_called('GET', '/servers')
[assert_isinstance(s, servers.Server) for s in sl]
def test_list_servers_undetailed(self):
sl = cs.servers.list(detailed=False)
cs.assert_called('GET', '/servers')
[self.assertTrue(isinstance(s, servers.Server)) for s in sl]
def test_get_server_details(self):
s = cs.servers.get(1234)
cs.assert_called('GET', '/servers/1234')
self.assertTrue(isinstance(s, servers.Server))
self.assertEqual(s.id, 1234)
self.assertEqual(s.status, 'BUILD')
def test_get_server_details():
s = os.servers.get(1234)
os.assert_called('GET', '/servers/1234')
assert_isinstance(s, servers.Server)
assert_equal(s.id, 1234)
assert_equal(s.status, 'BUILD')
def test_create_server(self):
s = cs.servers.create(
name="My server",
image=1,
flavor=1,
meta={'foo': 'bar'},
files={
'/etc/passwd': 'some data', # a file
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
}
)
cs.assert_called('POST', '/servers')
self.assertTrue(isinstance(s, servers.Server))
def test_update_server(self):
s = cs.servers.get(1234)
def test_create_server():
s = os.servers.create(
name="My server",
image=1,
flavor=1,
meta={'foo': 'bar'},
files={
'/etc/passwd': 'some data', # a file
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
}
)
os.assert_called('POST', '/servers')
assert_isinstance(s, servers.Server)
# Update via instance
s.update(name='hi')
cs.assert_called('PUT', '/servers/1234')
s.update(name='hi')
cs.assert_called('PUT', '/servers/1234')
# Silly, but not an error
s.update()
def test_update_server():
s = os.servers.get(1234)
# Update via manager
cs.servers.update(s, name='hi')
cs.assert_called('PUT', '/servers/1234')
# Update via instance
s.update(name='hi')
os.assert_called('PUT', '/servers/1234')
def test_delete_server(self):
s = cs.servers.get(1234)
s.delete()
cs.assert_called('DELETE', '/servers/1234')
cs.servers.delete(1234)
cs.assert_called('DELETE', '/servers/1234')
cs.servers.delete(s)
cs.assert_called('DELETE', '/servers/1234')
# Silly, but not an error
s.update()
def test_find(self):
s = cs.servers.find(name='sample-server')
cs.assert_called('GET', '/servers/detail')
self.assertEqual(s.name, 'sample-server')
# Update via manager
os.servers.update(s, name='hi')
os.assert_called('PUT', '/servers/1234')
# Find with multiple results arbitraility returns the first item
s = cs.servers.find(flavor={"id": 1, "name": "256 MB Server"})
sl = cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"})
self.assertEqual(sl[0], s)
self.assertEqual([s.id for s in sl], [1234, 5678])
def test_reboot_server(self):
s = cs.servers.get(1234)
s.reboot()
cs.assert_called('POST', '/servers/1234/action')
cs.servers.reboot(s, type='HARD')
cs.assert_called('POST', '/servers/1234/action')
def test_delete_server():
s = os.servers.get(1234)
s.delete()
os.assert_called('DELETE', '/servers/1234')
os.servers.delete(1234)
os.assert_called('DELETE', '/servers/1234')
os.servers.delete(s)
os.assert_called('DELETE', '/servers/1234')
def test_rebuild_server(self):
s = cs.servers.get(1234)
s.rebuild(image=1)
cs.assert_called('POST', '/servers/1234/action')
cs.servers.rebuild(s, image=1)
cs.assert_called('POST', '/servers/1234/action')
def test_resize_server(self):
s = cs.servers.get(1234)
s.resize(flavor=1)
cs.assert_called('POST', '/servers/1234/action')
cs.servers.resize(s, flavor=1)
cs.assert_called('POST', '/servers/1234/action')
def test_find():
s = os.servers.find(name='sample-server')
os.assert_called('GET', '/servers/detail')
assert_equal(s.name, 'sample-server')
def test_confirm_resized_server(self):
s = cs.servers.get(1234)
s.confirm_resize()
cs.assert_called('POST', '/servers/1234/action')
cs.servers.confirm_resize(s)
cs.assert_called('POST', '/servers/1234/action')
# Find with multiple results arbitraility returns the first item
s = os.servers.find(flavor_id=1)
sl = os.servers.findall(flavor_id=1)
assert_equal(sl[0], s)
assert_equal([s.id for s in sl], [1234, 5678])
def test_reboot_server():
s = os.servers.get(1234)
s.reboot()
os.assert_called('POST', '/servers/1234/action')
os.servers.reboot(s, type='HARD')
os.assert_called('POST', '/servers/1234/action')
def test_rebuild_server():
s = os.servers.get(1234)
s.rebuild(image=1)
os.assert_called('POST', '/servers/1234/action')
os.servers.rebuild(s, image=1)
os.assert_called('POST', '/servers/1234/action')
def test_resize_server():
s = os.servers.get(1234)
s.resize(flavor=1)
os.assert_called('POST', '/servers/1234/action')
os.servers.resize(s, flavor=1)
os.assert_called('POST', '/servers/1234/action')
def test_confirm_resized_server():
s = os.servers.get(1234)
s.confirm_resize()
os.assert_called('POST', '/servers/1234/action')
os.servers.confirm_resize(s)
os.assert_called('POST', '/servers/1234/action')
def test_revert_resized_server():
s = os.servers.get(1234)
s.revert_resize()
os.assert_called('POST', '/servers/1234/action')
os.servers.revert_resize(s)
os.assert_called('POST', '/servers/1234/action')
def test_revert_resized_server(self):
s = cs.servers.get(1234)
s.revert_resize()
cs.assert_called('POST', '/servers/1234/action')
cs.servers.revert_resize(s)
cs.assert_called('POST', '/servers/1234/action')

View File

@ -1,234 +1,210 @@
from __future__ import absolute_import
import os
import mock
import httplib2
from nose.tools import assert_raises, assert_equal
from novaclient.v1_1.shell import OpenStackShell, CommandError
from .fakes import FakeClient
from .utils import assert_in
from novaclient.shell import OpenStackComputeShell
from novaclient import exceptions
from tests.v1_1 import fakes
from tests import utils
# Patch os.environ to avoid required auth info.
def setup():
global _old_env
fake_env = {
'NOVA_USERNAME': 'username',
'NOVA_API_KEY': 'password',
'NOVA_PROJECT_ID': 'project_id'
}
_old_env, os.environ = os.environ, fake_env.copy()
class ShellTest(utils.TestCase):
# Make a fake shell object, a helping wrapper to call it, and a quick way
# of asserting that certain API calls were made.
global shell, _shell, assert_called, assert_called_anytime
_shell = OpenStackShell()
_shell._api_class = FakeClient
assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b)
assert_called_anytime = lambda m, u, b=None: \
_shell.cs.assert_called_anytime(m, u, b)
shell = lambda cmd: _shell.main(cmd.split())
def teardown():
global _old_env
os.environ = _old_env
def test_boot():
shell('boot --image 1 some-server')
assert_called(
'POST', '/servers',
{'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1'}}
)
shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
assert_called(
'POST', '/servers',
{'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
'metadata': {'foo': 'bar', 'spam': 'eggs'}}}
)
def test_boot_files():
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
expected_file_data = open(testfile).read().encode('base64')
shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' %
(testfile, testfile))
assert_called(
'POST', '/servers',
{'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
'personality': [
{'path': '/tmp/bar', 'contents': expected_file_data},
{'path': '/tmp/foo', 'contents': expected_file_data}
]}
# Patch os.environ to avoid required auth info.
def setUp(self):
global _old_env
fake_env = {
'NOVA_USERNAME': 'username',
'NOVA_API_KEY': 'password',
'NOVA_PROJECT_ID': 'project_id'
}
)
_old_env, os.environ = os.environ, fake_env.copy()
# Make a fake shell object, a helping wrapper to call it, and a quick way
# of asserting that certain API calls were made.
global shell, _shell, assert_called, assert_called_anytime
_shell = OpenStackComputeShell()
_shell._api_class = fakes.FakeClient
assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b)
assert_called_anytime = lambda m, u, b=None: \
_shell.cs.assert_called_anytime(m, u, b)
shell = lambda cmd: _shell.main(cmd.split())
def test_boot_invalid_file():
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
assert_raises(CommandError, shell, 'boot some-server --image 1 '
'--file /foo=%s' % invalid_file)
def tearDown(self):
global _old_env
os.environ = _old_env
def test_boot_key_auto():
mock_exists = mock.Mock(return_value=True)
mock_open = mock.Mock()
mock_open.return_value = mock.Mock()
mock_open.return_value.read = mock.Mock(return_value='SSHKEY')
@mock.patch('os.path.exists', mock_exists)
@mock.patch('__builtin__.open', mock_open)
def test_shell_call():
shell('boot some-server --image 1 --key')
def test_boot(self):
shell('boot --image 1 some-server')
assert_called(
'POST', '/servers',
{'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
'personality': [{
'path': '/root/.ssh/authorized_keys2',
'contents': ('SSHKEY').encode('base64')},
]}
{'server': {
'flavorRef': 1,
'name': 'some-server',
'imageRef': '1',
'personality': [],
'metadata': {},
}}
)
shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
assert_called(
'POST', '/servers',
{'server': {
'flavorRef': 1,
'name': 'some-server',
'imageRef': '1',
'metadata': {'foo': 'bar', 'spam': 'eggs'},
'personality': [],
}}
)
def test_boot_files(self):
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
expected_file_data = open(testfile).read().encode('base64')
cmd = 'boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s'
shell(cmd % (testfile, testfile))
assert_called(
'POST', '/servers',
{'server': {
'flavorRef': 1,
'name': 'some-server',
'imageRef': '1',
'metadata': {},
'personality': [
{'path': '/tmp/bar', 'contents': expected_file_data},
{'path': '/tmp/foo', 'contents': expected_file_data}
]}
}
)
test_shell_call()
def test_boot_invalid_file(self):
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
cmd = 'boot some-server --image 1 --file /foo=%s' % invalid_file
self.assertRaises(exceptions.CommandError, shell, cmd)
def test_boot_key_auto(self):
mock_exists = mock.Mock(return_value=True)
mock_open = mock.Mock()
mock_open.return_value = mock.Mock()
mock_open.return_value.read = mock.Mock(return_value='SSHKEY')
def test_boot_key_auto_no_keys():
mock_exists = mock.Mock(return_value=False)
@mock.patch('os.path.exists', mock_exists)
def test_shell_call():
assert_raises(CommandError, shell, 'boot some-server --image 1 --key')
test_shell_call()
def test_boot_key_file():
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
expected_file_data = open(testfile).read().encode('base64')
shell('boot some-server --image 1 --key %s' % testfile)
assert_called(
'POST', '/servers',
{'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
'personality': [
{'path': '/root/.ssh/authorized_keys2', 'contents':
expected_file_data},
@mock.patch('os.path.exists', mock_exists)
@mock.patch('__builtin__.open', mock_open)
def test_shell_call():
shell('boot some-server --image 1 --key')
assert_called(
'POST', '/servers',
{'server': {
'flavorRef': 1,
'name': 'some-server',
'imageRef': '1',
'metadata': {},
'personality': [{
'path': '/root/.ssh/authorized_keys2',
'contents': ('SSHKEY').encode('base64')},
]}
}
)
}
)
test_shell_call()
def test_boot_invalid_keyfile():
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
assert_raises(CommandError, shell, 'boot some-server '
'--image 1 --key %s' % invalid_file)
def test_boot_key_auto_no_keys(self):
mock_exists = mock.Mock(return_value=False)
@mock.patch('os.path.exists', mock_exists)
def test_shell_call():
self.assertRaises(exceptions.CommandError, shell,
'boot some-server --image 1 --key')
def test_flavor_list():
shell('flavor-list')
assert_called_anytime('GET', '/flavors/detail')
test_shell_call()
def test_boot_key_file(self):
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
expected_file_data = open(testfile).read().encode('base64')
shell('boot some-server --image 1 --key %s' % testfile)
assert_called(
'POST', '/servers',
{'server': {
'flavorRef': 1,
'name': 'some-server', 'imageRef': '1',
'metadata': {},
'personality': [
{'path': '/root/.ssh/authorized_keys2',
'contents':expected_file_data},
]}
}
)
def test_image_list():
shell('image-list')
assert_called('GET', '/images/detail')
def test_boot_invalid_keyfile(self):
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
self.assertRaises(exceptions.CommandError, shell, 'boot some-server '
'--image 1 --key %s' % invalid_file)
def test_flavor_list(self):
shell('flavor-list')
assert_called_anytime('GET', '/flavors/detail')
def test_create_image():
shell('create-image sample-server mysnapshot')
assert_called(
'POST', '/servers/1234/action',
{'createImage': {'name': 'mysnapshot', "metadata": {}}}
)
def test_image_list(self):
shell('image-list')
assert_called('GET', '/images/detail')
def test_create_image(self):
shell('create-image sample-server mysnapshot')
assert_called(
'POST', '/servers/1234/action',
{'createImage': {'name': 'mysnapshot', 'metadata': {}}}
)
def test_image_delete():
shell('image-delete 1')
assert_called('DELETE', '/images/1')
def test_image_delete(self):
shell('image-delete 1')
assert_called('DELETE', '/images/1')
def test_list(self):
shell('list')
assert_called('GET', '/servers/detail')
def test_list():
shell('list')
assert_called('GET', '/servers/detail')
def test_reboot(self):
shell('reboot sample-server')
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
shell('reboot sample-server --hard')
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
def test_rebuild(self):
shell('rebuild sample-server 1')
assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1}})
def test_reboot():
shell('reboot sample-server')
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
shell('reboot sample-server --hard')
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
def test_rename(self):
shell('rename sample-server newname')
assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
def test_resize(self):
shell('resize sample-server 1')
assert_called('POST', '/servers/1234/action', {'resize': {'flavorRef': 1}})
def test_rebuild():
shell('rebuild sample-server 1')
assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1}})
def test_resize_confirm(self):
shell('resize-confirm sample-server')
assert_called('POST', '/servers/1234/action', {'confirmResize': None})
def test_resize_revert(self):
shell('resize-revert sample-server')
assert_called('POST', '/servers/1234/action', {'revertResize': None})
def test_rename():
shell('rename sample-server newname')
assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
@mock.patch('getpass.getpass', mock.Mock(return_value='p'))
def test_root_password(self):
shell('root-password sample-server')
assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}})
def test_show(self):
shell('show 1234')
# XXX need a way to test multiple calls
# assert_called('GET', '/servers/1234')
assert_called('GET', '/images/2')
def test_resize():
shell('resize sample-server 1')
assert_called('POST', '/servers/1234/action', {'resize': {'flavorRef': 1}})
def test_resize_confirm():
shell('resize-confirm sample-server')
assert_called('POST', '/servers/1234/action', {'confirmResize': None})
def test_resize_revert():
shell('resize-revert sample-server')
assert_called('POST', '/servers/1234/action', {'revertResize': None})
@mock.patch('getpass.getpass', mock.Mock(return_value='p'))
def test_root_password():
shell('root-password sample-server')
assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}})
def test_show():
shell('show 1234')
# XXX need a way to test multiple calls
# assert_called('GET', '/servers/1234')
assert_called('GET', '/images/2')
def test_delete():
shell('delete 1234')
assert_called('DELETE', '/servers/1234')
shell('delete sample-server')
assert_called('DELETE', '/servers/1234')
def test_help():
@mock.patch.object(_shell.parser, 'print_help')
def test_help(m):
shell('help')
m.assert_called()
@mock.patch.object(_shell.subcommands['delete'], 'print_help')
def test_help_delete(m):
shell('help delete')
m.assert_called()
test_help()
test_help_delete()
assert_raises(CommandError, shell, 'help foofoo')
def test_debug():
httplib2.debuglevel = 0
shell('--debug list')
assert httplib2.debuglevel == 1
def test_delete(self):
shell('delete 1234')
assert_called('DELETE', '/servers/1234')
shell('delete sample-server')
assert_called('DELETE', '/servers/1234')

View File

@ -1 +1 @@
OH HAI!
BLAH