Split everything down the middle into v1_0 and v1_1, including tests.

This commit is contained in:
Brian Lamar 2011-08-03 16:36:03 -04:00
parent 20251ccc2f
commit f8496672cc
53 changed files with 3425 additions and 539 deletions

5
.gitignore vendored
View File

@ -1,2 +1,5 @@
.coverage
*,cover
cover
*.pyc
.idea
.idea

View File

@ -14,74 +14,4 @@
# License for the specific language governing permissions and limitations
# under the License.
"""
novaclient module.
"""
__version__ = '2.5'
from novaclient.accounts import Account, AccountManager
from novaclient.backup_schedules import (
BackupSchedule, BackupScheduleManager,
BACKUP_WEEKLY_DISABLED, BACKUP_WEEKLY_SUNDAY, BACKUP_WEEKLY_MONDAY,
BACKUP_WEEKLY_TUESDAY, BACKUP_WEEKLY_WEDNESDAY,
BACKUP_WEEKLY_THURSDAY, BACKUP_WEEKLY_FRIDAY, BACKUP_WEEKLY_SATURDAY,
BACKUP_DAILY_DISABLED, BACKUP_DAILY_H_0000_0200,
BACKUP_DAILY_H_0200_0400, BACKUP_DAILY_H_0400_0600,
BACKUP_DAILY_H_0600_0800, BACKUP_DAILY_H_0800_1000,
BACKUP_DAILY_H_1000_1200, BACKUP_DAILY_H_1200_1400,
BACKUP_DAILY_H_1400_1600, BACKUP_DAILY_H_1600_1800,
BACKUP_DAILY_H_1800_2000, BACKUP_DAILY_H_2000_2200,
BACKUP_DAILY_H_2200_0000)
from novaclient.client import OpenStackClient
from novaclient.exceptions import (OpenStackException, BadRequest,
Unauthorized, Forbidden, NotFound, OverLimit)
from novaclient.flavors import FlavorManager, Flavor
from novaclient.images import ImageManager, Image
from novaclient.ipgroups import IPGroupManager, IPGroup
from novaclient.servers import (ServerManager, Server, REBOOT_HARD,
REBOOT_SOFT)
from novaclient.zones import Zone, ZoneManager
class OpenStack(object):
"""
Top-level object to access the OpenStack Nova API.
Create an instance with your creds::
>>> os = OpenStack(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='https://auth.api.rackspacecloud.com/v1.0', timeout=None):
self.backup_schedules = BackupScheduleManager(self)
self.client = OpenStackClient(username, apikey, projectid, auth_url,
timeout=timeout)
self.flavors = FlavorManager(self)
self.images = ImageManager(self)
self.ipgroups = IPGroupManager(self)
self.servers = ServerManager(self)
self.zones = ZoneManager(self)
self.accounts = AccountManager(self)
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

@ -0,0 +1,74 @@
# 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,9 +1,10 @@
from novaclient import base
from novaclient.v1_0 import base
class Account(base.Resource):
pass
class AccountManager(base.BootingManagerWithFind):
resource_class = Account
def create_instance_for(self, account_id, name, image, flavor,

View File

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

View File

@ -19,7 +19,7 @@
Base utilities to build API operation managers and objects on top of.
"""
from novaclient.exceptions import NotFound
from novaclient.v1_0 import exceptions
# Python 2.4 compat
try:
@ -68,7 +68,7 @@ class Manager(object):
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])
@ -101,7 +101,7 @@ class ManagerWithFind(Manager):
try:
return rl[0]
except IndexError:
raise NotFound(404, "No %s matching %s." %
raise exceptions.NotFound(404, "No %s matching %s." %
(self.resource_class.__name__, kwargs))
def findall(self, **kwargs):

View File

@ -20,16 +20,16 @@ if not hasattr(urlparse, 'parse_qsl'):
urlparse.parse_qsl = cgi.parse_qsl
import novaclient
from novaclient import exceptions
from novaclient.v1_0 import exceptions
_logger = logging.getLogger(__name__)
class OpenStackClient(httplib2.Http):
class HTTPClient(httplib2.Http):
USER_AGENT = 'python-novaclient/%s' % novaclient.__version__
def __init__(self, user, apikey, projectid, auth_url, timeout=None):
super(OpenStackClient, self).__init__(timeout=timeout)
super(HTTPClient, self).__init__(timeout=timeout)
self.user = user
self.apikey = apikey
self.projectid = projectid
@ -45,7 +45,7 @@ class OpenStackClient(httplib2.Http):
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'):
@ -66,10 +66,10 @@ class OpenStackClient(httplib2.Http):
kwargs['headers']['Content-Type'] = 'application/json'
kwargs['body'] = json.dumps(kwargs['body'])
resp, body = super(OpenStackClient, self).request(*args, **kwargs)
resp, body = super(HTTPClient, self).request(*args, **kwargs)
self.http_log(args, kwargs, resp, body)
if body:
try:
body = json.loads(body)

View File

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

View File

@ -3,7 +3,7 @@
Image interface.
"""
from novaclient import base
from novaclient.v1_0 import base
class Image(base.Resource):
@ -60,7 +60,7 @@ class ImageManager(base.ManagerWithFind):
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")

View File

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

View File

@ -20,7 +20,7 @@ Server interface.
"""
import urllib
from novaclient import base
from novaclient.v1_0 import base
REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
@ -228,7 +228,7 @@ class ServerManager(base.BootingManagerWithFind):
qparams[opt] = val
query_string = "?%s" % urllib.urlencode(qparams) if qparams else ""
detail = ""
if detailed:
detail = "/detail"

View File

@ -20,7 +20,6 @@ Command-line interface to the OpenStack Nova API.
"""
import argparse
import novaclient
import getpass
import httplib2
import os
@ -29,12 +28,17 @@ import sys
import textwrap
import uuid
import novaclient.v1_0
from novaclient.v1_0 import backup_schedules
from novaclient.v1_0 import exceptions
from novaclient.v1_0 import servers
# Choices for flags.
DAY_CHOICES = [getattr(novaclient, i).lower()
for i in dir(novaclient)
DAY_CHOICES = [getattr(backup_schedules, i).lower()
for i in dir(backup_schedules)
if i.startswith('BACKUP_WEEKLY_')]
HOUR_CHOICES = [getattr(novaclient, i).lower()
for i in dir(novaclient)
HOUR_CHOICES = [getattr(backup_schedules, i).lower()
for i in dir(backup_schedules)
if i.startswith('BACKUP_DAILY_')]
@ -66,7 +70,7 @@ def env(e):
class OpenStackShell(object):
# Hook for the test suite to inject a fake server.
_api_class = novaclient.OpenStack
_api_class = novaclient.v1_0.Client
def __init__(self):
self.parser = argparse.ArgumentParser(
@ -98,7 +102,7 @@ class OpenStackShell(object):
help='Defaults to env[NOVA_API_KEY].')
self.parser.add_argument('--projectid',
default=env('NOVA_PROJECT_ID'),
default=env('NOVA_PROJECT_ID'),
help='Defaults to env[NOVA_PROJECT_ID].')
auth_url = env('NOVA_URL')
@ -165,7 +169,7 @@ class OpenStackShell(object):
self.cs = self._api_class(user, apikey, projectid, url)
try:
self.cs.authenticate()
except novaclient.Unauthorized:
except exceptions.Unauthorized:
raise CommandError("Invalid OpenStack Nova credentials.")
args.func(args)
@ -208,10 +212,10 @@ class OpenStackShell(object):
# If we have some flags, update the backup
backup = {}
if args.daily:
backup['daily'] = getattr(novaclient, 'BACKUP_DAILY_%s' %
backup['daily'] = getattr(backup_schedules, 'BACKUP_DAILY_%s' %
args.daily.upper())
if args.weekly:
backup['weekly'] = getattr(novaclient, 'BACKUP_WEEKLY_%s' %
backup['weekly'] = getattr(backup_schedules, 'BACKUP_WEEKLY_%s' %
args.weekly.upper())
if args.enabled is not None:
backup['enabled'] = args.enabled
@ -281,7 +285,7 @@ class OpenStackShell(object):
except IOError, e:
raise CommandError("Can't open '%s': %s" % (keyfile, e))
return (args.name, image, flavor, ipgroup, metadata, files,
return (args.name, image, flavor, ipgroup, metadata, files,
reservation_id, min_count, max_count)
@arg('--flavor',
@ -461,7 +465,7 @@ class OpenStackShell(object):
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])
def do_flavor_list(self, args):
"""Print a list of available 'flavors' (sizes of servers)."""
flavors = self.cs.flavors.list()
@ -630,8 +634,8 @@ class OpenStackShell(object):
@arg('--hard',
dest='reboot_type',
action='store_const',
const=novaclient.REBOOT_HARD,
default=novaclient.REBOOT_SOFT,
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):
@ -766,7 +770,7 @@ class OpenStackShell(object):
def do_zone(self, args):
"""Show or edit a child zone. No zone arg for this zone."""
zone = self.cs.zones.get(args.zone)
# If we have some flags, update the zone
zone_delta = {}
if args.api_url:
@ -790,7 +794,7 @@ class OpenStackShell(object):
print_dict(zone._info)
@arg('api_url', metavar='<api_url>', help="URL for the Zone's API")
@arg('zone_username', metavar='<zone_username>',
@arg('zone_username', metavar='<zone_username>',
help='Authentication username.')
@arg('password', metavar='<password>', help='Authentication password.')
@arg('weight_offset', metavar='<weight_offset>',
@ -799,7 +803,7 @@ class OpenStackShell(object):
help='Child Zone weight scale (typically 1.0).')
def do_zone_add(self, args):
"""Add a new child zone."""
zone = self.cs.zones.create(args.api_url, args.zone_username,
zone = self.cs.zones.create(args.api_url, args.zone_username,
args.password, args.weight_offset,
args.weight_scale)
print_dict(zone._info)
@ -820,7 +824,7 @@ class OpenStackShell(object):
"""Add new IP address to network."""
server = self._find_server(args.server)
server.add_fixed_ip(args.network_id)
@arg('server', metavar='<server>', help='Name or ID of server.')
@arg('address', metavar='<address>', help='IP Address.')
def do_remove_fixed_ip(self, args):
@ -844,7 +848,7 @@ class OpenStackShell(object):
"""Get a flavor by name, ID, or RAM size."""
try:
return self._find_resource(self.cs.flavors, flavor)
except novaclient.NotFound:
except exceptions.NotFound:
return self.cs.flavors.find(ram=flavor)
def _find_resource(self, manager, name_or_id):
@ -858,7 +862,7 @@ class OpenStackShell(object):
return manager.get(name_or_id)
except ValueError:
return manager.find(name=name_or_id)
except novaclient.NotFound:
except exceptions.NotFound:
raise CommandError("No %s with a name or ID of '%s' exists." %
(manager.resource_class.__name__.lower(), name_or_id))

View File

@ -17,14 +17,14 @@
Zone interface.
"""
from novaclient import base
from novaclient.v1_0 import base
class Weighting(base.Resource):
def __init__(self, manager, info):
self.name = "n/a"
super(Weighting, self).__init__(manager, info)
def __repr__(self):
return "<Weighting: %s>" % self.name

View File

@ -0,0 +1,66 @@
# 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()

214
novaclient/v1_1/base.py Normal file
View File

@ -0,0 +1,214 @@
# 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.v1_1 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:
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):
"""Like a `ManagerWithFind`, but has the ability to boot servers."""
def _boot(self, resource_url, response_key, name, image, flavor,
meta=None, files=None, return_raw=False):
"""
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 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 return_raw: If True, don't try to coearse the result into
a Resource object.
"""
body = {"server": {
"name": name,
"imageRef": getid(image),
"flavorRef": getid(flavor),
}}
if meta:
body["server"]["metadata"] = meta
# 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

154
novaclient/v1_1/client.py Normal file
View File

@ -0,0 +1,154 @@
# 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
import novaclient
from novaclient.v1_1 import exceptions
_logger = logging.getLogger(__name__)
class HTTPClient(httplib2.Http):
USER_AGENT = 'python-novaclient/%s' % novaclient.__version__
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']:
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)
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 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.
"""
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

@ -0,0 +1,100 @@
# 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

@ -0,0 +1,41 @@
# Copyright 2010 Jacob Kaplan-Moss
"""
Flavor interface.
"""
from novaclient.v1_1 import base
class Flavor(base.Resource):
"""
A flavor is an available hardware configuration for a server.
"""
def __repr__(self):
return "<Flavor: %s>" % self.name
class FlavorManager(base.ManagerWithFind):
"""
Manage :class:`Flavor` resources.
"""
resource_class = Flavor
def list(self, detailed=True):
"""
Get a list of all flavors.
:rtype: list of :class:`Flavor`.
"""
detail = ""
if detailed:
detail = "/detail"
return self._list("/flavors%s" % detail, "flavors")
def get(self, flavor):
"""
Get a specific flavor.
:param flavor: The ID of the :class:`Flavor` to get.
:rtype: :class:`Flavor`
"""
return self._get("/flavors/%s" % base.getid(flavor), "flavor")

86
novaclient/v1_1/images.py Normal file
View File

@ -0,0 +1,86 @@
# Copyright 2010 Jacob Kaplan-Moss
"""
Image interface.
"""
from novaclient.v1_1 import base
class Image(base.Resource):
"""
An image is a collection of files used to create or rebuild a server.
"""
def __repr__(self):
return "<Image: %s>" % self.name
def delete(self):
"""
Delete this image.
"""
return self.manager.delete(self)
class ImageManager(base.ManagerWithFind):
"""
Manage :class:`Image` resources.
"""
resource_class = Image
def get(self, image):
"""
Get an image.
:param image: The ID of the image to get.
:rtype: :class:`Image`
"""
return self._get("/images/%s" % base.getid(image), "image")
def list(self, detailed=True):
"""
Get a list of all images.
: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")
def delete(self, image):
"""
Delete an image.
It should go without saying that you can't delete an image
that you didn't create.
:param image: The :class:`Image` (or its ID) to delete.
"""
self._delete("/images/%s" % base.getid(image))

300
novaclient/v1_1/servers.py Normal file
View File

@ -0,0 +1,300 @@
# 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.
"""
Server interface.
"""
import urllib
from novaclient.v1_1 import base
REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
class Server(base.Resource):
def __repr__(self):
return "<Server: %s>" % self.name
def delete(self):
"""
Delete (i.e. shut down and delete the image) this server.
"""
self.manager.delete(self)
def update(self, name=None):
"""
Update the name or the password for this server.
: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)
def change_password(self, password):
"""
Update the root password on this server.
:param password: The password to set.
"""
self.manager.change_password(self, password)
def reboot(self, type=REBOOT_SOFT):
"""
Reboot the server.
:param type: either :data:`REBOOT_SOFT` for a software-level reboot,
or `REBOOT_HARD` for a virtual power cycle hard reboot.
"""
self.manager.reboot(self, type)
def rebuild(self, image):
"""
Rebuild -- shut down and then re-image -- this server.
:param image: the :class:`Image` (or its ID) to re-image with.
"""
self.manager.rebuild(self, image)
def resize(self, flavor):
"""
Resize the server's resources.
:param flavor: the :class:`Flavor` (or its ID) to resize to.
Until a resize event is confirmed with :meth:`confirm_resize`, the old
server will be kept around and you'll be able to roll back to the old
flavor quickly with :meth:`revert_resize`. All resizes are
automatically confirmed after 24 hours.
"""
self.manager.resize(self, flavor)
def confirm_resize(self):
"""
Confirm that the resize worked, thus removing the original server.
"""
self.manager.confirm_resize(self)
def revert_resize(self):
"""
Revert a previous resize, switching back to the old server.
"""
self.manager.revert_resize(self)
@property
def public_ip(self):
"""
Shortcut to get this server's primary public IP address.
"""
if len(self.addresses['public']) == 0:
return ""
return self.addresses['public']
@property
def private_ip(self):
"""
Shortcut to get this server's primary private IP address.
"""
if len(self.addresses['private']) == 0:
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):
resource_class = Server
def get(self, server):
"""
Get a server.
:param server: ID of the :class:`Server` to get.
:rtype: :class:`Server`
"""
return self._get("/servers/%s" % base.getid(server), "server")
def list(self, detailed=True, search_opts=None):
"""
Get a list of servers.
Optional detailed returns details server info.
Optional reservation_id only returns instances with that
reservation_id.
:rtype: list of :class:`Server`
"""
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
query_string = "?%s" % urllib.urlencode(qparams) if qparams else ""
detail = ""
if detailed:
detail = "/detail"
return self._list("/servers%s%s" % (detail, query_string), "servers")
def create(self, name, image, flavor, meta=None, files=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 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.
"""
return self._boot("/servers", "server", name, image, flavor,
meta=meta, files=files)
def update(self, server, name=None):
"""
Update the name or the password for a server.
:param server: The :class:`Server` (or its ID) to update.
:param name: Update the server's name.
"""
if name is None:
return
body = {
"server": {
"name": name,
},
}
self._update("/servers/%s" % base.getid(server), body)
def delete(self, server):
"""
Delete (i.e. shut down and delete the image) this server.
"""
self._delete("/servers/%s" % base.getid(server))
def reboot(self, server, type=REBOOT_SOFT):
"""
Reboot a server.
:param server: The :class:`Server` (or its ID) to share onto.
:param type: either :data:`REBOOT_SOFT` for a software-level reboot,
or `REBOOT_HARD` for a virtual power cycle hard reboot.
"""
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.
:param server: The :class:`Server` (or its ID) to share onto.
:param image: the :class:`Image` (or its ID) to re-image with.
"""
self._action('rebuild', server, {'imageRef': base.getid(image)})
def resize(self, server, flavor):
"""
Resize a server's resources.
:param server: The :class:`Server` (or its ID) to share onto.
:param flavor: the :class:`Flavor` (or its ID) to resize to.
Until a resize event is confirmed with :meth:`confirm_resize`, the old
server will be kept around and you'll be able to roll back to the old
flavor quickly with :meth:`revert_resize`. All resizes are
automatically confirmed after 24 hours.
"""
self._action('resize', server, {'flavorRef': base.getid(flavor)})
def confirm_resize(self, server):
"""
Confirm that the resize worked, thus removing the original server.
:param server: The :class:`Server` (or its ID) to share onto.
"""
self._action('confirmResize', server)
def revert_resize(self, server):
"""
Revert a previous resize, switching back to the old server.
:param server: The :class:`Server` (or its ID) to share onto.
"""
self._action('revertResize', server)
def _action(self, action, server, info=None):
"""
Perform a server "action" -- reboot/rebuild/resize/etc.
"""
self.api.client.post('/servers/%s/action' % base.getid(server),
body={action: info})

584
novaclient/v1_1/shell.py Normal file
View File

@ -0,0 +1,584 @@
# 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 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.v1_1 import servers
def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)
# 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
class CommandError(Exception):
pass
def env(e):
return os.environ.get(e, '')
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)
try:
self.cs.authenticate()
except exceptions.Unauthorized:
raise CommandError("Invalid OpenStack Nova credentials.")
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)
else:
self.parser.print_help()
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."""
try:
return self._find_resource(self.cs.flavors, flavor)
except exceptions.NotFound:
return self.cs.flavors.find(ram=flavor)
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))
# 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)
# Helpers
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')
def main():
try:
OpenStackShell().main(sys.argv[1:])
except Exception, e:
if httplib2.debuglevel == 1:
raise # dump stack.
else:
print >> sys.stderr, e
sys.exit(1)

196
novaclient/v1_1/zones.py Normal file
View File

@ -0,0 +1,196 @@
# 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.
"""
Zone interface.
"""
from novaclient.v1_1 import base
class Weighting(base.Resource):
def __init__(self, manager, info):
self.name = "n/a"
super(Weighting, self).__init__(manager, info)
def __repr__(self):
return "<Weighting: %s>" % self.name
def to_dict(self):
"""Return the original info setting, which is a dict."""
return self._info
class Zone(base.Resource):
def __init__(self, manager, info):
self.name = "n/a"
self.is_active = "n/a"
self.capabilities = "n/a"
super(Zone, self).__init__(manager, info)
def __repr__(self):
return "<Zone: %s>" % self.api_url
def delete(self):
"""
Delete a child zone.
"""
self.manager.delete(self)
def update(self, api_url=None, username=None, password=None,
weight_offset=None, weight_scale=None):
"""
Update the name for this child zone.
:param api_url: Update the child zone's API URL.
:param username: Update the child zone's username.
:param password: Update the child zone's password.
:param weight_offset: Update the child zone's weight offset.
:param weight_scale: Update the child zone's weight scale.
"""
self.manager.update(self, api_url, username, password,
weight_offset, weight_scale)
class ZoneManager(base.BootingManagerWithFind):
resource_class = Zone
def info(self):
"""
Get info on this zone.
:rtype: :class:`Zone`
"""
return self._get("/zones/info", "zone")
def get(self, zone):
"""
Get a child zone.
:param server: ID of the :class:`Zone` to get.
:rtype: :class:`Zone`
"""
return self._get("/zones/%s" % base.getid(zone), "zone")
def list(self, detailed=True):
"""
Get a list of child zones.
:rtype: list of :class:`Zone`
"""
detail = ""
if detailed:
detail = "/detail"
return self._list("/zones%s" % detail, "zones")
def create(self, api_url, username, password,
weight_offset=0.0, weight_scale=1.0):
"""
Create a new child zone.
:param api_url: The child zone's API URL.
:param username: The child zone's username.
:param password: The child zone's password.
:param weight_offset: The child zone's weight offset.
:param weight_scale: The child zone's weight scale.
"""
body = {"zone": {
"api_url": api_url,
"username": username,
"password": password,
"weight_offset": weight_offset,
"weight_scale": weight_scale
}}
return self._create("/zones", body, "zone")
def boot(self, name, image, flavor, ipgroup=None, meta=None, files=None,
zone_blob=None, reservation_id=None, min_count=None,
max_count=None):
"""
Create (boot) a new server while being aware of Zones.
: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 min_count: minimum number of servers to create.
:param max_count: maximum number of servers to create.
"""
if not min_count:
min_count = 1
if not max_count:
max_count = min_count
return self._boot("/zones/boot", "reservation_id", name, image, flavor,
ipgroup=ipgroup, meta=meta, files=files,
zone_blob=zone_blob, reservation_id=reservation_id,
return_raw=True, min_count=min_count,
max_count=max_count)
def select(self, *args, **kwargs):
"""
Given requirements for a new instance, select hosts
in this zone that best match those requirements.
"""
# 'specs' may be passed in as None, so change to an empty string.
specs = kwargs.get("specs") or ""
url = "/zones/select"
weighting_list = self._list(url, "weights", Weighting, body=specs)
return [wt.to_dict() for wt in weighting_list]
def delete(self, zone):
"""
Delete a child zone.
"""
self._delete("/zones/%s" % base.getid(zone))
def update(self, zone, api_url=None, username=None, password=None,
weight_offset=None, weight_scale=None):
"""
Update the name or the api_url for a zone.
:param zone: The :class:`Zone` (or its ID) to update.
:param api_url: Update the API URL.
:param username: Update the username.
:param password: Update the password.
:param weight_offset: Update the child zone's weight offset.
:param weight_scale: Update the child zone's weight scale.
"""
body = {"zone": {}}
if api_url:
body["zone"]["api_url"] = api_url
if username:
body["zone"]["username"] = username
if password:
body["zone"]["password"] = password
if weight_offset:
body["zone"]["weight_offset"] = weight_offset
if weight_scale:
body["zone"]["weight_scale"] = weight_scale
if not len(body["zone"]):
return
self._update("/zones/%s" % base.getid(zone), body)

View File

@ -1,58 +0,0 @@
from novaclient.backup_schedules import *
from fakeserver import FakeServer
from utils import assert_isinstance
cs = FakeServer()
def test_get_backup_schedule():
s = cs.servers.get(1234)
# access via manager
b = cs.backup_schedules.get(server=s)
assert_isinstance(b, BackupSchedule)
cs.assert_called('GET', '/servers/1234/backup_schedule')
b = cs.backup_schedules.get(server=1234)
assert_isinstance(b, BackupSchedule)
cs.assert_called('GET', '/servers/1234/backup_schedule')
# access via instance
assert_isinstance(s.backup_schedule, BackupSchedule)
cs.assert_called('GET', '/servers/1234/backup_schedule')
# Just for coverage's sake
b = s.backup_schedule.get()
cs.assert_called('GET', '/servers/1234/backup_schedule')
def test_create_update_backup_schedule():
s = cs.servers.get(1234)
# create/update via manager
cs.backup_schedules.update(
server=s,
enabled=True,
weekly=BACKUP_WEEKLY_THURSDAY,
daily=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():
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,37 +0,0 @@
from novaclient import Flavor, NotFound
from fakeserver import FakeServer
from utils import assert_isinstance
from nose.tools import assert_raises, assert_equal
cs = FakeServer()
def test_list_flavors():
fl = cs.flavors.list()
cs.assert_called('GET', '/flavors/detail')
[assert_isinstance(f, Flavor) for f in fl]
def test_list_flavors_undetailed():
fl = cs.flavors.list(detailed=False)
cs.assert_called('GET', '/flavors')
[assert_isinstance(f, Flavor) for f in fl]
def test_get_flavor_details():
f = cs.flavors.get(1)
cs.assert_called('GET', '/flavors/1')
assert_isinstance(f, Flavor)
assert_equal(f.ram, 256)
assert_equal(f.disk, 10)
def test_find():
f = cs.flavors.find(ram=256)
cs.assert_called('GET', '/flavors/detail')
assert_equal(f.name, '256 MB Server')
f = cs.flavors.find(disk=20)
assert_equal(f.name, '512 MB Server')
assert_raises(NotFound, cs.flavors.find, disk=12345)

View File

@ -1,47 +0,0 @@
from novaclient import Image
from fakeserver import FakeServer
from utils import assert_isinstance
from nose.tools import assert_equal
cs = FakeServer()
def test_list_images():
il = cs.images.list()
cs.assert_called('GET', '/images/detail')
[assert_isinstance(i, Image) for i in il]
def test_list_images_undetailed():
il = cs.images.list(detailed=False)
cs.assert_called('GET', '/images')
[assert_isinstance(i, Image) for i in il]
def test_get_image_details():
i = cs.images.get(1)
cs.assert_called('GET', '/images/1')
assert_isinstance(i, Image)
assert_equal(i.id, 1)
assert_equal(i.name, 'CentOS 5.2')
def test_create_image():
i = cs.images.create(server=1234, name="Just in case")
cs.assert_called('POST', '/images')
assert_isinstance(i, Image)
def test_delete_image():
cs.images.delete(1)
cs.assert_called('DELETE', '/images/1')
def test_find():
i = cs.images.find(name="CentOS 5.2")
assert_equal(i.id, 1)
cs.assert_called('GET', '/images/detail')
iml = cs.images.findall(status='SAVING')
assert_equal(len(iml), 1)
assert_equal(iml[0].name, 'My Server Backup')

View File

@ -1,48 +0,0 @@
from novaclient import IPGroup
from fakeserver import FakeServer
from utils import assert_isinstance
from nose.tools import assert_equal
cs = FakeServer()
def test_list_ipgroups():
ipl = cs.ipgroups.list()
cs.assert_called('GET', '/shared_ip_groups/detail')
[assert_isinstance(ipg, IPGroup) for ipg in ipl]
def test_list_ipgroups_undetailed():
ipl = cs.ipgroups.list(detailed=False)
cs.assert_called('GET', '/shared_ip_groups')
[assert_isinstance(ipg, IPGroup) for ipg in ipl]
def test_get_ipgroup():
ipg = cs.ipgroups.get(1)
cs.assert_called('GET', '/shared_ip_groups/1')
assert_isinstance(ipg, IPGroup)
def test_create_ipgroup():
ipg = cs.ipgroups.create("My group", 1234)
cs.assert_called('POST', '/shared_ip_groups')
assert_isinstance(ipg, IPGroup)
def test_delete_ipgroup():
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_find():
ipg = cs.ipgroups.find(name='group1')
cs.assert_called('GET', '/shared_ip_groups/detail')
assert_equal(ipg.name, 'group1')
ipgl = cs.ipgroups.findall(id=1)
assert_equal(ipgl, [IPGroup(None, {'id': 1})])

View File

@ -1,174 +0,0 @@
import StringIO
from nose.tools import assert_equal
from fakeserver import FakeServer
from utils import assert_isinstance
from novaclient import Server
cs = FakeServer()
def test_list_servers():
sl = cs.servers.list()
cs.assert_called('GET', '/servers/detail')
[assert_isinstance(s, Server) for s in sl]
def test_list_servers_undetailed():
sl = cs.servers.list(detailed=False)
cs.assert_called('GET', '/servers')
[assert_isinstance(s, Server) for s in sl]
def test_get_server_details():
s = cs.servers.get(1234)
cs.assert_called('GET', '/servers/1234')
assert_isinstance(s, Server)
assert_equal(s.id, 1234)
assert_equal(s.status, 'BUILD')
def test_create_server():
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')
assert_isinstance(s, Server)
def test_update_server():
s = cs.servers.get(1234)
# 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()
# 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')
def test_delete_server():
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')
def test_find():
s = cs.servers.find(name='sample-server')
cs.assert_called('GET', '/servers/detail')
assert_equal(s.name, 'sample-server')
# Find with multiple results arbitraility returns the first item
s = cs.servers.find(flavorId=1)
sl = cs.servers.findall(flavorId=1)
assert_equal(sl[0], s)
assert_equal([s.id for s in sl], [1234, 5678])
def test_share_ip():
s = cs.servers.get(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_unshare_ip():
s = cs.servers.get(1234)
# 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_reboot_server():
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_rebuild_server():
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():
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():
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_revert_resized_server():
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')
def test_migrate_server():
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():
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_remove_fixed_ip():
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')

0
tests/v1_0/__init__.py Normal file
View File

View File

@ -6,21 +6,29 @@ wrong the tests might fail. I've indicated in comments the places where actual
behavior differs from the spec.
"""
import httplib2
from __future__ import absolute_import
import urlparse
import urllib
from nose.tools import assert_equal
from novaclient import OpenStack
from novaclient.client import OpenStackClient
from utils import fail, assert_in, assert_not_in, assert_has_keys
import httplib2
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
class FakeServer(OpenStack):
def assert_equal(value_one, value_two):
assert value_one == value_two
class FakeClient(Client):
def __init__(self, username=None, password=None, project_id=None,
auth_url=None):
super(FakeServer, self).__init__('username', 'apikey',
super(FakeClient, self).__init__('username', 'apikey',
'project_id', 'auth_url')
self.client = FakeClient()
self.client = FakeHTTPClient()
def assert_called(self, method, url, body=None):
"""
@ -56,7 +64,7 @@ class FakeServer(OpenStack):
found = True
break
assert found, 'Expected %s %s; got %s' % \
assert found, 'Expected %s; got %s' % \
(expected, self.client.callstack)
if body is not None:
assert_equal(entry[2], body)
@ -67,7 +75,7 @@ class FakeServer(OpenStack):
pass
class FakeClient(OpenStackClient):
class FakeHTTPClient(HTTPClient):
def __init__(self):
self.username = 'username'
self.apikey = 'apikey'
@ -412,7 +420,7 @@ class FakeClient(OpenStackClient):
def get_zones_detail(self, **kw):
return (200, {'zones': [
{'id': 1, 'api_url': 'http://foo.com', 'username': 'bob',
{'id': 1, 'api_url': 'http://foo.com', 'username': 'bob',
'password': 'qwerty'},
{'id': 2, 'api_url': 'http://foo.com', 'username': 'alice',
'password': 'password'}

View File

@ -1,14 +1,13 @@
from __future__ import absolute_import
import StringIO
from nose.tools import assert_equal
from .fakes import FakeClient
from fakeserver import FakeServer
from novaclient import Account
cs = FakeServer()
os = FakeClient()
def test_instance_creation_for_account():
s = cs.accounts.create_instance_for(
s = os.accounts.create_instance_for(
account_id='test_account',
name="My server",
image=1,
@ -19,4 +18,4 @@ def test_instance_creation_for_account():
'/etc/passwd': 'some data', # a file
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
})
cs.assert_called('POST', '/accounts/test_account/create_instance')
os.assert_called('POST', '/accounts/test_account/create_instance')

View File

@ -1,11 +1,13 @@
import mock
import novaclient
import httplib2
from nose.tools import assert_raises, assert_equal
import novaclient.v1_0
from novaclient.v1_0 import exceptions
def test_authenticate_success():
cs = novaclient.OpenStack("username", "apikey", "project_id")
cs = novaclient.v1_0.Client("username", "apikey", "project_id")
auth_response = httplib2.Response({
'status': 204,
'x-server-management-url':
@ -32,19 +34,19 @@ def test_authenticate_success():
def test_authenticate_failure():
cs = novaclient.OpenStack("username", "apikey", "project_id")
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():
assert_raises(novaclient.Unauthorized, cs.client.authenticate)
assert_raises(exceptions.Unauthorized, cs.client.authenticate)
test_auth_call()
def test_auth_automatic():
client = novaclient.OpenStack("username", "apikey", "project_id").client
client = novaclient.v1_0.Client("username", "apikey", "project_id").client
client.management_url = ''
mock_request = mock.Mock(return_value=(None, None))
@ -59,7 +61,7 @@ def test_auth_automatic():
def test_auth_manual():
cs = novaclient.OpenStack("username", "apikey", "project_id")
cs = novaclient.v1_0.Client("username", "apikey", "project_id")
@mock.patch.object(cs.client, 'authenticate')
def test_auth_call(m):

View File

@ -0,0 +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()
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')
def test_create_update_backup_schedule():
s = os.servers.get(1234)
# 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')
# and via instance
s.backup_schedule.update(enabled=False)
os.assert_called('POST', '/servers/1234/backup_schedule')
def test_delete_backup_schedule():
s = os.servers.get(1234)
# 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')
# and via instance
s.backup_schedule.delete()
os.assert_called('DELETE', '/servers/1234/backup_schedule')

View File

@ -1,59 +1,61 @@
from __future__ import absolute_import
import mock
import novaclient.base
from novaclient import Flavor
from novaclient.exceptions import NotFound
from novaclient.base import Resource
from nose.tools import assert_equal, assert_not_equal, assert_raises
from fakeserver import FakeServer
cs = FakeServer()
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 = Resource(None, dict(foo="bar", baz="spam"))
r = base.Resource(None, dict(foo="bar", baz="spam"))
assert_equal(repr(r), "<Resource baz=spam, foo=bar>")
def test_getid():
assert_equal(novaclient.base.getid(4), 4)
assert_equal(base.getid(4), 4)
class O(object):
id = 4
assert_equal(novaclient.base.getid(O), 4)
assert_equal(base.getid(O), 4)
def test_resource_lazy_getattr():
f = Flavor(cs.flavors, {'id': 1})
f = flavors.Flavor(os.flavors, {'id': 1})
assert_equal(f.name, '256 MB Server')
cs.assert_called('GET', '/flavors/1')
os.assert_called('GET', '/flavors/1')
# Missing stuff still fails after a second get
assert_raises(AttributeError, getattr, f, 'blahblah')
cs.assert_called('GET', '/flavors/1')
os.assert_called('GET', '/flavors/1')
def test_eq():
# Two resources of the same type with the same id: equal
r1 = Resource(None, {'id': 1, 'name': 'hi'})
r2 = Resource(None, {'id': 1, 'name': 'hello'})
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 = Resource(None, {'id': 1})
r2 = Flavor(None, {'id': 1})
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 = Resource(None, {'name': 'joe', 'age': 12})
r2 = Resource(None, {'name': 'joe', 'age': 12})
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.
cs.flavors.findall(vegetable='carrot')
os.flavors.findall(vegetable='carrot')
# However, find() should raise an error
assert_raises(NotFound, cs.flavors.find, vegetable='carrot')
assert_raises(exceptions.NotFound, os.flavors.find, vegetable='carrot')

View File

@ -1,6 +1,7 @@
import mock
import httplib2
from novaclient.client import OpenStackClient
from novaclient.v1_0 import client
from nose.tools import assert_equal
fake_response = httplib2.Response({"status": 200})
@ -8,15 +9,15 @@ fake_body = '{"hi": "there"}'
mock_request = mock.Mock(return_value=(fake_response, fake_body))
def client():
cl = OpenStackClient("username", "apikey", "project_id", "auth_test")
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 = client()
cl = get_client()
@mock.patch.object(httplib2.Http, "request", mock_request)
@mock.patch('time.time', mock.Mock(return_value=1234))
@ -34,7 +35,7 @@ def test_get():
def test_post():
cl = client()
cl = get_client()
@mock.patch.object(httplib2.Http, "request", mock_request)
def test_post_call():

View File

@ -0,0 +1,42 @@
from __future__ import absolute_import
from nose.tools import assert_raises, assert_equal
from novaclient.v1_0 import flavors
from novaclient.v1_0 import exceptions
from .fakes import FakeClient
from .utils import assert_isinstance
os = FakeClient()
def test_list_flavors():
fl = os.flavors.list()
os.assert_called('GET', '/flavors/detail')
[assert_isinstance(f, flavors.Flavor) for f in fl]
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]
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_find():
f = os.flavors.find(ram=256)
os.assert_called('GET', '/flavors/detail')
assert_equal(f.name, '256 MB Server')
f = os.flavors.find(disk=20)
assert_equal(f.name, '512 MB Server')
assert_raises(exceptions.NotFound, os.flavors.find, disk=12345)

51
tests/v1_0/test_images.py Normal file
View File

@ -0,0 +1,51 @@
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()
def test_list_images():
il = os.images.list()
os.assert_called('GET', '/images/detail')
[assert_isinstance(i, images.Image) for i in il]
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]
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_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():
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')

View File

@ -0,0 +1,52 @@
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()
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]
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]
def test_get_ipgroup():
ipg = os.ipgroups.get(1)
os.assert_called('GET', '/shared_ip_groups/1')
assert_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_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})])

180
tests/v1_0/test_servers.py Normal file
View File

@ -0,0 +1,180 @@
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
os = FakeClient()
def test_list_servers():
sl = os.servers.list()
os.assert_called('GET', '/servers/detail')
[assert_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_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():
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)
def test_update_server():
s = os.servers.get(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')
# Silly, but not an error
s.update()
# 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')
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_find():
s = os.servers.find(name='sample-server')
os.assert_called('GET', '/servers/detail')
assert_equal(s.name, 'sample-server')
# 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])
def test_share_ip():
s = os.servers.get(1234)
# 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')
# 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_unshare_ip():
s = os.servers.get(1234)
# Unshare via instance
s.unshare_ip('1.2.3.4')
os.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
# 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_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')

View File

@ -1,10 +1,15 @@
from __future__ import absolute_import
import os
import mock
import httplib2
from nose.tools import assert_raises, assert_equal
from novaclient.shell import OpenStackShell, CommandError
from fakeserver import FakeServer
from utils import assert_in
from novaclient.v1_0.shell import OpenStackShell, CommandError
from .fakes import FakeClient
from .utils import assert_in
# Patch os.environ to avoid required auth info.
@ -21,7 +26,7 @@ def setup():
# of asserting that certain API calls were made.
global shell, _shell, assert_called, assert_called_anytime
_shell = OpenStackShell()
_shell._api_class = FakeServer
_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)
@ -330,7 +335,7 @@ 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',
{'zone': {'api_url': 'http://zzz', 'username': 'frank',
'password': 'xxx',
'weight_offset': '0.0', 'weight_scale': '1.0'}}
)

View File

@ -1,28 +1,33 @@
import StringIO
from nose.tools import assert_equal
from fakeserver import FakeServer
from utils import assert_isinstance
from novaclient import Zone
from __future__ import absolute_import
os = FakeServer()
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()
def test_list_zones():
sl = os.zones.list()
os.assert_called('GET', '/zones/detail')
[assert_isinstance(s, Zone) for s in sl]
[assert_isinstance(s, zones.Zone) for s in sl]
def test_list_zones_undetailed():
sl = os.zones.list(detailed=False)
os.assert_called('GET', '/zones')
[assert_isinstance(s, Zone) for s in sl]
[assert_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, Zone)
assert_isinstance(s, zones.Zone)
assert_equal(s.id, 1)
assert_equal(s.api_url, 'http://foo.com')
@ -31,7 +36,7 @@ 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, Zone)
assert_isinstance(s, zones.Zone)
def test_update_zone():

0
tests/v1_1/__init__.py Normal file
View File

496
tests/v1_1/fakes.py Normal file
View File

@ -0,0 +1,496 @@
"""
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
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
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(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):
self.username = 'username'
self.apikey = 'apikey'
self.auth_url = 'auth_url'
self.callstack = []
def _cs_request(self, url, method, **kwargs):
# Check that certain things are called correctly
if method in ['GET', 'DELETE']:
assert_not_in('body', kwargs)
elif method in ['PUT', 'POST']:
assert_in('body', 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))
# Note the call
self.callstack.append((method, url, kwargs.get('body', None)))
status, body = getattr(self, callback)(**kwargs)
return httplib2.Response({"status": status}), body
def _munge_get_url(self, url):
return url
#
# Limits
#
def get_limits(self, **kw):
return (200, {"limits": {
"rate": [
{
"verb": "POST",
"URI": "*",
"regex": ".*",
"value": 10,
"remaining": 2,
"unit": "MINUTE",
"resetTime": 1244425439
},
{
"verb": "POST",
"URI": "*/servers",
"regex": "^/servers",
"value": 50,
"remaining": 49,
"unit": "DAY", "resetTime": 1244511839
},
{
"verb": "PUT",
"URI": "*",
"regex": ".*",
"value": 10,
"remaining": 2,
"unit": "MINUTE",
"resetTime": 1244425439
},
{
"verb": "GET",
"URI": "*changes-since*",
"regex": "changes-since",
"value": 3,
"remaining": 3,
"unit": "MINUTE",
"resetTime": 1244425439
},
{
"verb": "DELETE",
"URI": "*",
"regex": ".*",
"value": 100,
"remaining": 100,
"unit": "MINUTE",
"resetTime": 1244425439
}
],
"absolute": {
"maxTotalRAMSize": 51200,
"maxIPGroups": 50,
"maxIPGroupMembers": 25
}
}})
#
# Servers
#
def get_servers(self, **kw):
return (200, {"servers": [
{'id': 1234, 'name': 'sample-server'},
{'id': 5678, 'name': 'sample-server2'}
]})
def get_servers_detail(self, **kw):
return (200, {"servers": [
{
"id": 1234,
"name": "sample-server",
"image": {
"id": 2,
},
"flavor": {
"id": 1,
},
"hostId": "e4d909c290d0fb1ca068ffaddf22cbd0",
"status": "BUILD",
"progress": 60,
"addresses": {
"public": [{
"addr": "1.2.3.4",
"version": 4,
},
{
"addr": "5.6.7.8",
"version": 4,
}],
"private": [{
"addr": "10.11.12.13",
"version": 4,
}],
},
"metadata": {
"Server Label": "Web Head 1",
"Image Version": "2.1"
}
},
{
"id": 5678,
"name": "sample-server2",
"image": {
"id": 2,
},
"flavor": {
"id": 1,
},
"hostId": "9e107d9d372bb6826bd81d3542a419d6",
"status": "ACTIVE",
"addresses": {
"public": [{
"addr": "1.2.3.5",
"version": 4,
},
{
"addr": "5.6.7.9",
"version": 4,
}],
"private": [{
"addr": "10.13.12.13",
"version": 4,
}],
},
"metadata": {
"Server Label": "DB 1"
}
}
]})
def post_servers(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])
def get_servers_1234(self, **kw):
r = {'server': self.get_servers_detail()[1]['servers'][0]}
return (200, r)
def get_servers_5678(self, **kw):
r = {'server': self.get_servers_detail()[1]['servers'][1]}
return (200, r)
def put_servers_1234(self, body, **kw):
assert_equal(body.keys(), ['server'])
assert_has_keys(body['server'], optional=['name', 'adminPass'])
return (204, None)
def delete_servers_1234(self, **kw):
return (202, None)
#
# Server Addresses
#
def get_servers_1234_ips(self, **kw):
return (200, {'addresses':
self.get_servers_1234()[1]['server']['addresses']})
def get_servers_1234_ips_public(self, **kw):
return (200, {'public':
self.get_servers_1234_ips()[1]['addresses']['public']})
def get_servers_1234_ips_private(self, **kw):
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)
#
# Server actions
#
def post_servers_1234_action(self, body, **kw):
assert_equal(len(body.keys()), 1)
action = body.keys()[0]
if action == 'reboot':
assert_equal(body[action].keys(), ['type'])
assert_in(body[action]['type'], ['HARD', 'SOFT'])
elif action == 'rebuild':
assert_equal(body[action].keys(), ['imageRef'])
elif action == 'resize':
assert_equal(body[action].keys(), ['flavorRef'])
elif action == 'confirmResize':
assert_equal(body[action], 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"])
elif action == 'createImage':
assert_equal(body[action].keys(), ["name", "metadata"])
else:
fail("Unexpected server action: %s" % action)
return (202, None)
#
# Flavors
#
def get_flavors(self, **kw):
return (200, {'flavors': [
{'id': 1, 'name': '256 MB Server'},
{'id': 2, 'name': '512 MB Server'}
]})
def get_flavors_detail(self, **kw):
return (200, {'flavors': [
{'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10},
{'id': 2, 'name': '512 MB Server', 'ram': 512, 'disk': 20}
]})
def get_flavors_1(self, **kw):
return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][0]})
def get_flavors_2(self, **kw):
return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][1]})
#
# Images
#
def get_images(self, **kw):
return (200, {'images': [
{'id': 1, 'name': 'CentOS 5.2'},
{'id': 2, 'name': 'My Server Backup'}
]})
def get_images_detail(self, **kw):
return (200, {'images': [
{
'id': 1,
'name': 'CentOS 5.2',
"updated": "2010-10-10T12:00:00Z",
"created": "2010-08-10T12:00:00Z",
"status": "ACTIVE"
},
{
"id": 743,
"name": "My Server Backup",
"serverId": 12,
"updated": "2010-10-10T12:00:00Z",
"created": "2010-08-10T12:00:00Z",
"status": "SAVING",
"progress": 80
}
]})
def get_images_1(self, **kw):
return (200, {'image': self.get_images_detail()[1]['images'][0]})
def get_images_2(self, **kw):
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'])
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])

61
tests/v1_1/test_base.py Normal file
View File

@ -0,0 +1,61 @@
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')

52
tests/v1_1/test_client.py Normal file
View File

@ -0,0 +1,52 @@
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

@ -0,0 +1,42 @@
from __future__ import absolute_import
from nose.tools import assert_raises, assert_equal
from novaclient.v1_1 import flavors
from novaclient.v1_1 import exceptions
from .fakes import FakeClient
from .utils import assert_isinstance
os = FakeClient()
def test_list_flavors():
fl = os.flavors.list()
os.assert_called('GET', '/flavors/detail')
[assert_isinstance(f, flavors.Flavor) for f in fl]
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]
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_find():
f = os.flavors.find(ram=256)
os.assert_called('GET', '/flavors/detail')
assert_equal(f.name, '256 MB Server')
f = os.flavors.find(disk=20)
assert_equal(f.name, '512 MB Server')
assert_raises(exceptions.NotFound, os.flavors.find, disk=12345)

51
tests/v1_1/test_images.py Normal file
View File

@ -0,0 +1,51 @@
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()
def test_list_images():
il = os.images.list()
os.assert_called('GET', '/images/detail')
[assert_isinstance(i, images.Image) for i in il]
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]
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_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():
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')

125
tests/v1_1/test_servers.py Normal file
View File

@ -0,0 +1,125 @@
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
os = FakeClient()
def test_list_servers():
sl = os.servers.list()
os.assert_called('GET', '/servers/detail')
[assert_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_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():
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)
def test_update_server():
s = os.servers.get(1234)
# Update via instance
s.update(name='hi')
os.assert_called('PUT', '/servers/1234')
# Silly, but not an error
s.update()
# Update via manager
os.servers.update(s, name='hi')
os.assert_called('PUT', '/servers/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')
def test_find():
s = os.servers.find(name='sample-server')
os.assert_called('GET', '/servers/detail')
assert_equal(s.name, 'sample-server')
# 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')

234
tests/v1_1/test_shell.py Normal file
View File

@ -0,0 +1,234 @@
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
# 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()
# 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}
]}
}
)
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 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')
assert_called(
'POST', '/servers',
{'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
'personality': [{
'path': '/root/.ssh/authorized_keys2',
'contents': ('SSHKEY').encode('base64')},
]}
}
)
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': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '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_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_create_image():
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_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': {'imageRef': 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': {'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

1
tests/v1_1/testfile.txt Normal file
View File

@ -0,0 +1 @@
OH HAI!

29
tests/v1_1/utils.py Normal file
View File

@ -0,0 +1,29 @@
from nose.tools import ok_
def fail(msg):
raise AssertionError(msg)
def assert_in(thing, seq, msg=None):
msg = msg or "'%s' not found in %s" % (thing, seq)
ok_(thing in seq, msg)
def assert_not_in(thing, seq, msg=None):
msg = msg or "unexpected '%s' found in %s" % (thing, seq)
ok_(thing not in seq, msg)
def assert_has_keys(dict, required=[], optional=[]):
keys = dict.keys()
for k in required:
assert_in(k, keys, "required key %s missing from %s" % (k, dict))
allowed_keys = set(required) | set(optional)
extra_keys = set(keys).difference(set(required + optional))
if extra_keys:
fail("found unexpected keys: %s" % list(extra_keys))
def assert_isinstance(thing, kls):
ok_(isinstance(thing, kls), "%s is not an instance of %s" % (thing, kls))