Tests working again...merged in some work we did earlier.
This commit is contained in:
parent
f8496672cc
commit
454daa2cba
@ -1,17 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
__version__ = '2.5'
|
235
novaclient/base.py
Normal file
235
novaclient/base.py
Normal file
@ -0,0 +1,235 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
from novaclient import exceptions
|
||||
|
||||
|
||||
# Python 2.4 compat
|
||||
try:
|
||||
all
|
||||
except NameError:
|
||||
def all(iterable):
|
||||
return True not in (not x for x in iterable)
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""
|
||||
Abstracts the common pattern of allowing both an object or an object's ID
|
||||
(UUID) as a parameter when dealing with relationships.
|
||||
"""
|
||||
|
||||
# Try to return the object's UUID first, if we have a UUID.
|
||||
try:
|
||||
if obj.uuid:
|
||||
return obj.uuid
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
class Manager(object):
|
||||
"""
|
||||
Managers interact with a particular type of API (servers, flavors, images,
|
||||
etc.) and provide CRUD operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
def _list(self, url, response_key, obj_class=None, body=None):
|
||||
resp = None
|
||||
if body:
|
||||
resp, body = self.api.client.post(url, body=body)
|
||||
else:
|
||||
resp, body = self.api.client.get(url)
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
return [obj_class(self, res)
|
||||
for res in body[response_key] if res]
|
||||
|
||||
def _get(self, url, response_key):
|
||||
resp, body = self.api.client.get(url)
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _create(self, url, body, response_key, return_raw=False):
|
||||
resp, body = self.api.client.post(url, body=body)
|
||||
if return_raw:
|
||||
return body[response_key]
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _delete(self, url):
|
||||
resp, body = self.api.client.delete(url)
|
||||
|
||||
def _update(self, url, body):
|
||||
resp, body = self.api.client.put(url, body=body)
|
||||
|
||||
|
||||
class ManagerWithFind(Manager):
|
||||
"""
|
||||
Like a `Manager`, but with additional `find()`/`findall()` methods.
|
||||
"""
|
||||
def find(self, **kwargs):
|
||||
"""
|
||||
Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
rl = self.findall(**kwargs)
|
||||
try:
|
||||
return rl[0]
|
||||
except IndexError:
|
||||
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
|
||||
raise exceptions.NotFound(404, msg)
|
||||
|
||||
def findall(self, **kwargs):
|
||||
"""
|
||||
Find all items with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
found = []
|
||||
searches = kwargs.items()
|
||||
|
||||
for obj in self.list():
|
||||
try:
|
||||
if all(getattr(obj, attr) == value
|
||||
for (attr, value) in searches):
|
||||
found.append(obj)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
return found
|
||||
|
||||
|
||||
class BootingManagerWithFind(ManagerWithFind):
|
||||
"""Like a `ManagerWithFind`, but has the ability to boot servers."""
|
||||
def _boot(self, resource_url, response_key, name, image, flavor,
|
||||
ipgroup=None, meta=None, files=None, zone_blob=None,
|
||||
reservation_id=None, return_raw=False, min_count=None,
|
||||
max_count=None):
|
||||
"""
|
||||
Create (boot) a new server.
|
||||
|
||||
:param name: Something to name the server.
|
||||
:param image: The :class:`Image` to boot with.
|
||||
:param flavor: The :class:`Flavor` to boot onto.
|
||||
:param ipgroup: An initial :class:`IPGroup` for this server.
|
||||
:param meta: A dict of arbitrary key/value metadata to store for this
|
||||
server. A maximum of five entries is allowed, and both
|
||||
keys and values must be 255 characters or less.
|
||||
:param files: A dict of files to overrwrite on the server upon boot.
|
||||
Keys are file names (i.e. ``/etc/passwd``) and values
|
||||
are the file contents (either as a string or as a
|
||||
file-like object). A maximum of five entries is allowed,
|
||||
and each file must be 10k or less.
|
||||
:param zone_blob: a single (encrypted) string which is used internally
|
||||
by Nova for routing between Zones. Users cannot populate
|
||||
this field.
|
||||
:param reservation_id: a UUID for the set of servers being requested.
|
||||
:param return_raw: If True, don't try to coearse the result into
|
||||
a Resource object.
|
||||
"""
|
||||
body = {"server": {
|
||||
"name": name,
|
||||
"imageId": getid(image),
|
||||
"flavorId": getid(flavor),
|
||||
}}
|
||||
if ipgroup:
|
||||
body["server"]["sharedIpGroupId"] = getid(ipgroup)
|
||||
if meta:
|
||||
body["server"]["metadata"] = meta
|
||||
if reservation_id:
|
||||
body["server"]["reservation_id"] = reservation_id
|
||||
if zone_blob:
|
||||
body["server"]["zone_blob"] = zone_blob
|
||||
|
||||
if not min_count:
|
||||
min_count = 1
|
||||
if not max_count:
|
||||
max_count = min_count
|
||||
body["server"]["min_count"] = min_count
|
||||
body["server"]["max_count"] = max_count
|
||||
|
||||
# Files are a slight bit tricky. They're passed in a "personality"
|
||||
# list to the POST. Each item is a dict giving a file name and the
|
||||
# base64-encoded contents of the file. We want to allow passing
|
||||
# either an open file *or* some contents as files here.
|
||||
if files:
|
||||
personality = body['server']['personality'] = []
|
||||
for filepath, file_or_string in files.items():
|
||||
if hasattr(file_or_string, 'read'):
|
||||
data = file_or_string.read()
|
||||
else:
|
||||
data = file_or_string
|
||||
personality.append({
|
||||
'path': filepath,
|
||||
'contents': data.encode('base64'),
|
||||
})
|
||||
|
||||
return self._create(resource_url, body, response_key,
|
||||
return_raw=return_raw)
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""
|
||||
A resource represents a particular instance of an object (server, flavor,
|
||||
etc). This is pretty much just a bag for attributes.
|
||||
"""
|
||||
def __init__(self, manager, info):
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in info.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
def __getattr__(self, k):
|
||||
self.get()
|
||||
if k not in self.__dict__:
|
||||
raise AttributeError(k)
|
||||
else:
|
||||
return self.__dict__[k]
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
|
||||
k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
return "<%s %s>" % (self.__class__.__name__, info)
|
||||
|
||||
def get(self):
|
||||
new = self.manager.get(self.id)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||
return self.id == other.id
|
||||
return self._info == other._info
|
157
novaclient/client.py
Normal file
157
novaclient/client.py
Normal file
@ -0,0 +1,157 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
"""
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
import time
|
||||
import urlparse
|
||||
import urllib
|
||||
import httplib2
|
||||
import logging
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
# Python 2.5 compat fix
|
||||
if not hasattr(urlparse, 'parse_qsl'):
|
||||
import cgi
|
||||
urlparse.parse_qsl = cgi.parse_qsl
|
||||
|
||||
|
||||
from novaclient import exceptions
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HTTPClient(httplib2.Http):
|
||||
|
||||
USER_AGENT = 'python-novaclient'
|
||||
|
||||
def __init__(self, user, apikey, projectid, auth_url, timeout=None):
|
||||
super(HTTPClient, self).__init__(timeout=timeout)
|
||||
self.user = user
|
||||
self.apikey = apikey
|
||||
self.projectid = projectid
|
||||
self.auth_url = auth_url
|
||||
self.version = 'v1.0'
|
||||
|
||||
self.management_url = None
|
||||
self.auth_token = None
|
||||
|
||||
# httplib2 overrides
|
||||
self.force_exception_to_status_code = True
|
||||
|
||||
def http_log(self, args, kwargs, resp, body):
|
||||
if not _logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
|
||||
string_parts = ['curl -i']
|
||||
for element in args:
|
||||
if element in ('GET', 'POST'):
|
||||
string_parts.append(' -X %s' % element)
|
||||
else:
|
||||
string_parts.append(' %s' % element)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
_logger.debug("REQ: %s\n" % "".join(string_parts))
|
||||
_logger.debug("RESP:%s %s\n", resp, body)
|
||||
|
||||
def request(self, *args, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||
if 'body' in kwargs:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['body'] = json.dumps(kwargs['body'])
|
||||
|
||||
resp, body = super(HTTPClient, self).request(*args, **kwargs)
|
||||
|
||||
self.http_log(args, kwargs, resp, body)
|
||||
|
||||
if body:
|
||||
try:
|
||||
body = json.loads(body)
|
||||
except ValueError, e:
|
||||
pass
|
||||
else:
|
||||
body = None
|
||||
|
||||
if resp.status in (400, 401, 403, 404, 408, 413, 500, 501):
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
return resp, body
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
if not self.management_url:
|
||||
self.authenticate()
|
||||
|
||||
# Perform the request once. If we get a 401 back then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
try:
|
||||
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
|
||||
if self.projectid:
|
||||
kwargs['headers']['X-Auth-Project-Id'] = self.projectid
|
||||
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.Unauthorized, ex:
|
||||
try:
|
||||
self.authenticate()
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.Unauthorized:
|
||||
raise ex
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
url = self._munge_get_url(url)
|
||||
return self._cs_request(url, 'GET', **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._cs_request(url, 'POST', **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._cs_request(url, 'PUT', **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self._cs_request(url, 'DELETE', **kwargs)
|
||||
|
||||
def authenticate(self):
|
||||
scheme, netloc, path, query, frag = urlparse.urlsplit(
|
||||
self.auth_url)
|
||||
path_parts = path.split('/')
|
||||
for part in path_parts:
|
||||
if len(part) > 0 and part[0] == 'v':
|
||||
self.version = part
|
||||
break
|
||||
|
||||
headers = {'X-Auth-User': self.user,
|
||||
'X-Auth-Key': self.apikey}
|
||||
if self.projectid:
|
||||
headers['X-Auth-Project-Id'] = self.projectid
|
||||
resp, body = self.request(self.auth_url, 'GET', headers=headers)
|
||||
self.management_url = resp['x-server-management-url']
|
||||
|
||||
self.auth_token = resp['x-auth-token']
|
||||
|
||||
def _munge_get_url(self, url):
|
||||
"""
|
||||
Munge GET URLs to always return uncached content.
|
||||
|
||||
The OpenStack Compute API caches data *very* agressively and doesn't
|
||||
respect cache headers. To avoid stale data, then, we append a little
|
||||
bit of nonsense onto GET parameters; this appears to force the data not
|
||||
to be cached.
|
||||
"""
|
||||
scheme, netloc, path, query, frag = urlparse.urlsplit(url)
|
||||
query = urlparse.parse_qsl(query)
|
||||
query.append(('fresh', str(time.time())))
|
||||
query = urllib.urlencode(query)
|
||||
return urlparse.urlunsplit((scheme, netloc, path, query, frag))
|
@ -3,7 +3,12 @@
|
||||
Exception definitions.
|
||||
"""
|
||||
|
||||
class OpenStackException(Exception):
|
||||
|
||||
class CommandError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""
|
||||
The base exception class for all exceptions this library raises.
|
||||
"""
|
||||
@ -16,7 +21,7 @@ class OpenStackException(Exception):
|
||||
return "%s (HTTP %s)" % (self.message, self.code)
|
||||
|
||||
|
||||
class BadRequest(OpenStackException):
|
||||
class BadRequest(ClientException):
|
||||
"""
|
||||
HTTP 400 - Bad request: you sent some malformed data.
|
||||
"""
|
||||
@ -24,7 +29,7 @@ class BadRequest(OpenStackException):
|
||||
message = "Bad request"
|
||||
|
||||
|
||||
class Unauthorized(OpenStackException):
|
||||
class Unauthorized(ClientException):
|
||||
"""
|
||||
HTTP 401 - Unauthorized: bad credentials.
|
||||
"""
|
||||
@ -32,7 +37,7 @@ class Unauthorized(OpenStackException):
|
||||
message = "Unauthorized"
|
||||
|
||||
|
||||
class Forbidden(OpenStackException):
|
||||
class Forbidden(ClientException):
|
||||
"""
|
||||
HTTP 403 - Forbidden: your credentials don't give you access to this
|
||||
resource.
|
||||
@ -41,7 +46,7 @@ class Forbidden(OpenStackException):
|
||||
message = "Forbidden"
|
||||
|
||||
|
||||
class NotFound(OpenStackException):
|
||||
class NotFound(ClientException):
|
||||
"""
|
||||
HTTP 404 - Not found
|
||||
"""
|
||||
@ -49,7 +54,7 @@ class NotFound(OpenStackException):
|
||||
message = "Not found"
|
||||
|
||||
|
||||
class OverLimit(OpenStackException):
|
||||
class OverLimit(ClientException):
|
||||
"""
|
||||
HTTP 413 - Over limit: you're over the API limits for this time period.
|
||||
"""
|
||||
@ -58,7 +63,7 @@ class OverLimit(OpenStackException):
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HTTPNotImplemented(OpenStackException):
|
||||
class HTTPNotImplemented(ClientException):
|
||||
"""
|
||||
HTTP 501 - Not Implemented: the server does not support this operation.
|
||||
"""
|
||||
@ -69,7 +74,7 @@ class HTTPNotImplemented(OpenStackException):
|
||||
# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
|
||||
# so we can do this:
|
||||
# _code_map = dict((c.http_status, c)
|
||||
# for c in OpenStackException.__subclasses__())
|
||||
# for c in ClientException.__subclasses__())
|
||||
#
|
||||
# Instead, we have to hardcode it:
|
||||
_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
|
||||
@ -78,7 +83,7 @@ _code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
|
||||
|
||||
def from_response(response, body):
|
||||
"""
|
||||
Return an instance of an OpenStackException or subclass
|
||||
Return an instance of an ClientException or subclass
|
||||
based on an httplib2 response.
|
||||
|
||||
Usage::
|
||||
@ -87,7 +92,7 @@ def from_response(response, body):
|
||||
if resp.status != 200:
|
||||
raise exception_from_response(resp, body)
|
||||
"""
|
||||
cls = _code_map.get(response.status, OpenStackException)
|
||||
cls = _code_map.get(response.status, ClientException)
|
||||
if body:
|
||||
message = "n/a"
|
||||
details = "n/a"
|
204
novaclient/shell.py
Normal file
204
novaclient/shell.py
Normal file
@ -0,0 +1,204 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Command-line interface to the OpenStack Nova API.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import httplib2
|
||||
import os
|
||||
import prettytable
|
||||
import sys
|
||||
|
||||
from novaclient import exceptions
|
||||
from novaclient import utils
|
||||
from novaclient.v1_0 import shell as shell_v1_0
|
||||
from novaclient.v1_1 import shell as shell_v1_1
|
||||
|
||||
|
||||
def env(e):
|
||||
return os.environ.get(e, '')
|
||||
|
||||
|
||||
class OpenStackComputeShell(object):
|
||||
|
||||
# Hook for the test suite to inject a fake server.
|
||||
_api_class = None
|
||||
|
||||
def get_base_parser(self):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='nova',
|
||||
description=__doc__.strip(),
|
||||
epilog='See "nova help COMMAND" '\
|
||||
'for help on a specific command.',
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter,
|
||||
)
|
||||
|
||||
# Global arguments
|
||||
parser.add_argument('-h', '--help',
|
||||
action='help',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--debug',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--username',
|
||||
default=env('NOVA_USERNAME'),
|
||||
help='Defaults to env[NOVA_USERNAME].')
|
||||
|
||||
parser.add_argument('--apikey',
|
||||
default=env('NOVA_API_KEY'),
|
||||
help='Defaults to env[NOVA_API_KEY].')
|
||||
|
||||
parser.add_argument('--projectid',
|
||||
default=env('NOVA_PROJECT_ID'),
|
||||
help='Defaults to env[NOVA_PROJECT_ID].')
|
||||
|
||||
parser.add_argument('--url',
|
||||
default=env('NOVA_URL'),
|
||||
help='Defaults to env[NOVA_URL].')
|
||||
|
||||
parser.add_argument('--version',
|
||||
default='1.1',
|
||||
help='Accepts 1.0 or 1.1, defaults to 1.1')
|
||||
|
||||
return parser
|
||||
|
||||
def get_subcommand_parser(self, version):
|
||||
parser = self.get_base_parser()
|
||||
|
||||
self.subcommands = {}
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
|
||||
actions_module = {
|
||||
'1.0': shell_v1_0,
|
||||
'1.1': shell_v1_1,
|
||||
}[version]
|
||||
|
||||
self._find_actions(subparsers, actions_module)
|
||||
self._find_actions(subparsers, self)
|
||||
|
||||
return parser
|
||||
|
||||
def _find_actions(self, subparsers, actions_module):
|
||||
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||
# I prefer to be hypen-separated instead of underscores.
|
||||
command = attr[3:].replace('_', '-')
|
||||
callback = getattr(actions_module, attr)
|
||||
desc = callback.__doc__ or ''
|
||||
help = desc.strip().split('\n')[0]
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(command,
|
||||
help=help,
|
||||
description=desc,
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter
|
||||
)
|
||||
subparser.add_argument('-h', '--help',
|
||||
action='help',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
self.subcommands[command] = subparser
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
def main(self, argv):
|
||||
# Parse args once to find version
|
||||
parser = self.get_base_parser()
|
||||
(options, args) = parser.parse_known_args(argv)
|
||||
|
||||
# build available subcommands based on version
|
||||
subcommand_parser = self.get_subcommand_parser(options.version)
|
||||
self.parser = subcommand_parser
|
||||
|
||||
# Parse args again and call whatever callback was selected
|
||||
args = subcommand_parser.parse_args(argv)
|
||||
|
||||
# Deal with global arguments
|
||||
if args.debug:
|
||||
httplib2.debuglevel = 1
|
||||
|
||||
# Short-circuit and deal with help right away.
|
||||
if args.func == self.do_help:
|
||||
self.do_help(args)
|
||||
return 0
|
||||
|
||||
user, apikey, projectid, url = args.username, args.apikey, \
|
||||
args.projectid, args.url
|
||||
|
||||
#FIXME(usrleon): Here should be restrict for project id same as
|
||||
# for username or apikey but for compatibility it is not.
|
||||
|
||||
if not user:
|
||||
raise exceptions.CommandError("You must provide a username, either via "
|
||||
"--username or via env[NOVA_USERNAME]")
|
||||
if not apikey:
|
||||
raise exceptions.CommandError("You must provide an API key, either via "
|
||||
"--apikey or via env[NOVA_API_KEY]")
|
||||
|
||||
self.cs = self.get_api_class()(user, apikey, projectid, url)
|
||||
try:
|
||||
self.cs.authenticate()
|
||||
except exceptions.Unauthorized:
|
||||
raise exceptions.CommandError("Invalid OpenStack Nova credentials.")
|
||||
|
||||
args.func(self.cs, args)
|
||||
|
||||
def get_api_class(self):
|
||||
return self._api_class or shell_v1_0.CLIENT_CLASS
|
||||
|
||||
@utils.arg('command', metavar='<subcommand>', nargs='?',
|
||||
help='Display help for <subcommand>')
|
||||
def do_help(self, args):
|
||||
"""
|
||||
Display help about this program or one of its subcommands.
|
||||
"""
|
||||
if args.command:
|
||||
if args.command in self.subcommands:
|
||||
self.subcommands[args.command].print_help()
|
||||
else:
|
||||
raise exceptions.CommandError("'%s' is not a valid subcommand." %
|
||||
args.command)
|
||||
else:
|
||||
self.parser.print_help()
|
||||
|
||||
|
||||
# I'm picky about my shell help.
|
||||
class OpenStackHelpFormatter(argparse.HelpFormatter):
|
||||
def start_section(self, heading):
|
||||
# Title-case the headings
|
||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||
super(OpenStackHelpFormatter, self).start_section(heading)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
OpenStackComputeShell().main(sys.argv[1:])
|
||||
|
||||
except Exception, e:
|
||||
if httplib2.debuglevel == 1:
|
||||
raise # dump stack.
|
||||
else:
|
||||
print >> sys.stderr, e
|
||||
sys.exit(1)
|
43
novaclient/utils.py
Normal file
43
novaclient/utils.py
Normal file
@ -0,0 +1,43 @@
|
||||
|
||||
import prettytable
|
||||
|
||||
|
||||
# Decorator for cli-args
|
||||
def arg(*args, **kwargs):
|
||||
def _decorator(func):
|
||||
# Because of the sematics of decorator composition if we just append
|
||||
# to the options list positional options will appear to be backwards.
|
||||
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
|
||||
return func
|
||||
return _decorator
|
||||
|
||||
|
||||
def pretty_choice_list(l):
|
||||
return ', '.join("'%s'" % i for i in l)
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters={}):
|
||||
pt = prettytable.PrettyTable([f for f in fields], caching=False)
|
||||
pt.aligns = ['l' for f in fields]
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
field_name = field.lower().replace(' ', '_')
|
||||
data = getattr(o, field_name, '')
|
||||
row.append(data)
|
||||
pt.add_row(row)
|
||||
|
||||
pt.printt(sortby=fields[0])
|
||||
|
||||
|
||||
def print_dict(d):
|
||||
pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
|
||||
pt.aligns = ['l', 'l']
|
||||
[pt.add_row(list(r)) for r in d.iteritems()]
|
||||
pt.printt(sortby='Property')
|
||||
|
||||
|
@ -1,74 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from novaclient.v1_0 import accounts
|
||||
from novaclient.v1_0 import backup_schedules
|
||||
from novaclient.v1_0 import client
|
||||
from novaclient.v1_0 import exceptions
|
||||
from novaclient.v1_0 import flavors
|
||||
from novaclient.v1_0 import images
|
||||
from novaclient.v1_0 import ipgroups
|
||||
from novaclient.v1_0 import servers
|
||||
from novaclient.v1_0 import zones
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""
|
||||
Top-level object to access the OpenStack Compute v1.0 API.
|
||||
|
||||
Create an instance with your creds::
|
||||
|
||||
>>> os = novaclient.v1_0.Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
|
||||
|
||||
Then call methods on its managers::
|
||||
|
||||
>>> os.servers.list()
|
||||
...
|
||||
>>> os.flavors.list()
|
||||
...
|
||||
|
||||
&c.
|
||||
"""
|
||||
|
||||
def __init__(self, username, apikey, projectid, auth_url=None, timeout=None):
|
||||
"""Initialize v1.0 Openstack Client."""
|
||||
self.backup_schedules = backup_schedules.BackupScheduleManager(self)
|
||||
self.flavors = flavors.FlavorManager(self)
|
||||
self.images = images.ImageManager(self)
|
||||
self.ipgroups = ipgroups.IPGroupManager(self)
|
||||
self.servers = servers.ServerManager(self)
|
||||
self.zones = zones.ZoneManager(self)
|
||||
self.accounts = accounts.AccountManager(self)
|
||||
|
||||
auth_url = auth_url or "https://auth.api.rackspacecloud.com/v1.0"
|
||||
|
||||
self.client = client.HTTPClient(username,
|
||||
apikey,
|
||||
projectid,
|
||||
auth_url,
|
||||
timeout=timeout)
|
||||
|
||||
def authenticate(self):
|
||||
"""
|
||||
Authenticate against the server.
|
||||
|
||||
Normally this is called automatically when you first access the API,
|
||||
but you can call this method to force authentication right now.
|
||||
|
||||
Returns on success; raises :exc:`novaclient.Unauthorized` if the
|
||||
credentials are wrong.
|
||||
"""
|
||||
self.client.authenticate()
|
@ -1,10 +1,13 @@
|
||||
from novaclient.v1_0 import base
|
||||
|
||||
from novaclient import base
|
||||
from novaclient.v1_0 import base as local_base
|
||||
|
||||
|
||||
class Account(base.Resource):
|
||||
pass
|
||||
|
||||
class AccountManager(base.BootingManagerWithFind):
|
||||
|
||||
class AccountManager(local_base.BootingManagerWithFind):
|
||||
resource_class = Account
|
||||
|
||||
def create_instance_for(self, account_id, name, image, flavor,
|
||||
|
@ -3,7 +3,8 @@
|
||||
Backup Schedule interface.
|
||||
"""
|
||||
|
||||
from novaclient.v1_0 import base
|
||||
from novaclient import base
|
||||
|
||||
|
||||
BACKUP_WEEKLY_DISABLED = 'DISABLED'
|
||||
BACKUP_WEEKLY_SUNDAY = 'SUNDAY'
|
||||
|
@ -19,7 +19,9 @@
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
from novaclient.v1_0 import exceptions
|
||||
from novaclient import base
|
||||
from novaclient import exceptions
|
||||
|
||||
|
||||
# Python 2.4 compat
|
||||
try:
|
||||
@ -29,103 +31,7 @@ except NameError:
|
||||
return True not in (not x for x in iterable)
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""
|
||||
Abstracts the common pattern of allowing both an object or an object's ID
|
||||
(UUID) as a parameter when dealing with relationships.
|
||||
"""
|
||||
|
||||
# Try to return the object's UUID first, if we have a UUID.
|
||||
try:
|
||||
if obj.uuid:
|
||||
return obj.uuid
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
class Manager(object):
|
||||
"""
|
||||
Managers interact with a particular type of API (servers, flavors, images,
|
||||
etc.) and provide CRUD operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
def _list(self, url, response_key, obj_class=None, body=None):
|
||||
resp = None
|
||||
if body:
|
||||
resp, body = self.api.client.post(url, body=body)
|
||||
else:
|
||||
resp, body = self.api.client.get(url)
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
return [obj_class(self, res)
|
||||
for res in body[response_key] if res]
|
||||
|
||||
def _get(self, url, response_key):
|
||||
resp, body = self.api.client.get(url)
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _create(self, url, body, response_key, return_raw=False):
|
||||
resp, body = self.api.client.post(url, body=body)
|
||||
if return_raw:
|
||||
return body[response_key]
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _delete(self, url):
|
||||
resp, body = self.api.client.delete(url)
|
||||
|
||||
def _update(self, url, body):
|
||||
resp, body = self.api.client.put(url, body=body)
|
||||
|
||||
|
||||
class ManagerWithFind(Manager):
|
||||
"""
|
||||
Like a `Manager`, but with additional `find()`/`findall()` methods.
|
||||
"""
|
||||
def find(self, **kwargs):
|
||||
"""
|
||||
Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
rl = self.findall(**kwargs)
|
||||
try:
|
||||
return rl[0]
|
||||
except IndexError:
|
||||
raise exceptions.NotFound(404, "No %s matching %s." %
|
||||
(self.resource_class.__name__, kwargs))
|
||||
|
||||
def findall(self, **kwargs):
|
||||
"""
|
||||
Find all items with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
found = []
|
||||
searches = kwargs.items()
|
||||
|
||||
for obj in self.list():
|
||||
try:
|
||||
if all(getattr(obj, attr) == value
|
||||
for (attr, value) in searches):
|
||||
found.append(obj)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
return found
|
||||
|
||||
|
||||
class BootingManagerWithFind(ManagerWithFind):
|
||||
class BootingManagerWithFind(base.ManagerWithFind):
|
||||
"""Like a `ManagerWithFind`, but has the ability to boot servers."""
|
||||
def _boot(self, resource_url, response_key, name, image, flavor,
|
||||
ipgroup=None, meta=None, files=None, zone_blob=None,
|
||||
@ -155,11 +61,11 @@ class BootingManagerWithFind(ManagerWithFind):
|
||||
"""
|
||||
body = {"server": {
|
||||
"name": name,
|
||||
"imageId": getid(image),
|
||||
"flavorId": getid(flavor),
|
||||
"imageId": base.getid(image),
|
||||
"flavorId": base.getid(flavor),
|
||||
}}
|
||||
if ipgroup:
|
||||
body["server"]["sharedIpGroupId"] = getid(ipgroup)
|
||||
body["server"]["sharedIpGroupId"] = base.getid(ipgroup)
|
||||
if meta:
|
||||
body["server"]["metadata"] = meta
|
||||
if reservation_id:
|
||||
@ -192,43 +98,3 @@ class BootingManagerWithFind(ManagerWithFind):
|
||||
|
||||
return self._create(resource_url, body, response_key,
|
||||
return_raw=return_raw)
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""
|
||||
A resource represents a particular instance of an object (server, flavor,
|
||||
etc). This is pretty much just a bag for attributes.
|
||||
"""
|
||||
def __init__(self, manager, info):
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in info.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
def __getattr__(self, k):
|
||||
self.get()
|
||||
if k not in self.__dict__:
|
||||
raise AttributeError(k)
|
||||
else:
|
||||
return self.__dict__[k]
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
|
||||
k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
return "<%s %s>" % (self.__class__.__name__, info)
|
||||
|
||||
def get(self):
|
||||
new = self.manager.get(self.id)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||
return self.id == other.id
|
||||
return self._info == other._info
|
||||
|
@ -1,154 +1,60 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
"""
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
import time
|
||||
import urlparse
|
||||
import urllib
|
||||
import httplib2
|
||||
import logging
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
from novaclient import client
|
||||
from novaclient.v1_0 import accounts
|
||||
from novaclient.v1_0 import backup_schedules
|
||||
from novaclient.v1_0 import flavors
|
||||
from novaclient.v1_0 import images
|
||||
from novaclient.v1_0 import ipgroups
|
||||
from novaclient.v1_0 import servers
|
||||
from novaclient.v1_0 import zones
|
||||
|
||||
# Python 2.5 compat fix
|
||||
if not hasattr(urlparse, 'parse_qsl'):
|
||||
import cgi
|
||||
urlparse.parse_qsl = cgi.parse_qsl
|
||||
|
||||
import novaclient
|
||||
from novaclient.v1_0 import exceptions
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
class Client(object):
|
||||
"""
|
||||
Top-level object to access the OpenStack Compute API.
|
||||
|
||||
class HTTPClient(httplib2.Http):
|
||||
Create an instance with your creds::
|
||||
|
||||
USER_AGENT = 'python-novaclient/%s' % novaclient.__version__
|
||||
>>> client = Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
|
||||
|
||||
def __init__(self, user, apikey, projectid, auth_url, timeout=None):
|
||||
super(HTTPClient, self).__init__(timeout=timeout)
|
||||
self.user = user
|
||||
self.apikey = apikey
|
||||
self.projectid = projectid
|
||||
self.auth_url = auth_url
|
||||
self.version = 'v1.0'
|
||||
Then call methods on its managers::
|
||||
|
||||
self.management_url = None
|
||||
self.auth_token = None
|
||||
>>> client.servers.list()
|
||||
...
|
||||
>>> client.flavors.list()
|
||||
...
|
||||
|
||||
# httplib2 overrides
|
||||
self.force_exception_to_status_code = True
|
||||
"""
|
||||
|
||||
def http_log(self, args, kwargs, resp, body):
|
||||
if not _logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
def __init__(self, username, api_key, project_id, auth_url=None,
|
||||
timeout=None):
|
||||
|
||||
string_parts = ['curl -i']
|
||||
for element in args:
|
||||
if element in ('GET','POST'):
|
||||
string_parts.append(' -X %s' % element)
|
||||
else:
|
||||
string_parts.append(' %s' % element)
|
||||
self.accounts = accounts.AccountManager(self)
|
||||
self.backup_schedules = backup_schedules.BackupScheduleManager(self)
|
||||
self.flavors = flavors.FlavorManager(self)
|
||||
self.images = images.ImageManager(self)
|
||||
self.ipgroups = ipgroups.IPGroupManager(self)
|
||||
self.servers = servers.ServerManager(self)
|
||||
self.zones = zones.ZoneManager(self)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
string_parts.append(' -H "%s: %s"' % (element,kwargs['headers'][element]))
|
||||
_auth_url = auth_url or 'https://auth.api.rackspacecloud.com/v1.0'
|
||||
|
||||
_logger.debug("REQ: %s\n" % "".join(string_parts))
|
||||
_logger.debug("RESP:%s %s\n", resp,body)
|
||||
|
||||
def request(self, *args, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||
if 'body' in kwargs:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['body'] = json.dumps(kwargs['body'])
|
||||
|
||||
resp, body = super(HTTPClient, self).request(*args, **kwargs)
|
||||
|
||||
self.http_log(args, kwargs, resp, body)
|
||||
|
||||
if body:
|
||||
try:
|
||||
body = json.loads(body)
|
||||
except ValueError, e:
|
||||
pass
|
||||
else:
|
||||
body = None
|
||||
|
||||
if resp.status in (400, 401, 403, 404, 408, 413, 500, 501):
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
return resp, body
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
if not self.management_url:
|
||||
self.authenticate()
|
||||
|
||||
# Perform the request once. If we get a 401 back then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
try:
|
||||
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
|
||||
if self.projectid:
|
||||
kwargs['headers']['X-Auth-Project-Id'] = self.projectid
|
||||
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.Unauthorized, ex:
|
||||
try:
|
||||
self.authenticate()
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.Unauthorized:
|
||||
raise ex
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
url = self._munge_get_url(url)
|
||||
return self._cs_request(url, 'GET', **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._cs_request(url, 'POST', **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._cs_request(url, 'PUT', **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self._cs_request(url, 'DELETE', **kwargs)
|
||||
self.client = client.HTTPClient(username,
|
||||
api_key,
|
||||
project_id,
|
||||
_auth_url,
|
||||
timeout=timeout)
|
||||
|
||||
def authenticate(self):
|
||||
scheme, netloc, path, query, frag = urlparse.urlsplit(
|
||||
self.auth_url)
|
||||
path_parts = path.split('/')
|
||||
for part in path_parts:
|
||||
if len(part) > 0 and part[0] == 'v':
|
||||
self.version = part
|
||||
break
|
||||
|
||||
headers = {'X-Auth-User': self.user,
|
||||
'X-Auth-Key': self.apikey}
|
||||
if self.projectid:
|
||||
headers['X-Auth-Project-Id'] = self.projectid
|
||||
resp, body = self.request(self.auth_url, 'GET', headers=headers)
|
||||
self.management_url = resp['x-server-management-url']
|
||||
|
||||
self.auth_token = resp['x-auth-token']
|
||||
|
||||
def _munge_get_url(self, url):
|
||||
"""
|
||||
Munge GET URLs to always return uncached content.
|
||||
Authenticate against the server.
|
||||
|
||||
The OpenStack Nova API caches data *very* agressively and doesn't
|
||||
respect cache headers. To avoid stale data, then, we append a little
|
||||
bit of nonsense onto GET parameters; this appears to force the data not
|
||||
to be cached.
|
||||
Normally this is called automatically when you first access the API,
|
||||
but you can call this method to force authentication right now.
|
||||
|
||||
Returns on success; raises :exc:`exceptions.Unauthorized` if the
|
||||
credentials are wrong.
|
||||
"""
|
||||
scheme, netloc, path, query, frag = urlparse.urlsplit(url)
|
||||
query = urlparse.parse_qsl(query)
|
||||
query.append(('fresh', str(time.time())))
|
||||
query = urllib.urlencode(query)
|
||||
return urlparse.urlunsplit((scheme, netloc, path, query, frag))
|
||||
self.client.authenticate()
|
||||
|
@ -3,7 +3,7 @@
|
||||
Flavor interface.
|
||||
"""
|
||||
|
||||
from novaclient.v1_0 import base
|
||||
from novaclient import base
|
||||
|
||||
|
||||
class Flavor(base.Resource):
|
||||
|
@ -3,7 +3,7 @@
|
||||
Image interface.
|
||||
"""
|
||||
|
||||
from novaclient.v1_0 import base
|
||||
from novaclient import base
|
||||
|
||||
|
||||
class Image(base.Resource):
|
||||
@ -46,8 +46,7 @@ class ImageManager(base.ManagerWithFind):
|
||||
detail = "/detail"
|
||||
return self._list("/images%s" % detail, "images")
|
||||
|
||||
|
||||
def create(self, server, name, image_type=None, backup_type=None, rotation=None):
|
||||
def create(self, server, name):
|
||||
"""
|
||||
Create a new image by snapshotting a running :class:`Server`
|
||||
|
||||
@ -55,23 +54,7 @@ class ImageManager(base.ManagerWithFind):
|
||||
:param server: The :class:`Server` (or its ID) to make a snapshot of.
|
||||
:rtype: :class:`Image`
|
||||
"""
|
||||
if image_type is None:
|
||||
image_type = "snapshot"
|
||||
|
||||
if image_type not in ("backup", "snapshot"):
|
||||
raise Exception("Invalid image_type: must be backup or snapshot")
|
||||
|
||||
if image_type == "backup":
|
||||
if not rotation:
|
||||
raise Exception("rotation is required for backups")
|
||||
elif not backup_type:
|
||||
raise Exception("backup_type required for backups")
|
||||
elif backup_type not in ("daily", "weekly"):
|
||||
raise Exception("Invalid backup_type: must be daily or weekly")
|
||||
|
||||
data = {"image": {"serverId": base.getid(server), "name": name,
|
||||
"image_type": image_type, "backup_type": backup_type,
|
||||
"rotation": rotation}}
|
||||
data = {"image": {"serverId": base.getid(server), "name": name}}
|
||||
return self._create("/images", data, "image")
|
||||
|
||||
def delete(self, image):
|
||||
|
@ -3,7 +3,7 @@
|
||||
IP Group interface.
|
||||
"""
|
||||
|
||||
from novaclient.v1_0 import base
|
||||
from novaclient import base
|
||||
|
||||
|
||||
class IPGroup(base.Resource):
|
||||
|
@ -20,7 +20,10 @@ Server interface.
|
||||
"""
|
||||
|
||||
import urllib
|
||||
from novaclient.v1_0 import base
|
||||
|
||||
from novaclient import base
|
||||
from novaclient.v1_0 import base as local_base
|
||||
|
||||
|
||||
REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
|
||||
|
||||
@ -154,6 +157,18 @@ class Server(base.Resource):
|
||||
"""
|
||||
self.manager.resize(self, flavor)
|
||||
|
||||
def backup(self, image_name, backup_type, rotation):
|
||||
"""
|
||||
Create a server backup.
|
||||
|
||||
:param server: The :class:`Server` (or its ID).
|
||||
:param image_name: The name to assign the newly create image.
|
||||
:param backup_type: 'daily' or 'weekly'
|
||||
:param rotation: number of backups of type 'backup_type' to keep
|
||||
:returns Newly created :class:`Image` object
|
||||
"""
|
||||
return self.manager.backup(self, image_name, backup_type, rotation)
|
||||
|
||||
def confirm_resize(self):
|
||||
"""
|
||||
Confirm that the resize worked, thus removing the original server.
|
||||
@ -198,7 +213,7 @@ class Server(base.Resource):
|
||||
return self.addresses['private']
|
||||
|
||||
|
||||
class ServerManager(base.BootingManagerWithFind):
|
||||
class ServerManager(local_base.BootingManagerWithFind):
|
||||
resource_class = Server
|
||||
|
||||
def get(self, server):
|
||||
@ -370,6 +385,31 @@ class ServerManager(base.BootingManagerWithFind):
|
||||
"""
|
||||
self._action('resize', server, {'flavorId': base.getid(flavor)})
|
||||
|
||||
def backup(self, server, image_name, backup_type, rotation):
|
||||
"""
|
||||
Create a server backup.
|
||||
|
||||
:param server: The :class:`Server` (or its ID).
|
||||
:param image_name: The name to assign the newly create image.
|
||||
:param backup_type: 'daily' or 'weekly'
|
||||
:param rotation: number of backups of type 'backup_type' to keep
|
||||
:returns Newly created :class:`Image` object
|
||||
"""
|
||||
if not rotation:
|
||||
raise Exception("rotation is required for backups")
|
||||
elif not backup_type:
|
||||
raise Exception("backup_type required for backups")
|
||||
elif backup_type not in ("daily", "weekly"):
|
||||
raise Exception("Invalid backup_type: must be daily or weekly")
|
||||
|
||||
data = {
|
||||
"name": image_name,
|
||||
"rotation": rotation,
|
||||
"backup_type": backup_type,
|
||||
}
|
||||
|
||||
self._action('createBackup', server, data)
|
||||
|
||||
def pause(self, server):
|
||||
"""
|
||||
Pause the server.
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,8 @@
|
||||
Zone interface.
|
||||
"""
|
||||
|
||||
from novaclient.v1_0 import base
|
||||
from novaclient import base
|
||||
from novaclient.v1_0 import base as local_base
|
||||
|
||||
|
||||
class Weighting(base.Resource):
|
||||
@ -64,7 +65,7 @@ class Zone(base.Resource):
|
||||
weight_offset, weight_scale)
|
||||
|
||||
|
||||
class ZoneManager(base.BootingManagerWithFind):
|
||||
class ZoneManager(local_base.BootingManagerWithFind):
|
||||
resource_class = Zone
|
||||
|
||||
def info(self):
|
||||
|
@ -1,66 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from novaclient.v1_1 import client
|
||||
from novaclient.v1_1 import exceptions
|
||||
from novaclient.v1_1 import flavors
|
||||
from novaclient.v1_1 import images
|
||||
from novaclient.v1_1 import servers
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""
|
||||
Top-level object to access the OpenStack Compute v1.0 API.
|
||||
|
||||
Create an instance with your creds::
|
||||
|
||||
>>> os = novaclient.v1_1.Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
|
||||
|
||||
Then call methods on its managers::
|
||||
|
||||
>>> os.servers.list()
|
||||
...
|
||||
>>> os.flavors.list()
|
||||
...
|
||||
|
||||
&c.
|
||||
"""
|
||||
|
||||
def __init__(self, username, apikey, projectid, auth_url=None, timeout=None):
|
||||
"""Initialize v1.0 Openstack Client."""
|
||||
self.flavors = flavors.FlavorManager(self)
|
||||
self.images = images.ImageManager(self)
|
||||
self.servers = servers.ServerManager(self)
|
||||
|
||||
auth_url = auth_url or "https://auth.api.rackspacecloud.com/v1.0"
|
||||
|
||||
self.client = client.HTTPClient(username,
|
||||
apikey,
|
||||
projectid,
|
||||
auth_url,
|
||||
timeout=timeout)
|
||||
|
||||
def authenticate(self):
|
||||
"""
|
||||
Authenticate against the server.
|
||||
|
||||
Normally this is called automatically when you first access the API,
|
||||
but you can call this method to force authentication right now.
|
||||
|
||||
Returns on success; raises :exc:`novaclient.Unauthorized` if the
|
||||
credentials are wrong.
|
||||
"""
|
||||
self.client.authenticate()
|
@ -19,7 +19,7 @@
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
from novaclient.v1_1 import exceptions
|
||||
from novaclient import exceptions
|
||||
|
||||
# Python 2.4 compat
|
||||
try:
|
||||
|
@ -1,154 +1,48 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
"""
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
import time
|
||||
import urlparse
|
||||
import urllib
|
||||
import httplib2
|
||||
import logging
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
from novaclient import client
|
||||
from novaclient.v1_1 import flavors
|
||||
from novaclient.v1_1 import images
|
||||
from novaclient.v1_1 import servers
|
||||
|
||||
# Python 2.5 compat fix
|
||||
if not hasattr(urlparse, 'parse_qsl'):
|
||||
import cgi
|
||||
urlparse.parse_qsl = cgi.parse_qsl
|
||||
|
||||
import novaclient
|
||||
from novaclient.v1_1 import exceptions
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
class Client(object):
|
||||
"""
|
||||
Top-level object to access the OpenStack Compute API.
|
||||
|
||||
class HTTPClient(httplib2.Http):
|
||||
Create an instance with your creds::
|
||||
|
||||
USER_AGENT = 'python-novaclient/%s' % novaclient.__version__
|
||||
>>> client = Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
|
||||
|
||||
def __init__(self, user, apikey, projectid, auth_url, timeout=None):
|
||||
super(HTTPClient, self).__init__(timeout=timeout)
|
||||
self.user = user
|
||||
self.apikey = apikey
|
||||
self.projectid = projectid
|
||||
self.auth_url = auth_url
|
||||
self.version = 'v1.0'
|
||||
Then call methods on its managers::
|
||||
|
||||
self.management_url = None
|
||||
self.auth_token = None
|
||||
>>> client.servers.list()
|
||||
...
|
||||
>>> client.flavors.list()
|
||||
...
|
||||
|
||||
# httplib2 overrides
|
||||
self.force_exception_to_status_code = True
|
||||
"""
|
||||
|
||||
def http_log(self, args, kwargs, resp, body):
|
||||
if not _logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
def __init__(self, username, api_key, project_id, auth_url, timeout=None):
|
||||
self.flavors = flavors.FlavorManager(self)
|
||||
self.images = images.ImageManager(self)
|
||||
self.servers = servers.ServerManager(self)
|
||||
|
||||
string_parts = ['curl -i']
|
||||
for element in args:
|
||||
if element in ('GET','POST'):
|
||||
string_parts.append(' -X %s' % element)
|
||||
else:
|
||||
string_parts.append(' %s' % element)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
string_parts.append(' -H "%s: %s"' % (element,kwargs['headers'][element]))
|
||||
|
||||
_logger.debug("REQ: %s\n" % "".join(string_parts))
|
||||
_logger.debug("RESP:%s %s\n", resp,body)
|
||||
|
||||
def request(self, *args, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||
if 'body' in kwargs:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['body'] = json.dumps(kwargs['body'])
|
||||
|
||||
resp, body = super(HTTPClient, self).request(*args, **kwargs)
|
||||
|
||||
self.http_log(args, kwargs, resp, body)
|
||||
|
||||
if body:
|
||||
try:
|
||||
body = json.loads(body)
|
||||
except ValueError, e:
|
||||
pass
|
||||
else:
|
||||
body = None
|
||||
|
||||
if resp.status in (400, 401, 403, 404, 408, 413, 500, 501):
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
return resp, body
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
if not self.management_url:
|
||||
self.authenticate()
|
||||
|
||||
# Perform the request once. If we get a 401 back then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
try:
|
||||
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
|
||||
if self.projectid:
|
||||
kwargs['headers']['X-Auth-Project-Id'] = self.projectid
|
||||
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.Unauthorized, ex:
|
||||
try:
|
||||
self.authenticate()
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.Unauthorized:
|
||||
raise ex
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
url = self._munge_get_url(url)
|
||||
return self._cs_request(url, 'GET', **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._cs_request(url, 'POST', **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._cs_request(url, 'PUT', **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self._cs_request(url, 'DELETE', **kwargs)
|
||||
self.client = client.HTTPClient(username,
|
||||
api_key,
|
||||
project_id,
|
||||
auth_url,
|
||||
timeout=timeout)
|
||||
|
||||
def authenticate(self):
|
||||
scheme, netloc, path, query, frag = urlparse.urlsplit(
|
||||
self.auth_url)
|
||||
path_parts = path.split('/')
|
||||
for part in path_parts:
|
||||
if len(part) > 0 and part[0] == 'v':
|
||||
self.version = part
|
||||
break
|
||||
|
||||
headers = {'X-Auth-User': self.user,
|
||||
'X-Auth-Key': self.apikey}
|
||||
if self.projectid:
|
||||
headers['X-Auth-Project-Id'] = self.projectid
|
||||
resp, body = self.request(self.auth_url, 'GET', headers=headers)
|
||||
self.management_url = resp['x-server-management-url']
|
||||
|
||||
self.auth_token = resp['x-auth-token']
|
||||
|
||||
def _munge_get_url(self, url):
|
||||
"""
|
||||
Munge GET URLs to always return uncached content.
|
||||
Authenticate against the server.
|
||||
|
||||
The OpenStack Nova API caches data *very* agressively and doesn't
|
||||
respect cache headers. To avoid stale data, then, we append a little
|
||||
bit of nonsense onto GET parameters; this appears to force the data not
|
||||
to be cached.
|
||||
Normally this is called automatically when you first access the API,
|
||||
but you can call this method to force authentication right now.
|
||||
|
||||
Returns on success; raises :exc:`exceptions.Unauthorized` if the
|
||||
credentials are wrong.
|
||||
"""
|
||||
scheme, netloc, path, query, frag = urlparse.urlsplit(url)
|
||||
query = urlparse.parse_qsl(query)
|
||||
query.append(('fresh', str(time.time())))
|
||||
query = urllib.urlencode(query)
|
||||
return urlparse.urlunsplit((scheme, netloc, path, query, frag))
|
||||
self.client.authenticate()
|
||||
|
@ -1,100 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
"""
|
||||
Exception definitions.
|
||||
"""
|
||||
|
||||
class OpenStackException(Exception):
|
||||
"""
|
||||
The base exception class for all exceptions this library raises.
|
||||
"""
|
||||
def __init__(self, code, message=None, details=None):
|
||||
self.code = code
|
||||
self.message = message or self.__class__.message
|
||||
self.details = details
|
||||
|
||||
def __str__(self):
|
||||
return "%s (HTTP %s)" % (self.message, self.code)
|
||||
|
||||
|
||||
class BadRequest(OpenStackException):
|
||||
"""
|
||||
HTTP 400 - Bad request: you sent some malformed data.
|
||||
"""
|
||||
http_status = 400
|
||||
message = "Bad request"
|
||||
|
||||
|
||||
class Unauthorized(OpenStackException):
|
||||
"""
|
||||
HTTP 401 - Unauthorized: bad credentials.
|
||||
"""
|
||||
http_status = 401
|
||||
message = "Unauthorized"
|
||||
|
||||
|
||||
class Forbidden(OpenStackException):
|
||||
"""
|
||||
HTTP 403 - Forbidden: your credentials don't give you access to this
|
||||
resource.
|
||||
"""
|
||||
http_status = 403
|
||||
message = "Forbidden"
|
||||
|
||||
|
||||
class NotFound(OpenStackException):
|
||||
"""
|
||||
HTTP 404 - Not found
|
||||
"""
|
||||
http_status = 404
|
||||
message = "Not found"
|
||||
|
||||
|
||||
class OverLimit(OpenStackException):
|
||||
"""
|
||||
HTTP 413 - Over limit: you're over the API limits for this time period.
|
||||
"""
|
||||
http_status = 413
|
||||
message = "Over limit"
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HTTPNotImplemented(OpenStackException):
|
||||
"""
|
||||
HTTP 501 - Not Implemented: the server does not support this operation.
|
||||
"""
|
||||
http_status = 501
|
||||
message = "Not Implemented"
|
||||
|
||||
|
||||
# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
|
||||
# so we can do this:
|
||||
# _code_map = dict((c.http_status, c)
|
||||
# for c in OpenStackException.__subclasses__())
|
||||
#
|
||||
# Instead, we have to hardcode it:
|
||||
_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
|
||||
Forbidden, NotFound, OverLimit, HTTPNotImplemented])
|
||||
|
||||
|
||||
def from_response(response, body):
|
||||
"""
|
||||
Return an instance of an OpenStackException or subclass
|
||||
based on an httplib2 response.
|
||||
|
||||
Usage::
|
||||
|
||||
resp, body = http.request(...)
|
||||
if resp.status != 200:
|
||||
raise exception_from_response(resp, body)
|
||||
"""
|
||||
cls = _code_map.get(response.status, OpenStackException)
|
||||
if body:
|
||||
message = "n/a"
|
||||
details = "n/a"
|
||||
if hasattr(body, 'keys'):
|
||||
error = body[body.keys()[0]]
|
||||
message = error.get('message', None)
|
||||
details = error.get('details', None)
|
||||
return cls(code=response.status, message=message, details=details)
|
||||
else:
|
||||
return cls(code=response.status)
|
@ -3,7 +3,7 @@
|
||||
Flavor interface.
|
||||
"""
|
||||
|
||||
from novaclient.v1_1 import base
|
||||
from novaclient import base
|
||||
|
||||
|
||||
class Flavor(base.Resource):
|
||||
@ -26,10 +26,10 @@ class FlavorManager(base.ManagerWithFind):
|
||||
|
||||
:rtype: list of :class:`Flavor`.
|
||||
"""
|
||||
detail = ""
|
||||
if detailed:
|
||||
detail = "/detail"
|
||||
return self._list("/flavors%s" % detail, "flavors")
|
||||
if detailed is True:
|
||||
return self._list("/flavors/detail", "flavors")
|
||||
else:
|
||||
return self._list("/flavors", "flavors")
|
||||
|
||||
def get(self, flavor):
|
||||
"""
|
||||
|
@ -3,7 +3,7 @@
|
||||
Image interface.
|
||||
"""
|
||||
|
||||
from novaclient.v1_1 import base
|
||||
from novaclient import base
|
||||
|
||||
|
||||
class Image(base.Resource):
|
||||
@ -41,38 +41,10 @@ class ImageManager(base.ManagerWithFind):
|
||||
|
||||
:rtype: list of :class:`Image`
|
||||
"""
|
||||
detail = ""
|
||||
if detailed:
|
||||
detail = "/detail"
|
||||
return self._list("/images%s" % detail, "images")
|
||||
|
||||
|
||||
def create(self, server, name, image_type=None, backup_type=None, rotation=None):
|
||||
"""
|
||||
Create a new image by snapshotting a running :class:`Server`
|
||||
|
||||
:param name: An (arbitrary) name for the new image.
|
||||
:param server: The :class:`Server` (or its ID) to make a snapshot of.
|
||||
:rtype: :class:`Image`
|
||||
"""
|
||||
if image_type is None:
|
||||
image_type = "snapshot"
|
||||
|
||||
if image_type not in ("backup", "snapshot"):
|
||||
raise Exception("Invalid image_type: must be backup or snapshot")
|
||||
|
||||
if image_type == "backup":
|
||||
if not rotation:
|
||||
raise Exception("rotation is required for backups")
|
||||
elif not backup_type:
|
||||
raise Exception("backup_type required for backups")
|
||||
elif backup_type not in ("daily", "weekly"):
|
||||
raise Exception("Invalid backup_type: must be daily or weekly")
|
||||
|
||||
data = {"image": {"serverId": base.getid(server), "name": name,
|
||||
"image_type": image_type, "backup_type": backup_type,
|
||||
"rotation": rotation}}
|
||||
return self._create("/images", data, "image")
|
||||
if detailed is True:
|
||||
return self._list("/images/detail", "images")
|
||||
else:
|
||||
return self._list("/images", "images")
|
||||
|
||||
def delete(self, image):
|
||||
"""
|
||||
|
@ -20,7 +20,10 @@ Server interface.
|
||||
"""
|
||||
|
||||
import urllib
|
||||
from novaclient.v1_1 import base
|
||||
|
||||
from novaclient import base
|
||||
from novaclient.v1_0 import base as local_base
|
||||
|
||||
|
||||
REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
|
||||
|
||||
@ -42,22 +45,11 @@ class Server(base.Resource):
|
||||
:param name: Update the server's name.
|
||||
:param password: Update the root password.
|
||||
"""
|
||||
self.manager.update(self, name)
|
||||
|
||||
def create_image(self, name, metadata=None):
|
||||
"""
|
||||
Create an image based on this server.
|
||||
|
||||
:param name: The name of the image to create
|
||||
:param metadata: The metadata to associated with the image.
|
||||
"""
|
||||
self.manager.create_image(self, name, metadata)
|
||||
self.manager.update(self, name=name)
|
||||
|
||||
def change_password(self, password):
|
||||
"""
|
||||
Update the root password on this server.
|
||||
|
||||
:param password: The password to set.
|
||||
Update the password for a server.
|
||||
"""
|
||||
self.manager.change_password(self, password)
|
||||
|
||||
@ -91,6 +83,15 @@ class Server(base.Resource):
|
||||
"""
|
||||
self.manager.resize(self, flavor)
|
||||
|
||||
def create_image(self, image_name, metadata):
|
||||
"""
|
||||
Create an image based on this server.
|
||||
|
||||
:param image_name: The name to assign the newly create image.
|
||||
:param metadata: Metadata to assign to the image.
|
||||
"""
|
||||
self.manager.create_image(self, image_name, metadata)
|
||||
|
||||
def confirm_resize(self):
|
||||
"""
|
||||
Confirm that the resize worked, thus removing the original server.
|
||||
@ -121,24 +122,8 @@ class Server(base.Resource):
|
||||
return ""
|
||||
return self.addresses['private']
|
||||
|
||||
@property
|
||||
def image_id(self):
|
||||
"""
|
||||
Shortcut to get the image identifier.
|
||||
"""
|
||||
return self.image["id"]
|
||||
|
||||
@property
|
||||
def flavor_id(self):
|
||||
"""
|
||||
Shortcut to get the flavor identifier.
|
||||
"""
|
||||
return self.flavor["id"]
|
||||
|
||||
|
||||
|
||||
|
||||
class ServerManager(base.BootingManagerWithFind):
|
||||
class ServerManager(local_base.BootingManagerWithFind):
|
||||
resource_class = Server
|
||||
|
||||
def get(self, server):
|
||||
@ -161,8 +146,9 @@ class ServerManager(base.BootingManagerWithFind):
|
||||
"""
|
||||
if search_opts is None:
|
||||
search_opts = {}
|
||||
|
||||
qparams = {}
|
||||
# only use values in query string if they are set
|
||||
|
||||
for opt, val in search_opts.iteritems():
|
||||
if val:
|
||||
qparams[opt] = val
|
||||
@ -190,8 +176,30 @@ class ServerManager(base.BootingManagerWithFind):
|
||||
file-like object). A maximum of five entries is allowed,
|
||||
and each file must be 10k or less.
|
||||
"""
|
||||
return self._boot("/servers", "server", name, image, flavor,
|
||||
meta=meta, files=files)
|
||||
personality = []
|
||||
|
||||
for file_path, filelike in files.items():
|
||||
try:
|
||||
data = filelike.read()
|
||||
except AttributeError:
|
||||
data = str(filelike)
|
||||
|
||||
personality.append({
|
||||
"path": file_path,
|
||||
"contents": data.encode("base64"),
|
||||
})
|
||||
|
||||
body = {
|
||||
"server": {
|
||||
"name": name,
|
||||
"imageRef": base.getid(image),
|
||||
"flavorRef": base.getid(flavor),
|
||||
"metadata": meta or {},
|
||||
"personality": personality,
|
||||
},
|
||||
}
|
||||
|
||||
return self._create("/servers", body, "server", return_raw=False)
|
||||
|
||||
def update(self, server, name=None):
|
||||
"""
|
||||
@ -211,6 +219,12 @@ class ServerManager(base.BootingManagerWithFind):
|
||||
|
||||
self._update("/servers/%s" % base.getid(server), body)
|
||||
|
||||
def change_password(self, server, password):
|
||||
"""
|
||||
Update the password for a server.
|
||||
"""
|
||||
self._action("changePassword", server, {"adminPass": password})
|
||||
|
||||
def delete(self, server):
|
||||
"""
|
||||
Delete (i.e. shut down and delete the image) this server.
|
||||
@ -227,32 +241,6 @@ class ServerManager(base.BootingManagerWithFind):
|
||||
"""
|
||||
self._action('reboot', server, {'type': type})
|
||||
|
||||
def create_image(self, server, name, metadata=None):
|
||||
"""
|
||||
Create an image based on this server.
|
||||
|
||||
:param server: The :class:`Server` (or its ID) to create image from.
|
||||
:param name: The name of the image to create
|
||||
:param metadata: The metadata to associated with the image.
|
||||
"""
|
||||
body = {
|
||||
"name": name,
|
||||
"metadata": metadata or {},
|
||||
}
|
||||
self._action('createImage', server, body)
|
||||
|
||||
def change_password(self, server, password):
|
||||
"""
|
||||
Update the root password on a server.
|
||||
|
||||
:param server: The :class:`Server` (or its ID) to share onto.
|
||||
:param password: The password to set.
|
||||
"""
|
||||
body = {
|
||||
"adminPass": password,
|
||||
}
|
||||
self._action('changePassword', server, body)
|
||||
|
||||
def rebuild(self, server, image):
|
||||
"""
|
||||
Rebuild -- shut down and then re-image -- a server.
|
||||
@ -292,6 +280,17 @@ class ServerManager(base.BootingManagerWithFind):
|
||||
"""
|
||||
self._action('revertResize', server)
|
||||
|
||||
def create_image(self, server, image_name, metadata=None):
|
||||
"""
|
||||
Snapshot a server.
|
||||
|
||||
:param server: The :class:`Server` (or its ID) to share onto.
|
||||
:param image_name: Name to give the snapshot image
|
||||
:param meta: Metadata to give newly-created image entity
|
||||
"""
|
||||
self._action('createImage', server,
|
||||
{'name': image_name, 'metadata': metadata or {}})
|
||||
|
||||
def _action(self, action, server, info=None):
|
||||
"""
|
||||
Perform a server "action" -- reboot/rebuild/resize/etc.
|
||||
|
@ -15,570 +15,316 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Command-line interface to the OpenStack Nova API.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import httplib2
|
||||
import os
|
||||
import prettytable
|
||||
import sys
|
||||
import textwrap
|
||||
import uuid
|
||||
|
||||
import novaclient.v1_1
|
||||
from novaclient.v1_1 import exceptions
|
||||
from novaclient import exceptions
|
||||
from novaclient import utils
|
||||
from novaclient.v1_1 import client
|
||||
from novaclient.v1_1 import servers
|
||||
|
||||
|
||||
def pretty_choice_list(l):
|
||||
return ', '.join("'%s'" % i for i in l)
|
||||
CLIENT_CLASS = client.Client
|
||||
|
||||
|
||||
# Sentinal for boot --key
|
||||
AUTO_KEY = object()
|
||||
|
||||
|
||||
# Decorator for args
|
||||
def arg(*args, **kwargs):
|
||||
def _decorator(func):
|
||||
# Because of the sematics of decorator composition if we just append
|
||||
# to the options list positional options will appear to be backwards.
|
||||
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
|
||||
return func
|
||||
return _decorator
|
||||
def _translate_flavor_keys(collection):
|
||||
convert = [('ram', 'memory_mb'), ('disk', 'local_gb')]
|
||||
for item in collection:
|
||||
keys = item.__dict__.keys()
|
||||
for from_key, to_key in convert:
|
||||
if from_key in keys and to_key not in keys:
|
||||
setattr(item, to_key, item._info[from_key])
|
||||
|
||||
@utils.arg('--flavor',
|
||||
default=None,
|
||||
metavar='<flavor>',
|
||||
help="Flavor ID (see 'osc flavors'). "\
|
||||
"Defaults to 256MB RAM instance.")
|
||||
@utils.arg('--image',
|
||||
default=None,
|
||||
metavar='<image>',
|
||||
help="Image ID (see 'osc images'). "\
|
||||
"Defaults to Ubuntu 10.04 LTS.")
|
||||
@utils.arg('--meta',
|
||||
metavar="<key=value>",
|
||||
action='append',
|
||||
default=[],
|
||||
help="Record arbitrary key/value metadata. "\
|
||||
"May be give multiple times.")
|
||||
@utils.arg('--file',
|
||||
metavar="<dst-path=src-path>",
|
||||
action='append',
|
||||
dest='files',
|
||||
default=[],
|
||||
help="Store arbitrary files from <src-path> locally to <dst-path> "\
|
||||
"on the new server. You may store up to 5 files.")
|
||||
@utils.arg('--key',
|
||||
metavar='<path>',
|
||||
nargs='?',
|
||||
const=AUTO_KEY,
|
||||
help="Key the server with an SSH keypair. "\
|
||||
"Looks in ~/.ssh for a key, "\
|
||||
"or takes an explicit <path> to one.")
|
||||
@utils.arg('name', metavar='<name>', help='Name for the new server')
|
||||
def do_boot(cs, args):
|
||||
"""Boot a new server."""
|
||||
name, image, flavor, metadata, files = _boot(cs, args)
|
||||
server = cs.servers.create(args.name, image, flavor,
|
||||
meta=metadata, files=files)
|
||||
utils.print_dict(server._info)
|
||||
|
||||
|
||||
class CommandError(Exception):
|
||||
pass
|
||||
def _boot(cs, args):
|
||||
"""Boot a new server."""
|
||||
|
||||
flavor = args.flavor or cs.flavors.find(ram=256)
|
||||
image = args.image or cs.images.find(name="Ubuntu 10.04 LTS "\
|
||||
"(lucid)")
|
||||
|
||||
def env(e):
|
||||
return os.environ.get(e, '')
|
||||
metadata = dict(v.split('=') for v in args.meta)
|
||||
|
||||
|
||||
class OpenStackShell(object):
|
||||
|
||||
# Hook for the test suite to inject a fake server.
|
||||
_api_class = novaclient.v1_1.Client
|
||||
|
||||
def __init__(self):
|
||||
self.parser = argparse.ArgumentParser(
|
||||
prog='nova',
|
||||
description=__doc__.strip(),
|
||||
epilog='See "nova help COMMAND" '\
|
||||
'for help on a specific command.',
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter,
|
||||
)
|
||||
|
||||
# Global arguments
|
||||
self.parser.add_argument('-h', '--help',
|
||||
action='help',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
self.parser.add_argument('--debug',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
self.parser.add_argument('--username',
|
||||
default=env('NOVA_USERNAME'),
|
||||
help='Defaults to env[NOVA_USERNAME].')
|
||||
|
||||
self.parser.add_argument('--apikey',
|
||||
default=env('NOVA_API_KEY'),
|
||||
help='Defaults to env[NOVA_API_KEY].')
|
||||
|
||||
self.parser.add_argument('--projectid',
|
||||
default=env('NOVA_PROJECT_ID'),
|
||||
help='Defaults to env[NOVA_PROJECT_ID].')
|
||||
|
||||
auth_url = env('NOVA_URL')
|
||||
if auth_url == '':
|
||||
auth_url = 'https://auth.api.rackspacecloud.com/v1.0'
|
||||
self.parser.add_argument('--url',
|
||||
default=auth_url,
|
||||
help='Defaults to env[NOVA_URL].')
|
||||
|
||||
# Subcommands
|
||||
subparsers = self.parser.add_subparsers(metavar='<subcommand>')
|
||||
self.subcommands = {}
|
||||
|
||||
# Everything that's do_* is a subcommand.
|
||||
for attr in (a for a in dir(self) if a.startswith('do_')):
|
||||
# I prefer to be hypen-separated instead of underscores.
|
||||
command = attr[3:].replace('_', '-')
|
||||
callback = getattr(self, attr)
|
||||
desc = callback.__doc__ or ''
|
||||
help = desc.strip().split('\n')[0]
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(command,
|
||||
help=help,
|
||||
description=desc,
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter
|
||||
)
|
||||
subparser.add_argument('-h', '--help',
|
||||
action='help',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
self.subcommands[command] = subparser
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
def main(self, argv):
|
||||
# Parse args and call whatever callback was selected
|
||||
args = self.parser.parse_args(argv)
|
||||
|
||||
# Short-circuit and deal with help right away.
|
||||
if args.func == self.do_help:
|
||||
self.do_help(args)
|
||||
return 0
|
||||
|
||||
# Deal with global arguments
|
||||
if args.debug:
|
||||
httplib2.debuglevel = 1
|
||||
|
||||
user, apikey, projectid, url = args.username, args.apikey, \
|
||||
args.projectid, args.url
|
||||
|
||||
#FIXME(usrleon): Here should be restrict for project id same as
|
||||
# for username or apikey but for compatibility it is not.
|
||||
|
||||
if not user:
|
||||
raise CommandError("You must provide a username, either via "
|
||||
"--username or via env[NOVA_USERNAME]")
|
||||
if not apikey:
|
||||
raise CommandError("You must provide an API key, either via "
|
||||
"--apikey or via env[NOVA_API_KEY]")
|
||||
|
||||
self.cs = self._api_class(user, apikey, projectid, url)
|
||||
files = {}
|
||||
for f in args.files:
|
||||
dst, src = f.split('=', 1)
|
||||
try:
|
||||
self.cs.authenticate()
|
||||
except exceptions.Unauthorized:
|
||||
raise CommandError("Invalid OpenStack Nova credentials.")
|
||||
files[dst] = open(src)
|
||||
except IOError, e:
|
||||
raise exceptions.CommandError("Can't open '%s': %s" % (src, e))
|
||||
|
||||
args.func(args)
|
||||
|
||||
@arg('command', metavar='<subcommand>', nargs='?',
|
||||
help='Display help for <subcommand>')
|
||||
def do_help(self, args):
|
||||
"""
|
||||
Display help about this program or one of its subcommands.
|
||||
"""
|
||||
if args.command:
|
||||
if args.command in self.subcommands:
|
||||
self.subcommands[args.command].print_help()
|
||||
else:
|
||||
raise CommandError("'%s' is not a valid subcommand." %
|
||||
args.command)
|
||||
if args.key is AUTO_KEY:
|
||||
possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k)
|
||||
for k in ('id_dsa.pub', 'id_rsa.pub')]
|
||||
for k in possible_keys:
|
||||
if os.path.exists(k):
|
||||
keyfile = k
|
||||
break
|
||||
else:
|
||||
self.parser.print_help()
|
||||
raise exceptions.CommandError("Couldn't find a key file: tried "
|
||||
"~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub")
|
||||
elif args.key:
|
||||
keyfile = args.key
|
||||
else:
|
||||
keyfile = None
|
||||
|
||||
def _boot(self, args, reservation_id=None, min_count=None, max_count=None):
|
||||
"""Boot a new server."""
|
||||
flavor = args.flavor or self.cs.flavors.find(ram=256)
|
||||
image = args.image or self.cs.images.find(name="Ubuntu 10.04 LTS "\
|
||||
"(lucid)")
|
||||
|
||||
metadata = dict(v.split('=') for v in args.meta)
|
||||
|
||||
files = {}
|
||||
for f in args.files:
|
||||
dst, src = f.split('=', 1)
|
||||
try:
|
||||
files[dst] = open(src)
|
||||
except IOError, e:
|
||||
raise CommandError("Can't open '%s': %s" % (src, e))
|
||||
|
||||
if args.key is AUTO_KEY:
|
||||
possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k)
|
||||
for k in ('id_dsa.pub', 'id_rsa.pub')]
|
||||
for k in possible_keys:
|
||||
if os.path.exists(k):
|
||||
keyfile = k
|
||||
break
|
||||
else:
|
||||
raise CommandError("Couldn't find a key file: tried "
|
||||
"~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub")
|
||||
elif args.key:
|
||||
keyfile = args.key
|
||||
else:
|
||||
keyfile = None
|
||||
|
||||
if keyfile:
|
||||
try:
|
||||
files['/root/.ssh/authorized_keys2'] = open(keyfile)
|
||||
except IOError, e:
|
||||
raise CommandError("Can't open '%s': %s" % (keyfile, e))
|
||||
|
||||
return (args.name, image, flavor, metadata, files)
|
||||
|
||||
@arg('--flavor',
|
||||
default=None,
|
||||
metavar='<flavor>',
|
||||
help="Flavor ID (see 'novaclient flavors'). "\
|
||||
"Defaults to 256MB RAM instance.")
|
||||
@arg('--image',
|
||||
default=None,
|
||||
metavar='<image>',
|
||||
help="Image ID (see 'novaclient images'). "\
|
||||
"Defaults to Ubuntu 10.04 LTS.")
|
||||
@arg('--meta',
|
||||
metavar="<key=value>",
|
||||
action='append',
|
||||
default=[],
|
||||
help="Record arbitrary key/value metadata. "\
|
||||
"May be give multiple times.")
|
||||
@arg('--file',
|
||||
metavar="<dst-path=src-path>",
|
||||
action='append',
|
||||
dest='files',
|
||||
default=[],
|
||||
help="Store arbitrary files from <src-path> locally to <dst-path> "\
|
||||
"on the new server. You may store up to 5 files.")
|
||||
@arg('--key',
|
||||
metavar='<path>',
|
||||
nargs='?',
|
||||
const=AUTO_KEY,
|
||||
help="Key the server with an SSH keypair. "\
|
||||
"Looks in ~/.ssh for a key, "\
|
||||
"or takes an explicit <path> to one.")
|
||||
@arg('name', metavar='<name>', help='Name for the new server')
|
||||
def do_boot(self, args):
|
||||
"""Boot a new server."""
|
||||
name, image, flavor, metadata, files = self._boot(args)
|
||||
|
||||
server = self.cs.servers.create(args.name, image, flavor,
|
||||
meta=metadata,
|
||||
files=files)
|
||||
print_dict(server._info)
|
||||
|
||||
def _translate_flavor_keys(self, collection):
|
||||
convert = [('ram', 'memory_mb'), ('disk', 'local_gb')]
|
||||
for item in collection:
|
||||
keys = item.__dict__.keys()
|
||||
for from_key, to_key in convert:
|
||||
if from_key in keys and to_key not in keys:
|
||||
setattr(item, to_key, item._info[from_key])
|
||||
|
||||
@arg('--fixed_ip',
|
||||
dest='fixed_ip',
|
||||
metavar='<fixed_ip>',
|
||||
default=None,
|
||||
help='Only match against fixed IP.')
|
||||
@arg('--reservation_id',
|
||||
dest='reservation_id',
|
||||
metavar='<reservation_id>',
|
||||
default=None,
|
||||
help='Only return instances that match reservation_id.')
|
||||
@arg('--recurse_zones',
|
||||
dest='recurse_zones',
|
||||
metavar='<0|1>',
|
||||
nargs='?',
|
||||
type=int,
|
||||
const=1,
|
||||
default=0,
|
||||
help='Recurse through all zones if set.')
|
||||
@arg('--ip',
|
||||
dest='ip',
|
||||
metavar='<ip_regexp>',
|
||||
default=None,
|
||||
help='Search with regular expression match by IP address')
|
||||
@arg('--ip6',
|
||||
dest='ip6',
|
||||
metavar='<ip6_regexp>',
|
||||
default=None,
|
||||
help='Search with regular expression match by IPv6 address')
|
||||
@arg('--server_name',
|
||||
dest='server_name',
|
||||
metavar='<name_regexp>',
|
||||
default=None,
|
||||
help='Search with regular expression match by server name')
|
||||
@arg('--name',
|
||||
dest='display_name',
|
||||
metavar='<name_regexp>',
|
||||
default=None,
|
||||
help='Search with regular expression match by display name')
|
||||
@arg('--instance_name',
|
||||
dest='name',
|
||||
metavar='<name_regexp>',
|
||||
default=None,
|
||||
help='Search with regular expression match by instance name')
|
||||
def do_list(self, args):
|
||||
"""List active servers."""
|
||||
recurse_zones = args.recurse_zones
|
||||
search_opts = {
|
||||
'reservation_id': args.reservation_id,
|
||||
'fixed_ip': args.fixed_ip,
|
||||
'recurse_zones': recurse_zones,
|
||||
'ip': args.ip,
|
||||
'ip6': args.ip6,
|
||||
'name': args.name,
|
||||
'server_name': args.server_name,
|
||||
'display_name': args.display_name}
|
||||
if recurse_zones:
|
||||
to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP']
|
||||
else:
|
||||
to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP']
|
||||
print_list(self.cs.servers.list(search_opts=search_opts),
|
||||
to_print)
|
||||
|
||||
def do_flavor_list(self, args):
|
||||
"""Print a list of available 'flavors' (sizes of servers)."""
|
||||
flavors = self.cs.flavors.list()
|
||||
self._translate_flavor_keys(flavors)
|
||||
print_list(flavors, [
|
||||
'ID',
|
||||
'Name',
|
||||
'Memory_MB',
|
||||
'Swap',
|
||||
'Local_GB',
|
||||
'VCPUs',
|
||||
'RXTX_Quota',
|
||||
'RXTX_Cap'])
|
||||
|
||||
def do_image_list(self, args):
|
||||
"""Print a list of available images to boot from."""
|
||||
print_list(self.cs.images.list(), ['ID', 'Name', 'Status'])
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
@arg('name', metavar='<name>', help='Name of backup or snapshot.')
|
||||
@arg('--image-type',
|
||||
metavar='<backup|snapshot>',
|
||||
default='snapshot',
|
||||
help='type of image (default: snapshot)')
|
||||
@arg('--backup-type',
|
||||
metavar='<daily|weekly>',
|
||||
default=None,
|
||||
help='type of backup')
|
||||
@arg('--rotation',
|
||||
default=None,
|
||||
type=int,
|
||||
metavar='<rotation>',
|
||||
help="Number of backups to retain. Used for backup image_type.")
|
||||
def do_create_image(self, args):
|
||||
"""Create a new image by taking a snapshot of a running server."""
|
||||
server = self._find_server(args.server)
|
||||
server.create_image(args.name)
|
||||
|
||||
@arg('image', metavar='<image>', help='Name or ID of image.')
|
||||
def do_image_delete(self, args):
|
||||
"""
|
||||
Delete an image.
|
||||
|
||||
It should go without saying, but you can only delete images you
|
||||
created.
|
||||
"""
|
||||
image = self._find_image(args.image)
|
||||
image.delete()
|
||||
|
||||
@arg('--hard',
|
||||
dest='reboot_type',
|
||||
action='store_const',
|
||||
const=servers.REBOOT_HARD,
|
||||
default=servers.REBOOT_SOFT,
|
||||
help='Perform a hard reboot (instead of a soft one).')
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_reboot(self, args):
|
||||
"""Reboot a server."""
|
||||
self._find_server(args.server).reboot(args.reboot_type)
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
@arg('image', metavar='<image>', help="Name or ID of new image.")
|
||||
def do_rebuild(self, args):
|
||||
"""Shutdown, re-image, and re-boot a server."""
|
||||
server = self._find_server(args.server)
|
||||
image = self._find_image(args.image)
|
||||
server.rebuild(image)
|
||||
|
||||
@arg('server', metavar='<server>', help='Name (old name) or ID of server.')
|
||||
@arg('name', metavar='<name>', help='New name for the server.')
|
||||
def do_rename(self, args):
|
||||
"""Rename a server."""
|
||||
self._find_server(args.server).update(name=args.name)
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
@arg('flavor', metavar='<flavor>', help="Name or ID of new flavor.")
|
||||
def do_resize(self, args):
|
||||
"""Resize a server."""
|
||||
server = self._find_server(args.server)
|
||||
flavor = self._find_flavor(args.flavor)
|
||||
server.resize(flavor)
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_migrate(self, args):
|
||||
"""Migrate a server."""
|
||||
self._find_server(args.server).migrate()
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_pause(self, args):
|
||||
"""Pause a server."""
|
||||
self._find_server(args.server).pause()
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_unpause(self, args):
|
||||
"""Unpause a server."""
|
||||
self._find_server(args.server).unpause()
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_suspend(self, args):
|
||||
"""Suspend a server."""
|
||||
self._find_server(args.server).suspend()
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_resume(self, args):
|
||||
"""Resume a server."""
|
||||
self._find_server(args.server).resume()
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_rescue(self, args):
|
||||
"""Rescue a server."""
|
||||
self._find_server(args.server).rescue()
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_unrescue(self, args):
|
||||
"""Unrescue a server."""
|
||||
self._find_server(args.server).unrescue()
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_diagnostics(self, args):
|
||||
"""Retrieve server diagnostics."""
|
||||
print_dict(self.cs.servers.diagnostics(args.server)[1])
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_actions(self, args):
|
||||
"""Retrieve server actions."""
|
||||
print_list(
|
||||
self.cs.servers.actions(args.server),
|
||||
["Created_At", "Action", "Error"])
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_resize_confirm(self, args):
|
||||
"""Confirm a previous resize."""
|
||||
self._find_server(args.server).confirm_resize()
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_resize_revert(self, args):
|
||||
"""Revert a previous resize (and return to the previous VM)."""
|
||||
self._find_server(args.server).revert_resize()
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_root_password(self, args):
|
||||
"""
|
||||
Change the root password for a server.
|
||||
"""
|
||||
server = self._find_server(args.server)
|
||||
p1 = getpass.getpass('New password: ')
|
||||
p2 = getpass.getpass('Again: ')
|
||||
if p1 != p2:
|
||||
raise CommandError("Passwords do not match.")
|
||||
server.change_password(p1)
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_show(self, args):
|
||||
"""Show details about the given server."""
|
||||
s = self._find_server(args.server)
|
||||
|
||||
info = s._info.copy()
|
||||
addresses = info.pop('addresses')
|
||||
for network_name in addresses.keys():
|
||||
ips = map(lambda x: x["addr"], addresses[network_name])
|
||||
info['%s ip' % network_name] = ', '.join(ips)
|
||||
|
||||
flavor = info.get('flavor', {})
|
||||
flavor_id = flavor.get('id')
|
||||
if flavor_id:
|
||||
info['flavor'] = self._find_flavor(flavor_id).name
|
||||
image = info.get('image', {})
|
||||
image_id = image.get('id')
|
||||
if image_id:
|
||||
info['image'] = self._find_image(image_id).name
|
||||
|
||||
print_dict(info)
|
||||
|
||||
@arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_delete(self, args):
|
||||
"""Immediately shut down and delete a server."""
|
||||
self._find_server(args.server).delete()
|
||||
|
||||
def _find_server(self, server):
|
||||
"""Get a server by name or ID."""
|
||||
return self._find_resource(self.cs.servers, server)
|
||||
|
||||
def _find_image(self, image):
|
||||
"""Get an image by name or ID."""
|
||||
return self._find_resource(self.cs.images, image)
|
||||
|
||||
def _find_flavor(self, flavor):
|
||||
"""Get a flavor by name, ID, or RAM size."""
|
||||
if keyfile:
|
||||
try:
|
||||
return self._find_resource(self.cs.flavors, flavor)
|
||||
except exceptions.NotFound:
|
||||
return self.cs.flavors.find(ram=flavor)
|
||||
files['/root/.ssh/authorized_keys2'] = open(keyfile)
|
||||
except IOError, e:
|
||||
raise exceptions.CommandError("Can't open '%s': %s" % (keyfile, e))
|
||||
|
||||
def _find_resource(self, manager, name_or_id):
|
||||
"""Helper for the _find_* methods."""
|
||||
try:
|
||||
if isinstance(name_or_id, int) or name_or_id.isdigit():
|
||||
return manager.get(int(name_or_id))
|
||||
|
||||
try:
|
||||
uuid.UUID(name_or_id)
|
||||
return manager.get(name_or_id)
|
||||
except ValueError:
|
||||
return manager.find(name=name_or_id)
|
||||
except exceptions.NotFound:
|
||||
raise CommandError("No %s with a name or ID of '%s' exists." %
|
||||
(manager.resource_class.__name__.lower(), name_or_id))
|
||||
return (args.name, image, flavor, metadata, files)
|
||||
|
||||
|
||||
# I'm picky about my shell help.
|
||||
class OpenStackHelpFormatter(argparse.HelpFormatter):
|
||||
def start_section(self, heading):
|
||||
# Title-case the headings
|
||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||
super(OpenStackHelpFormatter, self).start_section(heading)
|
||||
def do_flavor_list(cs, args):
|
||||
"""Print a list of available 'flavors' (sizes of servers)."""
|
||||
flavors = cs.flavors.list()
|
||||
_translate_flavor_keys(flavors)
|
||||
utils.print_list(flavors, [
|
||||
'ID',
|
||||
'Name',
|
||||
'Memory_MB',
|
||||
'Swap',
|
||||
'Local_GB',
|
||||
'VCPUs',
|
||||
'RXTX_Quota',
|
||||
'RXTX_Cap'])
|
||||
|
||||
def do_image_list(cs, args):
|
||||
"""Print a list of available images to boot from."""
|
||||
utils.print_list(cs.images.list(), ['ID', 'Name', 'Status'])
|
||||
|
||||
# Helpers
|
||||
def print_list(objs, fields, formatters={}):
|
||||
pt = prettytable.PrettyTable([f for f in fields], caching=False)
|
||||
pt.aligns = ['l' for f in fields]
|
||||
@utils.arg('image', metavar='<image>', help='Name or ID of image.')
|
||||
def do_image_delete(cs, args):
|
||||
"""
|
||||
Delete an image.
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
field_name = field.lower().replace(' ', '_')
|
||||
data = getattr(o, field_name, '')
|
||||
row.append(data)
|
||||
pt.add_row(row)
|
||||
It should go without saying, but you can only delete images you
|
||||
created.
|
||||
"""
|
||||
image = _find_image(cs, args.image)
|
||||
image.delete()
|
||||
|
||||
pt.printt(sortby=fields[0])
|
||||
@utils.arg('--fixed_ip',
|
||||
dest='fixed_ip',
|
||||
metavar='<fixed_ip>',
|
||||
default=None,
|
||||
help='Only match against fixed IP.')
|
||||
@utils.arg('--reservation_id',
|
||||
dest='reservation_id',
|
||||
metavar='<reservation_id>',
|
||||
default=None,
|
||||
help='Only return instances that match reservation_id.')
|
||||
@utils.arg('--recurse_zones',
|
||||
dest='recurse_zones',
|
||||
metavar='<0|1>',
|
||||
nargs='?',
|
||||
type=int,
|
||||
const=1,
|
||||
default=0,
|
||||
help='Recurse through all zones if set.')
|
||||
@utils.arg('--ip',
|
||||
dest='ip',
|
||||
metavar='<ip_regexp>',
|
||||
default=None,
|
||||
help='Search with regular expression match by IP address')
|
||||
@utils.arg('--ip6',
|
||||
dest='ip6',
|
||||
metavar='<ip6_regexp>',
|
||||
default=None,
|
||||
help='Search with regular expression match by IPv6 address')
|
||||
@utils.arg('--server_name',
|
||||
dest='server_name',
|
||||
metavar='<name_regexp>',
|
||||
default=None,
|
||||
help='Search with regular expression match by server name')
|
||||
@utils.arg('--name',
|
||||
dest='display_name',
|
||||
metavar='<name_regexp>',
|
||||
default=None,
|
||||
help='Search with regular expression match by display name')
|
||||
@utils.arg('--instance_name',
|
||||
dest='name',
|
||||
metavar='<name_regexp>',
|
||||
default=None,
|
||||
help='Search with regular expression match by instance name')
|
||||
def do_list(cs, args):
|
||||
"""List active servers."""
|
||||
recurse_zones = args.recurse_zones
|
||||
search_opts = {
|
||||
'reservation_id': args.reservation_id,
|
||||
'fixed_ip': args.fixed_ip,
|
||||
'recurse_zones': recurse_zones,
|
||||
'ip': args.ip,
|
||||
'ip6': args.ip6,
|
||||
'name': args.name,
|
||||
'server_name': args.server_name,
|
||||
'display_name': args.display_name}
|
||||
if recurse_zones:
|
||||
to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP']
|
||||
else:
|
||||
to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP']
|
||||
utils.print_list(cs.servers.list(search_opts=search_opts),
|
||||
to_print)
|
||||
|
||||
@utils.arg('--hard',
|
||||
dest='reboot_type',
|
||||
action='store_const',
|
||||
const=servers.REBOOT_HARD,
|
||||
default=servers.REBOOT_SOFT,
|
||||
help='Perform a hard reboot (instead of a soft one).')
|
||||
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_reboot(cs, args):
|
||||
"""Reboot a server."""
|
||||
_find_server(cs, args.server).reboot(args.reboot_type)
|
||||
|
||||
def print_dict(d):
|
||||
pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
|
||||
pt.aligns = ['l', 'l']
|
||||
[pt.add_row(list(r)) for r in d.iteritems()]
|
||||
pt.printt(sortby='Property')
|
||||
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
@utils.arg('image', metavar='<image>', help="Name or ID of new image.")
|
||||
def do_rebuild(cs, args):
|
||||
"""Shutdown, re-image, and re-boot a server."""
|
||||
server = _find_server(cs, args.server)
|
||||
image = _find_image(cs, args.image)
|
||||
server.rebuild(image)
|
||||
|
||||
@utils.arg('server', metavar='<server>', help='Name (old name) or ID of server.')
|
||||
@utils.arg('name', metavar='<name>', help='New name for the server.')
|
||||
def do_rename(cs, args):
|
||||
"""Rename a server."""
|
||||
_find_server(cs, args.server).update(name=args.name)
|
||||
|
||||
def main():
|
||||
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
@utils.arg('flavor', metavar='<flavor>', help="Name or ID of new flavor.")
|
||||
def do_resize(cs, args):
|
||||
"""Resize a server."""
|
||||
server = _find_server(cs, args.server)
|
||||
flavor = _find_flavor(cs, args.flavor)
|
||||
server.resize(flavor)
|
||||
|
||||
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_resize_confirm(cs, args):
|
||||
"""Confirm a previous resize."""
|
||||
_find_server(cs, args.server).confirm_resize()
|
||||
|
||||
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_resize_revert(cs, args):
|
||||
"""Revert a previous resize (and return to the previous VM)."""
|
||||
_find_server(cs, args.server).revert_resize()
|
||||
|
||||
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_root_password(cs, args):
|
||||
"""
|
||||
Change the root password for a server.
|
||||
"""
|
||||
server = _find_server(cs, args.server)
|
||||
p1 = getpass.getpass('New password: ')
|
||||
p2 = getpass.getpass('Again: ')
|
||||
if p1 != p2:
|
||||
raise exceptions.CommandError("Passwords do not match.")
|
||||
server.change_password(p1)
|
||||
|
||||
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
@utils.arg('name', metavar='<name>', help='Name of snapshot.')
|
||||
def do_create_image(cs, args):
|
||||
"""Create a new image by taking a snapshot of a running server."""
|
||||
server = _find_server(cs, args.server)
|
||||
cs.servers.create_image(server, args.name)
|
||||
|
||||
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_show(cs, args):
|
||||
"""Show details about the given server."""
|
||||
s = _find_server(cs, args.server)
|
||||
|
||||
info = s._info.copy()
|
||||
addresses = info.pop('addresses', [])
|
||||
for addrtype in addresses:
|
||||
ips = map(lambda x: x['addr'], addresses[addrtype])
|
||||
info['%s ip' % addrtype] = ', '.join(ips)
|
||||
|
||||
flavor = info.get('flavor', {})
|
||||
flavor_id = flavor.get('id', '')
|
||||
info['flavor'] = _find_flavor(cs, flavor_id).name
|
||||
|
||||
image = info.get('image', {})
|
||||
image_id = image.get('id', '')
|
||||
info['image'] = _find_image(cs, image_id).name
|
||||
|
||||
utils.print_dict(info)
|
||||
|
||||
@utils.arg('server', metavar='<server>', help='Name or ID of server.')
|
||||
def do_delete(cs, args):
|
||||
"""Immediately shut down and delete a server."""
|
||||
_find_server(cs, args.server).delete()
|
||||
|
||||
def _find_server(cs, server):
|
||||
"""Get a server by name or ID."""
|
||||
return _find_resource(cs.servers, server)
|
||||
|
||||
def _find_image(cs, image):
|
||||
"""Get an image by name or ID."""
|
||||
return _find_resource(cs.images, image)
|
||||
|
||||
def _find_flavor(cs, flavor):
|
||||
"""Get a flavor by name, ID, or RAM size."""
|
||||
try:
|
||||
OpenStackShell().main(sys.argv[1:])
|
||||
return _find_resource(cs.flavors, flavor)
|
||||
except exceptions.NotFound:
|
||||
return cs.flavors.find(ram=flavor)
|
||||
|
||||
def _find_resource(manager, name_or_id):
|
||||
"""Helper for the _find_* methods."""
|
||||
try:
|
||||
if isinstance(name_or_id, int) or name_or_id.isdigit():
|
||||
return manager.get(int(name_or_id))
|
||||
|
||||
try:
|
||||
uuid.UUID(name_or_id)
|
||||
return manager.get(name_or_id)
|
||||
except ValueError:
|
||||
return manager.find(name=name_or_id)
|
||||
except exceptions.NotFound:
|
||||
raise exceptions.CommandError("No %s with a name or ID of '%s' exists." %
|
||||
(manager.resource_class.__name__.lower(), name_or_id))
|
||||
|
||||
except Exception, e:
|
||||
if httplib2.debuglevel == 1:
|
||||
raise # dump stack.
|
||||
else:
|
||||
print >> sys.stderr, e
|
||||
sys.exit(1)
|
||||
|
66
tests/fakes.py
Normal file
66
tests/fakes.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""
|
||||
A fake server that "responds" to API methods with pre-canned responses.
|
||||
|
||||
All of these responses come from the spec, so if for some reason the spec's
|
||||
wrong the tests might raise AssertionError. I've indicated in comments the places where actual
|
||||
behavior differs from the spec.
|
||||
"""
|
||||
|
||||
import novaclient.client
|
||||
|
||||
|
||||
def assert_has_keys(dict, required=[], optional=[]):
|
||||
keys = dict.keys()
|
||||
for k in required:
|
||||
assert k in keys
|
||||
allowed_keys = set(required) | set(optional)
|
||||
extra_keys = set(keys).difference(set(required + optional))
|
||||
if extra_keys:
|
||||
raise AssertionError("found unexpected keys: %s" % list(extra_keys))
|
||||
|
||||
|
||||
class FakeClient(object):
|
||||
|
||||
def assert_called(self, method, url, body=None):
|
||||
"""
|
||||
Assert than an API method was just called.
|
||||
"""
|
||||
expected = (method, url)
|
||||
called = self.client.callstack[-1][0:2]
|
||||
|
||||
assert self.client.callstack, \
|
||||
"Expected %s %s but no calls were made." % expected
|
||||
|
||||
assert expected == called, 'Expected %s %s; got %s %s' % \
|
||||
(expected + called)
|
||||
|
||||
if body is not None:
|
||||
assert self.client.callstack[-1][2] == body
|
||||
|
||||
self.client.callstack = []
|
||||
|
||||
def assert_called_anytime(self, method, url, body=None):
|
||||
"""
|
||||
Assert than an API method was called anytime in the test.
|
||||
"""
|
||||
expected = (method, url)
|
||||
|
||||
assert self.client.callstack, \
|
||||
"Expected %s %s but no calls were made." % expected
|
||||
|
||||
found = False
|
||||
for entry in self.client.callstack:
|
||||
called = entry[0:2]
|
||||
if expected == entry[0:2]:
|
||||
found = True
|
||||
break
|
||||
|
||||
assert found, 'Expected %s %s; got %s' % \
|
||||
(expected, self.client.callstack)
|
||||
if body is not None:
|
||||
assert entry[2] == body
|
||||
|
||||
self.client.callstack = []
|
||||
|
||||
def authenticate(self):
|
||||
pass
|
60
tests/test_base.py
Normal file
60
tests/test_base.py
Normal file
@ -0,0 +1,60 @@
|
||||
|
||||
import mock
|
||||
|
||||
from novaclient import base
|
||||
from novaclient import exceptions
|
||||
from novaclient.v1_0 import flavors
|
||||
from tests.v1_0 import fakes
|
||||
from tests import utils
|
||||
|
||||
|
||||
cs = fakes.FakeClient()
|
||||
|
||||
|
||||
class BaseTest(utils.TestCase):
|
||||
|
||||
def test_resource_repr(self):
|
||||
r = base.Resource(None, dict(foo="bar", baz="spam"))
|
||||
self.assertEqual(repr(r), "<Resource baz=spam, foo=bar>")
|
||||
|
||||
def test_getid(self):
|
||||
self.assertEqual(base.getid(4), 4)
|
||||
|
||||
class TmpObject(object):
|
||||
id = 4
|
||||
self.assertEqual(base.getid(TmpObject), 4)
|
||||
|
||||
def test_resource_lazy_getattr(self):
|
||||
f = flavors.Flavor(cs.flavors, {'id': 1})
|
||||
self.assertEqual(f.name, '256 MB Server')
|
||||
cs.assert_called('GET', '/flavors/1')
|
||||
|
||||
# Missing stuff still fails after a second get
|
||||
self.assertRaises(AttributeError, getattr, f, 'blahblah')
|
||||
cs.assert_called('GET', '/flavors/1')
|
||||
|
||||
def test_eq(self):
|
||||
# Two resources of the same type with the same id: equal
|
||||
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
|
||||
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
# Two resoruces of different types: never equal
|
||||
r1 = base.Resource(None, {'id': 1})
|
||||
r2 = flavors.Flavor(None, {'id': 1})
|
||||
self.assertNotEqual(r1, r2)
|
||||
|
||||
# Two resources with no ID: equal if their info is equal
|
||||
r1 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
def test_findall_invalid_attribute(self):
|
||||
# Make sure findall with an invalid attribute doesn't cause errors.
|
||||
# The following should not raise an exception.
|
||||
cs.flavors.findall(vegetable='carrot')
|
||||
|
||||
# However, find() should raise an error
|
||||
self.assertRaises(exceptions.NotFound,
|
||||
cs.flavors.find,
|
||||
vegetable='carrot')
|
58
tests/test_http.py
Normal file
58
tests/test_http.py
Normal file
@ -0,0 +1,58 @@
|
||||
|
||||
import httplib2
|
||||
import mock
|
||||
|
||||
from novaclient import client
|
||||
from tests import utils
|
||||
|
||||
|
||||
fake_response = httplib2.Response({"status": 200})
|
||||
fake_body = '{"hi": "there"}'
|
||||
mock_request = mock.Mock(return_value=(fake_response, fake_body))
|
||||
|
||||
|
||||
def get_client():
|
||||
cl = client.HTTPClient("username", "apikey",
|
||||
"project_id", "auth_test")
|
||||
cl.management_url = "http://example.com"
|
||||
cl.auth_token = "token"
|
||||
return cl
|
||||
|
||||
|
||||
class ClientTest(utils.TestCase):
|
||||
|
||||
def test_get(self):
|
||||
cl = get_client()
|
||||
|
||||
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||
@mock.patch('time.time', mock.Mock(return_value=1234))
|
||||
def test_get_call():
|
||||
resp, body = cl.get("/hi")
|
||||
headers={"X-Auth-Token": "token",
|
||||
"X-Auth-Project-Id": "project_id",
|
||||
"User-Agent": cl.USER_AGENT,
|
||||
}
|
||||
mock_request.assert_called_with("http://example.com/hi?fresh=1234",
|
||||
"GET", headers=headers)
|
||||
# Automatic JSON parsing
|
||||
self.assertEqual(body, {"hi": "there"})
|
||||
|
||||
test_get_call()
|
||||
|
||||
|
||||
def test_post(self):
|
||||
cl = get_client()
|
||||
|
||||
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||
def test_post_call():
|
||||
cl.post("/hi", body=[1, 2, 3])
|
||||
headers={
|
||||
"X-Auth-Token": "token",
|
||||
"X-Auth-Project-Id": "project_id",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": cl.USER_AGENT
|
||||
}
|
||||
mock_request.assert_called_with("http://example.com/hi", "POST",
|
||||
headers=headers, body='[1, 2, 3]')
|
||||
|
||||
test_post_call()
|
39
tests/test_shell.py
Normal file
39
tests/test_shell.py
Normal file
@ -0,0 +1,39 @@
|
||||
|
||||
import os
|
||||
import mock
|
||||
import httplib2
|
||||
|
||||
from novaclient.shell import OpenStackComputeShell
|
||||
from novaclient import exceptions
|
||||
from tests import utils
|
||||
|
||||
|
||||
class ShellTest(utils.TestCase):
|
||||
|
||||
# Patch os.environ to avoid required auth info.
|
||||
def setUp(self):
|
||||
global _old_env
|
||||
fake_env = {
|
||||
'NOVA_USERNAME': 'username',
|
||||
'NOVA_API_KEY': 'password',
|
||||
'NOVA_PROJECT_ID': 'project_id'
|
||||
}
|
||||
_old_env, os.environ = os.environ, fake_env.copy()
|
||||
|
||||
# Make a fake shell object, a helping wrapper to call it, and a quick way
|
||||
# of asserting that certain API calls were made.
|
||||
global shell, _shell, assert_called, assert_called_anytime
|
||||
_shell = OpenStackComputeShell()
|
||||
shell = lambda cmd: _shell.main(cmd.split())
|
||||
|
||||
def tearDown(self):
|
||||
global _old_env
|
||||
os.environ = _old_env
|
||||
|
||||
def test_help_unknown_command(self):
|
||||
self.assertRaises(exceptions.CommandError, shell, 'help foofoo')
|
||||
|
||||
def test_debug(self):
|
||||
httplib2.debuglevel = 0
|
||||
shell('--debug help')
|
||||
assert httplib2.debuglevel == 1
|
5
tests/utils.py
Normal file
5
tests/utils.py
Normal file
@ -0,0 +1,5 @@
|
||||
import unittest
|
||||
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
pass
|
@ -1,82 +1,22 @@
|
||||
"""
|
||||
A fake server that "responds" to API methods with pre-canned responses.
|
||||
|
||||
All of these responses come from the spec, so if for some reason the spec's
|
||||
wrong the tests might fail. I've indicated in comments the places where actual
|
||||
behavior differs from the spec.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import urlparse
|
||||
import urllib
|
||||
|
||||
import httplib2
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
from novaclient.v1_0 import Client
|
||||
from novaclient.v1_0.client import HTTPClient
|
||||
|
||||
from .utils import fail, assert_in, assert_not_in, assert_has_keys
|
||||
from novaclient import client as base_client
|
||||
from novaclient.v1_0 import client
|
||||
from tests import fakes
|
||||
|
||||
|
||||
def assert_equal(value_one, value_two):
|
||||
assert value_one == value_two
|
||||
class FakeClient(fakes.FakeClient, client.Client):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
client.Client.__init__(self, 'username', 'apikey',
|
||||
'project_id', 'auth_url')
|
||||
self.client = FakeHTTPClient(**kwargs)
|
||||
|
||||
|
||||
class FakeClient(Client):
|
||||
def __init__(self, username=None, password=None, project_id=None,
|
||||
auth_url=None):
|
||||
super(FakeClient, self).__init__('username', 'apikey',
|
||||
'project_id', 'auth_url')
|
||||
self.client = FakeHTTPClient()
|
||||
|
||||
def assert_called(self, method, url, body=None):
|
||||
"""
|
||||
Assert than an API method was just called.
|
||||
"""
|
||||
expected = (method, url)
|
||||
called = self.client.callstack[-1][0:2]
|
||||
|
||||
assert self.client.callstack, \
|
||||
"Expected %s %s but no calls were made." % expected
|
||||
|
||||
assert expected == called, 'Expected %s %s; got %s %s' % \
|
||||
(expected + called)
|
||||
|
||||
if body is not None:
|
||||
assert_equal(self.client.callstack[-1][2], body)
|
||||
|
||||
self.client.callstack = []
|
||||
|
||||
def assert_called_anytime(self, method, url, body=None):
|
||||
"""
|
||||
Assert than an API method was called anytime in the test.
|
||||
"""
|
||||
expected = (method, url)
|
||||
|
||||
assert self.client.callstack, \
|
||||
"Expected %s %s but no calls were made." % expected
|
||||
|
||||
found = False
|
||||
for entry in self.client.callstack:
|
||||
called = entry[0:2]
|
||||
if expected == entry[0:2]:
|
||||
found = True
|
||||
break
|
||||
|
||||
assert found, 'Expected %s; got %s' % \
|
||||
(expected, self.client.callstack)
|
||||
if body is not None:
|
||||
assert_equal(entry[2], body)
|
||||
|
||||
self.client.callstack = []
|
||||
|
||||
def authenticate(self):
|
||||
pass
|
||||
|
||||
|
||||
class FakeHTTPClient(HTTPClient):
|
||||
def __init__(self):
|
||||
class FakeHTTPClient(base_client.HTTPClient):
|
||||
def __init__(self, **kwargs):
|
||||
self.username = 'username'
|
||||
self.apikey = 'apikey'
|
||||
self.auth_url = 'auth_url'
|
||||
@ -85,15 +25,15 @@ class FakeHTTPClient(HTTPClient):
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
# Check that certain things are called correctly
|
||||
if method in ['GET', 'DELETE']:
|
||||
assert_not_in('body', kwargs)
|
||||
assert 'body' not in kwargs
|
||||
elif method in ['PUT', 'POST']:
|
||||
assert_in('body', kwargs)
|
||||
assert 'body' in kwargs
|
||||
|
||||
# Call the method
|
||||
munged_url = url.strip('/').replace('/', '_').replace('.', '_')
|
||||
callback = "%s_%s" % (method.lower(), munged_url)
|
||||
if not hasattr(self, callback):
|
||||
fail('Called unknown API method: %s %s' % (method, url))
|
||||
raise AssertionError('Called unknown API method: %s %s' % (method, url))
|
||||
|
||||
# Note the call
|
||||
self.callstack.append((method, url, kwargs.get('body', None)))
|
||||
@ -210,14 +150,14 @@ class FakeHTTPClient(HTTPClient):
|
||||
]})
|
||||
|
||||
def post_servers(self, body, **kw):
|
||||
assert_equal(body.keys(), ['server'])
|
||||
assert_has_keys(body['server'],
|
||||
required=['name', 'imageId', 'flavorId'],
|
||||
optional=['sharedIpGroupId', 'metadata',
|
||||
'personality', 'min_count', 'max_count'])
|
||||
assert body.keys() == ['server']
|
||||
fakes.assert_has_keys(body['server'],
|
||||
required=['name', 'imageId', 'flavorId'],
|
||||
optional=['sharedIpGroupId', 'metadata',
|
||||
'personality', 'min_count', 'max_count'])
|
||||
if 'personality' in body['server']:
|
||||
for pfile in body['server']['personality']:
|
||||
assert_has_keys(pfile, required=['path', 'contents'])
|
||||
fakes.assert_has_keys(pfile, required=['path', 'contents'])
|
||||
return (202, self.get_servers_1234()[1])
|
||||
|
||||
def get_servers_1234(self, **kw):
|
||||
@ -229,8 +169,8 @@ class FakeHTTPClient(HTTPClient):
|
||||
return (200, r)
|
||||
|
||||
def put_servers_1234(self, body, **kw):
|
||||
assert_equal(body.keys(), ['server'])
|
||||
assert_has_keys(body['server'], optional=['name', 'adminPass'])
|
||||
assert body.keys() == ['server']
|
||||
fakes.assert_has_keys(body['server'], optional=['name', 'adminPass'])
|
||||
return (204, None)
|
||||
|
||||
def delete_servers_1234(self, **kw):
|
||||
@ -253,8 +193,8 @@ class FakeHTTPClient(HTTPClient):
|
||||
self.get_servers_1234_ips()[1]['addresses']['private']})
|
||||
|
||||
def put_servers_1234_ips_public_1_2_3_4(self, body, **kw):
|
||||
assert_equal(body.keys(), ['shareIp'])
|
||||
assert_has_keys(body['shareIp'], required=['sharedIpGroupId',
|
||||
assert body.keys() == ['shareIp']
|
||||
fakes.assert_has_keys(body['shareIp'], required=['sharedIpGroupId',
|
||||
'configureServer'])
|
||||
return (202, None)
|
||||
|
||||
@ -266,29 +206,32 @@ class FakeHTTPClient(HTTPClient):
|
||||
#
|
||||
|
||||
def post_servers_1234_action(self, body, **kw):
|
||||
assert_equal(len(body.keys()), 1)
|
||||
assert len(body.keys()) == 1
|
||||
action = body.keys()[0]
|
||||
if action == 'reboot':
|
||||
assert_equal(body[action].keys(), ['type'])
|
||||
assert_in(body[action]['type'], ['HARD', 'SOFT'])
|
||||
assert body[action].keys() == ['type']
|
||||
assert body[action]['type'] in ['HARD', 'SOFT']
|
||||
elif action == 'rebuild':
|
||||
assert_equal(body[action].keys(), ['imageId'])
|
||||
assert body[action].keys() == ['imageId']
|
||||
elif action == 'resize':
|
||||
assert_equal(body[action].keys(), ['flavorId'])
|
||||
assert body[action].keys() == ['flavorId']
|
||||
elif action == 'createBackup':
|
||||
assert set(body[action].keys()) == \
|
||||
set(['name', 'rotation', 'backup_type'])
|
||||
elif action == 'confirmResize':
|
||||
assert_equal(body[action], None)
|
||||
assert body[action] is None
|
||||
# This one method returns a different response code
|
||||
return (204, None)
|
||||
elif action == 'revertResize':
|
||||
assert_equal(body[action], None)
|
||||
assert body[action] is None
|
||||
elif action == 'migrate':
|
||||
assert_equal(body[action], None)
|
||||
assert body[action] is None
|
||||
elif action == 'addFixedIp':
|
||||
assert_equal(body[action].keys(), ['networkId'])
|
||||
assert body[action].keys() == ['networkId']
|
||||
elif action == 'removeFixedIp':
|
||||
assert_equal(body[action].keys(), ['address'])
|
||||
assert body[action].keys() == ['address']
|
||||
else:
|
||||
fail("Unexpected server action: %s" % action)
|
||||
raise AssertionError("Unexpected server action: %s" % action)
|
||||
return (202, None)
|
||||
|
||||
#
|
||||
@ -349,8 +292,8 @@ class FakeHTTPClient(HTTPClient):
|
||||
return (200, {'image': self.get_images_detail()[1]['images'][1]})
|
||||
|
||||
def post_images(self, body, **kw):
|
||||
assert_equal(body.keys(), ['image'])
|
||||
assert_has_keys(body['image'], required=['serverId', 'name', 'image_type', 'backup_type', 'rotation'])
|
||||
assert body.keys() == ['image']
|
||||
fakes.assert_has_keys(body['image'], required=['serverId', 'name'])
|
||||
return (202, self.get_images_1()[1])
|
||||
|
||||
def delete_images_1(self, **kw):
|
||||
@ -367,8 +310,8 @@ class FakeHTTPClient(HTTPClient):
|
||||
}})
|
||||
|
||||
def post_servers_1234_backup_schedule(self, body, **kw):
|
||||
assert_equal(body.keys(), ['backupSchedule'])
|
||||
assert_has_keys(body['backupSchedule'], required=['enabled'],
|
||||
assert body.keys() == ['backupSchedule']
|
||||
fakes.assert_has_keys(body['backupSchedule'], required=['enabled'],
|
||||
optional=['weekly', 'daily'])
|
||||
return (204, None)
|
||||
|
||||
@ -395,8 +338,8 @@ class FakeHTTPClient(HTTPClient):
|
||||
self.get_shared_ip_groups_detail()[1]['sharedIpGroups'][0]})
|
||||
|
||||
def post_shared_ip_groups(self, body, **kw):
|
||||
assert_equal(body.keys(), ['sharedIpGroup'])
|
||||
assert_has_keys(body['sharedIpGroup'], required=['name'],
|
||||
assert body.keys() == ['sharedIpGroup']
|
||||
fakes.assert_has_keys(body['sharedIpGroup'], required=['name'],
|
||||
optional=['server'])
|
||||
return (201, {'sharedIpGroup': {
|
||||
'id': 10101,
|
||||
@ -417,7 +360,6 @@ class FakeHTTPClient(HTTPClient):
|
||||
{'id': 2, 'api_url': 'http://foo.com', 'username': 'alice'},
|
||||
]})
|
||||
|
||||
|
||||
def get_zones_detail(self, **kw):
|
||||
return (200, {'zones': [
|
||||
{'id': 1, 'api_url': 'http://foo.com', 'username': 'bob',
|
||||
@ -435,16 +377,16 @@ class FakeHTTPClient(HTTPClient):
|
||||
return (200, r)
|
||||
|
||||
def post_zones(self, body, **kw):
|
||||
assert_equal(body.keys(), ['zone'])
|
||||
assert_has_keys(body['zone'],
|
||||
assert body.keys() == ['zone']
|
||||
fakes.assert_has_keys(body['zone'],
|
||||
required=['api_url', 'username', 'password'],
|
||||
optional=['weight_offset', 'weight_scale'])
|
||||
|
||||
return (202, self.get_zones_1()[1])
|
||||
|
||||
def put_zones_1(self, body, **kw):
|
||||
assert_equal(body.keys(), ['zone'])
|
||||
assert_has_keys(body['zone'], optional=['api_url', 'username',
|
||||
assert body.keys() == ['zone']
|
||||
fakes.assert_has_keys(body['zone'], optional=['api_url', 'username',
|
||||
'password',
|
||||
'weight_offset',
|
||||
'weight_scale'])
|
||||
@ -457,12 +399,14 @@ class FakeHTTPClient(HTTPClient):
|
||||
# Accounts
|
||||
#
|
||||
def post_accounts_test_account_create_instance(self, body, **kw):
|
||||
assert_equal(body.keys(), ['server'])
|
||||
assert_has_keys(body['server'],
|
||||
assert body.keys() == ['server']
|
||||
fakes.assert_has_keys(body['server'],
|
||||
required=['name', 'imageId', 'flavorId'],
|
||||
optional=['sharedIpGroupId', 'metadata',
|
||||
'personality', 'min_count', 'max_count'])
|
||||
if 'personality' in body['server']:
|
||||
for pfile in body['server']['personality']:
|
||||
assert_has_keys(pfile, required=['path', 'contents'])
|
||||
fakes.assert_has_keys(pfile, required=['path', 'contents'])
|
||||
return (202, self.get_servers_1234()[1])
|
||||
|
||||
|
||||
|
@ -1,21 +1,25 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import StringIO
|
||||
|
||||
from .fakes import FakeClient
|
||||
from tests.v1_0 import fakes
|
||||
from tests import utils
|
||||
|
||||
os = FakeClient()
|
||||
|
||||
def test_instance_creation_for_account():
|
||||
s = os.accounts.create_instance_for(
|
||||
account_id='test_account',
|
||||
name="My server",
|
||||
image=1,
|
||||
flavor=1,
|
||||
meta={'foo': 'bar'},
|
||||
ipgroup=1,
|
||||
files={
|
||||
'/etc/passwd': 'some data', # a file
|
||||
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
|
||||
})
|
||||
os.assert_called('POST', '/accounts/test_account/create_instance')
|
||||
cs = fakes.FakeClient()
|
||||
|
||||
|
||||
class AccountsTest(utils.TestCase):
|
||||
|
||||
def test_instance_creation_for_account(self):
|
||||
s = cs.accounts.create_instance_for(
|
||||
account_id='test_account',
|
||||
name="My server",
|
||||
image=1,
|
||||
flavor=1,
|
||||
meta={'foo': 'bar'},
|
||||
ipgroup=1,
|
||||
files={
|
||||
'/etc/passwd': 'some data', # a file
|
||||
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
|
||||
})
|
||||
cs.assert_called('POST', '/accounts/test_account/create_instance')
|
||||
|
@ -1,71 +1,74 @@
|
||||
import mock
|
||||
|
||||
import httplib2
|
||||
from nose.tools import assert_raises, assert_equal
|
||||
import mock
|
||||
|
||||
import novaclient.v1_0
|
||||
from novaclient.v1_0 import exceptions
|
||||
from novaclient.v1_0 import client
|
||||
from novaclient import exceptions
|
||||
from tests import utils
|
||||
|
||||
|
||||
def test_authenticate_success():
|
||||
cs = novaclient.v1_0.Client("username", "apikey", "project_id")
|
||||
auth_response = httplib2.Response({
|
||||
'status': 204,
|
||||
'x-server-management-url':
|
||||
'https://servers.api.rackspacecloud.com/v1.0/443470',
|
||||
'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
|
||||
})
|
||||
mock_request = mock.Mock(return_value=(auth_response, None))
|
||||
class AuthenticationTests(utils.TestCase):
|
||||
|
||||
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||
def test_auth_call():
|
||||
cs.client.authenticate()
|
||||
mock_request.assert_called_with(cs.client.auth_url, 'GET',
|
||||
def test_authenticate_success(self):
|
||||
cs = client.Client("username", "apikey", "project_id")
|
||||
management_url = 'https://servers.api.rackspacecloud.com/v1.0/443470'
|
||||
auth_response = httplib2.Response({
|
||||
'status': 204,
|
||||
'x-server-management-url': management_url,
|
||||
'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
|
||||
})
|
||||
mock_request = mock.Mock(return_value=(auth_response, None))
|
||||
|
||||
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||
def test_auth_call():
|
||||
cs.client.authenticate()
|
||||
headers={
|
||||
'X-Auth-User': 'username',
|
||||
'X-Auth-Key': 'apikey',
|
||||
'X-Auth-Project-Id': 'project_id',
|
||||
'User-Agent': cs.client.USER_AGENT
|
||||
})
|
||||
assert_equal(cs.client.management_url,
|
||||
auth_response['x-server-management-url'])
|
||||
assert_equal(cs.client.auth_token, auth_response['x-auth-token'])
|
||||
}
|
||||
mock_request.assert_called_with(cs.client.auth_url, 'GET',
|
||||
headers=headers)
|
||||
self.assertEqual(cs.client.management_url,
|
||||
auth_response['x-server-management-url'])
|
||||
self.assertEqual(cs.client.auth_token,
|
||||
auth_response['x-auth-token'])
|
||||
|
||||
test_auth_call()
|
||||
test_auth_call()
|
||||
|
||||
def test_authenticate_failure(self):
|
||||
cs = client.Client("username", "apikey", "project_id")
|
||||
auth_response = httplib2.Response({'status': 401})
|
||||
mock_request = mock.Mock(return_value=(auth_response, None))
|
||||
|
||||
def test_authenticate_failure():
|
||||
cs = novaclient.v1_0.Client("username", "apikey", "project_id")
|
||||
auth_response = httplib2.Response({'status': 401})
|
||||
mock_request = mock.Mock(return_value=(auth_response, None))
|
||||
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||
def test_auth_call():
|
||||
self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
|
||||
|
||||
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||
def test_auth_call():
|
||||
assert_raises(exceptions.Unauthorized, cs.client.authenticate)
|
||||
test_auth_call()
|
||||
|
||||
test_auth_call()
|
||||
def test_auth_automatic(self):
|
||||
cs = client.Client("username", "apikey", "project_id")
|
||||
http_client = cs.client
|
||||
http_client.management_url = ''
|
||||
mock_request = mock.Mock(return_value=(None, None))
|
||||
|
||||
@mock.patch.object(http_client, 'request', mock_request)
|
||||
@mock.patch.object(http_client, 'authenticate')
|
||||
def test_auth_call(m):
|
||||
http_client.get('/')
|
||||
m.assert_called()
|
||||
mock_request.assert_called()
|
||||
|
||||
def test_auth_automatic():
|
||||
client = novaclient.v1_0.Client("username", "apikey", "project_id").client
|
||||
client.management_url = ''
|
||||
mock_request = mock.Mock(return_value=(None, None))
|
||||
test_auth_call()
|
||||
|
||||
@mock.patch.object(client, 'request', mock_request)
|
||||
@mock.patch.object(client, 'authenticate')
|
||||
def test_auth_call(m):
|
||||
client.get('/')
|
||||
m.assert_called()
|
||||
mock_request.assert_called()
|
||||
def test_auth_manual(self):
|
||||
cs = client.Client("username", "apikey", "project_id")
|
||||
|
||||
test_auth_call()
|
||||
@mock.patch.object(cs.client, 'authenticate')
|
||||
def test_auth_call(m):
|
||||
cs.authenticate()
|
||||
m.assert_called()
|
||||
|
||||
|
||||
def test_auth_manual():
|
||||
cs = novaclient.v1_0.Client("username", "apikey", "project_id")
|
||||
|
||||
@mock.patch.object(cs.client, 'authenticate')
|
||||
def test_auth_call(m):
|
||||
cs.authenticate()
|
||||
m.assert_called()
|
||||
|
||||
test_auth_call()
|
||||
test_auth_call()
|
||||
|
@ -1,60 +1,60 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from novaclient.v1_0 import backup_schedules
|
||||
|
||||
from .fakes import FakeClient
|
||||
from .utils import assert_isinstance
|
||||
|
||||
os = FakeClient()
|
||||
from tests.v1_0 import fakes
|
||||
from tests import utils
|
||||
|
||||
|
||||
def test_get_backup_schedule():
|
||||
s = os.servers.get(1234)
|
||||
|
||||
# access via manager
|
||||
b = os.backup_schedules.get(server=s)
|
||||
assert_isinstance(b, backup_schedules.BackupSchedule)
|
||||
os.assert_called('GET', '/servers/1234/backup_schedule')
|
||||
|
||||
b = os.backup_schedules.get(server=1234)
|
||||
assert_isinstance(b, backup_schedules.BackupSchedule)
|
||||
os.assert_called('GET', '/servers/1234/backup_schedule')
|
||||
|
||||
# access via instance
|
||||
assert_isinstance(s.backup_schedule, backup_schedules.BackupSchedule)
|
||||
os.assert_called('GET', '/servers/1234/backup_schedule')
|
||||
|
||||
# Just for coverage's sake
|
||||
b = s.backup_schedule.get()
|
||||
os.assert_called('GET', '/servers/1234/backup_schedule')
|
||||
cs = fakes.FakeClient()
|
||||
|
||||
|
||||
def test_create_update_backup_schedule():
|
||||
s = os.servers.get(1234)
|
||||
class BackupSchedulesTest(utils.TestCase):
|
||||
|
||||
# create/update via manager
|
||||
os.backup_schedules.update(
|
||||
server=s,
|
||||
enabled=True,
|
||||
weekly=backup_schedules.BACKUP_WEEKLY_THURSDAY,
|
||||
daily=backup_schedules.BACKUP_DAILY_H_1000_1200
|
||||
)
|
||||
os.assert_called('POST', '/servers/1234/backup_schedule')
|
||||
def test_get_backup_schedule(self):
|
||||
s = cs.servers.get(1234)
|
||||
|
||||
# and via instance
|
||||
s.backup_schedule.update(enabled=False)
|
||||
os.assert_called('POST', '/servers/1234/backup_schedule')
|
||||
# access via manager
|
||||
b = cs.backup_schedules.get(server=s)
|
||||
self.assertTrue(isinstance(b, backup_schedules.BackupSchedule))
|
||||
cs.assert_called('GET', '/servers/1234/backup_schedule')
|
||||
|
||||
b = cs.backup_schedules.get(server=1234)
|
||||
self.assertTrue(isinstance(b, backup_schedules.BackupSchedule))
|
||||
cs.assert_called('GET', '/servers/1234/backup_schedule')
|
||||
|
||||
def test_delete_backup_schedule():
|
||||
s = os.servers.get(1234)
|
||||
# access via instance
|
||||
self.assertTrue(isinstance(s.backup_schedule,
|
||||
backup_schedules.BackupSchedule))
|
||||
cs.assert_called('GET', '/servers/1234/backup_schedule')
|
||||
|
||||
# delete via manager
|
||||
os.backup_schedules.delete(s)
|
||||
os.assert_called('DELETE', '/servers/1234/backup_schedule')
|
||||
os.backup_schedules.delete(1234)
|
||||
os.assert_called('DELETE', '/servers/1234/backup_schedule')
|
||||
# Just for coverage's sake
|
||||
b = s.backup_schedule.get()
|
||||
cs.assert_called('GET', '/servers/1234/backup_schedule')
|
||||
|
||||
# and via instance
|
||||
s.backup_schedule.delete()
|
||||
os.assert_called('DELETE', '/servers/1234/backup_schedule')
|
||||
def test_create_update_backup_schedule(self):
|
||||
s = cs.servers.get(1234)
|
||||
|
||||
# create/update via manager
|
||||
cs.backup_schedules.update(
|
||||
server=s,
|
||||
enabled=True,
|
||||
weekly=backup_schedules.BACKUP_WEEKLY_THURSDAY,
|
||||
daily=backup_schedules.BACKUP_DAILY_H_1000_1200
|
||||
)
|
||||
cs.assert_called('POST', '/servers/1234/backup_schedule')
|
||||
|
||||
# and via instance
|
||||
s.backup_schedule.update(enabled=False)
|
||||
cs.assert_called('POST', '/servers/1234/backup_schedule')
|
||||
|
||||
def test_delete_backup_schedule(self):
|
||||
s = cs.servers.get(1234)
|
||||
|
||||
# delete via manager
|
||||
cs.backup_schedules.delete(s)
|
||||
cs.assert_called('DELETE', '/servers/1234/backup_schedule')
|
||||
cs.backup_schedules.delete(1234)
|
||||
cs.assert_called('DELETE', '/servers/1234/backup_schedule')
|
||||
|
||||
# and via instance
|
||||
s.backup_schedule.delete()
|
||||
cs.assert_called('DELETE', '/servers/1234/backup_schedule')
|
||||
|
@ -1,61 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import mock
|
||||
from nose.tools import assert_equal, assert_not_equal, assert_raises
|
||||
|
||||
from novaclient.v1_0 import flavors
|
||||
from novaclient.v1_0 import exceptions
|
||||
from novaclient.v1_0 import base
|
||||
|
||||
from .fakes import FakeClient
|
||||
|
||||
os = FakeClient()
|
||||
|
||||
|
||||
def test_resource_repr():
|
||||
r = base.Resource(None, dict(foo="bar", baz="spam"))
|
||||
assert_equal(repr(r), "<Resource baz=spam, foo=bar>")
|
||||
|
||||
|
||||
def test_getid():
|
||||
assert_equal(base.getid(4), 4)
|
||||
|
||||
class O(object):
|
||||
id = 4
|
||||
assert_equal(base.getid(O), 4)
|
||||
|
||||
|
||||
def test_resource_lazy_getattr():
|
||||
f = flavors.Flavor(os.flavors, {'id': 1})
|
||||
assert_equal(f.name, '256 MB Server')
|
||||
os.assert_called('GET', '/flavors/1')
|
||||
|
||||
# Missing stuff still fails after a second get
|
||||
assert_raises(AttributeError, getattr, f, 'blahblah')
|
||||
os.assert_called('GET', '/flavors/1')
|
||||
|
||||
|
||||
def test_eq():
|
||||
# Two resources of the same type with the same id: equal
|
||||
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
|
||||
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
|
||||
assert_equal(r1, r2)
|
||||
|
||||
# Two resoruces of different types: never equal
|
||||
r1 = base.Resource(None, {'id': 1})
|
||||
r2 = flavors.Flavor(None, {'id': 1})
|
||||
assert_not_equal(r1, r2)
|
||||
|
||||
# Two resources with no ID: equal if their info is equal
|
||||
r1 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
assert_equal(r1, r2)
|
||||
|
||||
|
||||
def test_findall_invalid_attribute():
|
||||
# Make sure findall with an invalid attribute doesn't cause errors.
|
||||
# The following should not raise an exception.
|
||||
os.flavors.findall(vegetable='carrot')
|
||||
|
||||
# However, find() should raise an error
|
||||
assert_raises(exceptions.NotFound, os.flavors.find, vegetable='carrot')
|
@ -1,52 +0,0 @@
|
||||
import mock
|
||||
import httplib2
|
||||
|
||||
from novaclient.v1_0 import client
|
||||
from nose.tools import assert_equal
|
||||
|
||||
fake_response = httplib2.Response({"status": 200})
|
||||
fake_body = '{"hi": "there"}'
|
||||
mock_request = mock.Mock(return_value=(fake_response, fake_body))
|
||||
|
||||
|
||||
def get_client():
|
||||
cl = client.HTTPClient("username", "apikey", "project_id", "auth_test")
|
||||
cl.management_url = "http://example.com"
|
||||
cl.auth_token = "token"
|
||||
return cl
|
||||
|
||||
|
||||
def test_get():
|
||||
cl = get_client()
|
||||
|
||||
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||
@mock.patch('time.time', mock.Mock(return_value=1234))
|
||||
def test_get_call():
|
||||
resp, body = cl.get("/hi")
|
||||
mock_request.assert_called_with("http://example.com/hi?fresh=1234",
|
||||
"GET",
|
||||
headers={"X-Auth-Token": "token",
|
||||
"X-Auth-Project-Id": "project_id",
|
||||
"User-Agent": cl.USER_AGENT})
|
||||
# Automatic JSON parsing
|
||||
assert_equal(body, {"hi": "there"})
|
||||
|
||||
test_get_call()
|
||||
|
||||
|
||||
def test_post():
|
||||
cl = get_client()
|
||||
|
||||
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||
def test_post_call():
|
||||
cl.post("/hi", body=[1, 2, 3])
|
||||
mock_request.assert_called_with("http://example.com/hi", "POST",
|
||||
headers={
|
||||
"X-Auth-Token": "token",
|
||||
"X-Auth-Project-Id": "project_id",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": cl.USER_AGENT},
|
||||
body='[1, 2, 3]'
|
||||
)
|
||||
|
||||
test_post_call()
|
@ -1,42 +1,38 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from nose.tools import assert_raises, assert_equal
|
||||
|
||||
from novaclient import exceptions
|
||||
from novaclient.v1_0 import flavors
|
||||
from novaclient.v1_0 import exceptions
|
||||
|
||||
from .fakes import FakeClient
|
||||
from .utils import assert_isinstance
|
||||
|
||||
os = FakeClient()
|
||||
from tests.v1_0 import fakes
|
||||
from tests import utils
|
||||
|
||||
|
||||
def test_list_flavors():
|
||||
fl = os.flavors.list()
|
||||
os.assert_called('GET', '/flavors/detail')
|
||||
[assert_isinstance(f, flavors.Flavor) for f in fl]
|
||||
cs = fakes.FakeClient()
|
||||
|
||||
|
||||
def test_list_flavors_undetailed():
|
||||
fl = os.flavors.list(detailed=False)
|
||||
os.assert_called('GET', '/flavors')
|
||||
[assert_isinstance(f, flavors.Flavor) for f in fl]
|
||||
class FlavorsTest(utils.TestCase):
|
||||
|
||||
def test_list_flavors(self):
|
||||
fl = cs.flavors.list()
|
||||
cs.assert_called('GET', '/flavors/detail')
|
||||
[self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl]
|
||||
|
||||
def test_get_flavor_details():
|
||||
f = os.flavors.get(1)
|
||||
os.assert_called('GET', '/flavors/1')
|
||||
assert_isinstance(f, flavors.Flavor)
|
||||
assert_equal(f.ram, 256)
|
||||
assert_equal(f.disk, 10)
|
||||
def test_list_flavors_undetailed(self):
|
||||
fl = cs.flavors.list(detailed=False)
|
||||
cs.assert_called('GET', '/flavors')
|
||||
[self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl]
|
||||
|
||||
def test_get_flavor_details(self):
|
||||
f = cs.flavors.get(1)
|
||||
cs.assert_called('GET', '/flavors/1')
|
||||
self.assertTrue(isinstance(f, flavors.Flavor))
|
||||
self.assertEqual(f.ram, 256)
|
||||
self.assertEqual(f.disk, 10)
|
||||
|
||||
def test_find():
|
||||
f = os.flavors.find(ram=256)
|
||||
os.assert_called('GET', '/flavors/detail')
|
||||
assert_equal(f.name, '256 MB Server')
|
||||
def test_find(self):
|
||||
f = cs.flavors.find(ram=256)
|
||||
cs.assert_called('GET', '/flavors/detail')
|
||||
self.assertEqual(f.name, '256 MB Server')
|
||||
|
||||
f = os.flavors.find(disk=20)
|
||||
assert_equal(f.name, '512 MB Server')
|
||||
f = cs.flavors.find(disk=20)
|
||||
self.assertEqual(f.name, '512 MB Server')
|
||||
|
||||
assert_raises(exceptions.NotFound, os.flavors.find, disk=12345)
|
||||
self.assertRaises(exceptions.NotFound, cs.flavors.find, disk=12345)
|
||||
|
@ -1,51 +1,45 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from nose.tools import assert_equal
|
||||
|
||||
from novaclient.v1_0 import images
|
||||
|
||||
from .fakes import FakeClient
|
||||
from .utils import assert_isinstance
|
||||
|
||||
os = FakeClient()
|
||||
from tests.v1_0 import fakes
|
||||
from tests import utils
|
||||
|
||||
|
||||
def test_list_images():
|
||||
il = os.images.list()
|
||||
os.assert_called('GET', '/images/detail')
|
||||
[assert_isinstance(i, images.Image) for i in il]
|
||||
cs = fakes.FakeClient()
|
||||
|
||||
|
||||
def test_list_images_undetailed():
|
||||
il = os.images.list(detailed=False)
|
||||
os.assert_called('GET', '/images')
|
||||
[assert_isinstance(i, images.Image) for i in il]
|
||||
class ImagesTest(utils.TestCase):
|
||||
|
||||
def test_list_images(self):
|
||||
il = cs.images.list()
|
||||
cs.assert_called('GET', '/images/detail')
|
||||
[self.assertTrue(isinstance(i, images.Image)) for i in il]
|
||||
|
||||
def test_get_image_details():
|
||||
i = os.images.get(1)
|
||||
os.assert_called('GET', '/images/1')
|
||||
assert_isinstance(i, images.Image)
|
||||
assert_equal(i.id, 1)
|
||||
assert_equal(i.name, 'CentOS 5.2')
|
||||
def test_list_images_undetailed(self):
|
||||
il = cs.images.list(detailed=False)
|
||||
cs.assert_called('GET', '/images')
|
||||
[self.assertTrue(isinstance(i, images.Image)) for i in il]
|
||||
|
||||
def test_get_image_details(self):
|
||||
i = cs.images.get(1)
|
||||
cs.assert_called('GET', '/images/1')
|
||||
self.assertTrue(isinstance(i, images.Image))
|
||||
self.assertEqual(i.id, 1)
|
||||
self.assertEqual(i.name, 'CentOS 5.2')
|
||||
|
||||
def test_create_image():
|
||||
i = os.images.create(server=1234, name="Just in case")
|
||||
os.assert_called('POST', '/images')
|
||||
assert_isinstance(i, images.Image)
|
||||
def test_create_image(self):
|
||||
i = cs.images.create(server=1234, name="Just in case")
|
||||
cs.assert_called('POST', '/images')
|
||||
self.assertTrue(isinstance(i, images.Image))
|
||||
|
||||
def test_delete_image(self):
|
||||
cs.images.delete(1)
|
||||
cs.assert_called('DELETE', '/images/1')
|
||||
|
||||
def test_delete_image():
|
||||
os.images.delete(1)
|
||||
os.assert_called('DELETE', '/images/1')
|
||||
def test_find(self):
|
||||
i = cs.images.find(name="CentOS 5.2")
|
||||
self.assertEqual(i.id, 1)
|
||||
cs.assert_called('GET', '/images/detail')
|
||||
|
||||
|
||||
def test_find():
|
||||
i = os.images.find(name="CentOS 5.2")
|
||||
assert_equal(i.id, 1)
|
||||
os.assert_called('GET', '/images/detail')
|
||||
|
||||
iml = os.images.findall(status='SAVING')
|
||||
assert_equal(len(iml), 1)
|
||||
assert_equal(iml[0].name, 'My Server Backup')
|
||||
iml = cs.images.findall(status='SAVING')
|
||||
self.assertEqual(len(iml), 1)
|
||||
self.assertEqual(iml[0].name, 'My Server Backup')
|
||||
|
@ -1,52 +1,48 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from nose.tools import assert_equal
|
||||
|
||||
from novaclient.v1_0 import ipgroups
|
||||
|
||||
from .fakes import FakeClient
|
||||
from .utils import assert_isinstance
|
||||
|
||||
os = FakeClient()
|
||||
from tests.v1_0 import fakes
|
||||
from tests import utils
|
||||
|
||||
|
||||
def test_list_ipgroups():
|
||||
ipl = os.ipgroups.list()
|
||||
os.assert_called('GET', '/shared_ip_groups/detail')
|
||||
[assert_isinstance(ipg, ipgroups.IPGroup) for ipg in ipl]
|
||||
cs = fakes.FakeClient()
|
||||
|
||||
|
||||
def test_list_ipgroups_undetailed():
|
||||
ipl = os.ipgroups.list(detailed=False)
|
||||
os.assert_called('GET', '/shared_ip_groups')
|
||||
[assert_isinstance(ipg, ipgroups.IPGroup) for ipg in ipl]
|
||||
class IPGroupTest(utils.TestCase):
|
||||
|
||||
def test_list_ipgroups(self):
|
||||
ipl = cs.ipgroups.list()
|
||||
cs.assert_called('GET', '/shared_ip_groups/detail')
|
||||
[self.assertTrue(isinstance(ipg, ipgroups.IPGroup)) \
|
||||
for ipg in ipl]
|
||||
|
||||
def test_get_ipgroup():
|
||||
ipg = os.ipgroups.get(1)
|
||||
os.assert_called('GET', '/shared_ip_groups/1')
|
||||
assert_isinstance(ipg, ipgroups.IPGroup)
|
||||
def test_list_ipgroups_undetailed(self):
|
||||
ipl = cs.ipgroups.list(detailed=False)
|
||||
cs.assert_called('GET', '/shared_ip_groups')
|
||||
[self.assertTrue(isinstance(ipg, ipgroups.IPGroup)) \
|
||||
for ipg in ipl]
|
||||
|
||||
def test_get_ipgroup(self):
|
||||
ipg = cs.ipgroups.get(1)
|
||||
cs.assert_called('GET', '/shared_ip_groups/1')
|
||||
self.assertTrue(isinstance(ipg, ipgroups.IPGroup))
|
||||
|
||||
def test_create_ipgroup():
|
||||
ipg = os.ipgroups.create("My group", 1234)
|
||||
os.assert_called('POST', '/shared_ip_groups')
|
||||
assert_isinstance(ipg, ipgroups.IPGroup)
|
||||
def test_create_ipgroup(self):
|
||||
ipg = cs.ipgroups.create("My group", 1234)
|
||||
cs.assert_called('POST', '/shared_ip_groups')
|
||||
self.assertTrue(isinstance(ipg, ipgroups.IPGroup))
|
||||
|
||||
def test_delete_ipgroup(self):
|
||||
ipg = cs.ipgroups.get(1)
|
||||
ipg.delete()
|
||||
cs.assert_called('DELETE', '/shared_ip_groups/1')
|
||||
cs.ipgroups.delete(ipg)
|
||||
cs.assert_called('DELETE', '/shared_ip_groups/1')
|
||||
cs.ipgroups.delete(1)
|
||||
cs.assert_called('DELETE', '/shared_ip_groups/1')
|
||||
|
||||
def test_delete_ipgroup():
|
||||
ipg = os.ipgroups.get(1)
|
||||
ipg.delete()
|
||||
os.assert_called('DELETE', '/shared_ip_groups/1')
|
||||
os.ipgroups.delete(ipg)
|
||||
os.assert_called('DELETE', '/shared_ip_groups/1')
|
||||
os.ipgroups.delete(1)
|
||||
os.assert_called('DELETE', '/shared_ip_groups/1')
|
||||
|
||||
|
||||
def test_find():
|
||||
ipg = os.ipgroups.find(name='group1')
|
||||
os.assert_called('GET', '/shared_ip_groups/detail')
|
||||
assert_equal(ipg.name, 'group1')
|
||||
ipgl = os.ipgroups.findall(id=1)
|
||||
assert_equal(ipgl, [ipgroups.IPGroup(None, {'id': 1})])
|
||||
def test_find(self):
|
||||
ipg = cs.ipgroups.find(name='group1')
|
||||
cs.assert_called('GET', '/shared_ip_groups/detail')
|
||||
self.assertEqual(ipg.name, 'group1')
|
||||
ipgl = cs.ipgroups.findall(id=1)
|
||||
self.assertEqual(ipgl, [ipgroups.IPGroup(None, {'id': 1})])
|
||||
|
@ -1,180 +1,169 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import StringIO
|
||||
|
||||
from nose.tools import assert_equal
|
||||
|
||||
from novaclient.v1_0 import servers
|
||||
|
||||
from .fakes import FakeClient
|
||||
from .utils import assert_isinstance
|
||||
from tests.v1_0 import fakes
|
||||
from tests import utils
|
||||
|
||||
|
||||
os = FakeClient()
|
||||
cs = fakes.FakeClient()
|
||||
|
||||
|
||||
def test_list_servers():
|
||||
sl = os.servers.list()
|
||||
os.assert_called('GET', '/servers/detail')
|
||||
[assert_isinstance(s, servers.Server) for s in sl]
|
||||
class ServersTest(utils.TestCase):
|
||||
|
||||
def test_list_servers(self):
|
||||
sl = cs.servers.list()
|
||||
cs.assert_called('GET', '/servers/detail')
|
||||
[self.assertTrue(isinstance(s, servers.Server)) for s in sl]
|
||||
|
||||
def test_list_servers_undetailed():
|
||||
sl = os.servers.list(detailed=False)
|
||||
os.assert_called('GET', '/servers')
|
||||
[assert_isinstance(s, servers.Server) for s in sl]
|
||||
def test_list_servers_undetailed(self):
|
||||
sl = cs.servers.list(detailed=False)
|
||||
cs.assert_called('GET', '/servers')
|
||||
[self.assertTrue(isinstance(s, servers.Server)) for s in sl]
|
||||
|
||||
def test_get_server_details(self):
|
||||
s = cs.servers.get(1234)
|
||||
cs.assert_called('GET', '/servers/1234')
|
||||
self.assertTrue(isinstance(s, servers.Server))
|
||||
self.assertEqual(s.id, 1234)
|
||||
self.assertEqual(s.status, 'BUILD')
|
||||
|
||||
def test_get_server_details():
|
||||
s = os.servers.get(1234)
|
||||
os.assert_called('GET', '/servers/1234')
|
||||
assert_isinstance(s, servers.Server)
|
||||
assert_equal(s.id, 1234)
|
||||
assert_equal(s.status, 'BUILD')
|
||||
def test_create_server(self):
|
||||
s = cs.servers.create(
|
||||
name="My server",
|
||||
image=1,
|
||||
flavor=1,
|
||||
meta={'foo': 'bar'},
|
||||
ipgroup=1,
|
||||
files={
|
||||
'/etc/passwd': 'some data', # a file
|
||||
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
|
||||
}
|
||||
)
|
||||
cs.assert_called('POST', '/servers')
|
||||
self.assertTrue(isinstance(s, servers.Server))
|
||||
|
||||
def test_update_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
|
||||
def test_create_server():
|
||||
s = os.servers.create(
|
||||
name="My server",
|
||||
image=1,
|
||||
flavor=1,
|
||||
meta={'foo': 'bar'},
|
||||
ipgroup=1,
|
||||
files={
|
||||
'/etc/passwd': 'some data', # a file
|
||||
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
|
||||
}
|
||||
)
|
||||
os.assert_called('POST', '/servers')
|
||||
assert_isinstance(s, servers.Server)
|
||||
# Update via instance
|
||||
s.update(name='hi')
|
||||
cs.assert_called('PUT', '/servers/1234')
|
||||
s.update(name='hi', password='there')
|
||||
cs.assert_called('PUT', '/servers/1234')
|
||||
|
||||
# Silly, but not an error
|
||||
s.update()
|
||||
|
||||
def test_update_server():
|
||||
s = os.servers.get(1234)
|
||||
# Update via manager
|
||||
cs.servers.update(s, name='hi')
|
||||
cs.assert_called('PUT', '/servers/1234')
|
||||
cs.servers.update(1234, password='there')
|
||||
cs.assert_called('PUT', '/servers/1234')
|
||||
cs.servers.update(s, name='hi', password='there')
|
||||
cs.assert_called('PUT', '/servers/1234')
|
||||
|
||||
# Update via instance
|
||||
s.update(name='hi')
|
||||
os.assert_called('PUT', '/servers/1234')
|
||||
s.update(name='hi', password='there')
|
||||
os.assert_called('PUT', '/servers/1234')
|
||||
def test_delete_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.delete()
|
||||
cs.assert_called('DELETE', '/servers/1234')
|
||||
cs.servers.delete(1234)
|
||||
cs.assert_called('DELETE', '/servers/1234')
|
||||
cs.servers.delete(s)
|
||||
cs.assert_called('DELETE', '/servers/1234')
|
||||
|
||||
# Silly, but not an error
|
||||
s.update()
|
||||
def test_find(self):
|
||||
s = cs.servers.find(name='sample-server')
|
||||
cs.assert_called('GET', '/servers/detail')
|
||||
self.assertEqual(s.name, 'sample-server')
|
||||
|
||||
# Update via manager
|
||||
os.servers.update(s, name='hi')
|
||||
os.assert_called('PUT', '/servers/1234')
|
||||
os.servers.update(1234, password='there')
|
||||
os.assert_called('PUT', '/servers/1234')
|
||||
os.servers.update(s, name='hi', password='there')
|
||||
os.assert_called('PUT', '/servers/1234')
|
||||
# Find with multiple results arbitraility returns the first item
|
||||
s = cs.servers.find(flavorId=1)
|
||||
sl = cs.servers.findall(flavorId=1)
|
||||
self.assertEqual(sl[0], s)
|
||||
self.assertEqual([s.id for s in sl], [1234, 5678])
|
||||
|
||||
def test_share_ip(self):
|
||||
s = cs.servers.get(1234)
|
||||
|
||||
def test_delete_server():
|
||||
s = os.servers.get(1234)
|
||||
s.delete()
|
||||
os.assert_called('DELETE', '/servers/1234')
|
||||
os.servers.delete(1234)
|
||||
os.assert_called('DELETE', '/servers/1234')
|
||||
os.servers.delete(s)
|
||||
os.assert_called('DELETE', '/servers/1234')
|
||||
# Share via instance
|
||||
s.share_ip(ipgroup=1, address='1.2.3.4')
|
||||
cs.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4')
|
||||
|
||||
# Share via manager
|
||||
cs.servers.share_ip(s, ipgroup=1, address='1.2.3.4', configure=False)
|
||||
cs.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4')
|
||||
|
||||
def test_find():
|
||||
s = os.servers.find(name='sample-server')
|
||||
os.assert_called('GET', '/servers/detail')
|
||||
assert_equal(s.name, 'sample-server')
|
||||
def test_unshare_ip(self):
|
||||
s = cs.servers.get(1234)
|
||||
|
||||
# Find with multiple results arbitraility returns the first item
|
||||
s = os.servers.find(flavorId=1)
|
||||
sl = os.servers.findall(flavorId=1)
|
||||
assert_equal(sl[0], s)
|
||||
assert_equal([s.id for s in sl], [1234, 5678])
|
||||
# Unshare via instance
|
||||
s.unshare_ip('1.2.3.4')
|
||||
cs.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
|
||||
|
||||
# Unshare via manager
|
||||
cs.servers.unshare_ip(s, '1.2.3.4')
|
||||
cs.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
|
||||
|
||||
def test_share_ip():
|
||||
s = os.servers.get(1234)
|
||||
def test_reboot_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.reboot()
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.reboot(s, type='HARD')
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
# Share via instance
|
||||
s.share_ip(ipgroup=1, address='1.2.3.4')
|
||||
os.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4')
|
||||
def test_rebuild_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.rebuild(image=1)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.rebuild(s, image=1)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
# Share via manager
|
||||
os.servers.share_ip(s, ipgroup=1, address='1.2.3.4', configure=False)
|
||||
os.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4')
|
||||
def test_resize_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.resize(flavor=1)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.resize(s, flavor=1)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
def test_confirm_resized_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.confirm_resize()
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.confirm_resize(s)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
def test_unshare_ip():
|
||||
s = os.servers.get(1234)
|
||||
def test_revert_resized_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.revert_resize()
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.revert_resize(s)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
# Unshare via instance
|
||||
s.unshare_ip('1.2.3.4')
|
||||
os.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
|
||||
def test_backup_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.backup("ImageName", "daily", 10)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.backup(s, "ImageName", "daily", 10)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
# Unshare via manager
|
||||
os.servers.unshare_ip(s, '1.2.3.4')
|
||||
os.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
|
||||
def test_migrate_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.migrate()
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.migrate(s)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
def test_add_fixed_ip(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.add_fixed_ip(1)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.add_fixed_ip(s, 1)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
def test_reboot_server():
|
||||
s = os.servers.get(1234)
|
||||
s.reboot()
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.reboot(s, type='HARD')
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
|
||||
def test_rebuild_server():
|
||||
s = os.servers.get(1234)
|
||||
s.rebuild(image=1)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.rebuild(s, image=1)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
|
||||
def test_resize_server():
|
||||
s = os.servers.get(1234)
|
||||
s.resize(flavor=1)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.resize(s, flavor=1)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
|
||||
def test_confirm_resized_server():
|
||||
s = os.servers.get(1234)
|
||||
s.confirm_resize()
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.confirm_resize(s)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
|
||||
def test_revert_resized_server():
|
||||
s = os.servers.get(1234)
|
||||
s.revert_resize()
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.revert_resize(s)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
|
||||
def test_migrate_server():
|
||||
s = os.servers.get(1234)
|
||||
s.migrate()
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.migrate(s)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
|
||||
def test_add_fixed_ip():
|
||||
s = os.servers.get(1234)
|
||||
s.add_fixed_ip(1)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.add_fixed_ip(s, 1)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
|
||||
def test_remove_fixed_ip():
|
||||
s = os.servers.get(1234)
|
||||
s.remove_fixed_ip('10.0.0.1')
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.remove_fixed_ip(s, '10.0.0.1')
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
def test_remove_fixed_ip(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.remove_fixed_ip('10.0.0.1')
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.remove_fixed_ip(s, '10.0.0.1')
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
@ -1,374 +1,319 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import mock
|
||||
import httplib2
|
||||
|
||||
from nose.tools import assert_raises, assert_equal
|
||||
|
||||
from novaclient.v1_0.shell import OpenStackShell, CommandError
|
||||
|
||||
from .fakes import FakeClient
|
||||
from .utils import assert_in
|
||||
from novaclient.shell import OpenStackComputeShell
|
||||
from novaclient import exceptions
|
||||
from tests.v1_0 import fakes
|
||||
from tests import utils
|
||||
|
||||
|
||||
# Patch os.environ to avoid required auth info.
|
||||
def setup():
|
||||
global _old_env
|
||||
fake_env = {
|
||||
'NOVA_USERNAME': 'username',
|
||||
'NOVA_API_KEY': 'password',
|
||||
'NOVA_PROJECT_ID': 'project_id'
|
||||
}
|
||||
_old_env, os.environ = os.environ, fake_env.copy()
|
||||
class ShellTest(utils.TestCase):
|
||||
|
||||
# Make a fake shell object, a helping wrapper to call it, and a quick way
|
||||
# of asserting that certain API calls were made.
|
||||
global shell, _shell, assert_called, assert_called_anytime
|
||||
_shell = OpenStackShell()
|
||||
_shell._api_class = FakeClient
|
||||
assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b)
|
||||
assert_called_anytime = lambda m, u, b=None: \
|
||||
_shell.cs.assert_called_anytime(m, u, b)
|
||||
shell = lambda cmd: _shell.main(cmd.split())
|
||||
|
||||
|
||||
def teardown():
|
||||
global _old_env
|
||||
os.environ = _old_env
|
||||
|
||||
|
||||
def test_backup_schedule():
|
||||
shell('backup-schedule 1234')
|
||||
assert_called('GET', '/servers/1234/backup_schedule')
|
||||
|
||||
shell('backup-schedule sample-server --weekly monday')
|
||||
assert_called(
|
||||
'POST', '/servers/1234/backup_schedule',
|
||||
{'backupSchedule': {'enabled': True, 'daily': 'DISABLED',
|
||||
'weekly': 'MONDAY'}}
|
||||
)
|
||||
|
||||
shell('backup-schedule sample-server '
|
||||
'--weekly disabled --daily h_0000_0200')
|
||||
assert_called(
|
||||
'POST', '/servers/1234/backup_schedule',
|
||||
{'backupSchedule': {'enabled': True, 'daily': 'H_0000_0200',
|
||||
'weekly': 'DISABLED'}}
|
||||
)
|
||||
|
||||
shell('backup-schedule sample-server --disable')
|
||||
assert_called(
|
||||
'POST', '/servers/1234/backup_schedule',
|
||||
{'backupSchedule': {'enabled': False, 'daily': 'DISABLED',
|
||||
'weekly': 'DISABLED'}}
|
||||
)
|
||||
|
||||
|
||||
def test_backup_schedule_delete():
|
||||
shell('backup-schedule-delete 1234')
|
||||
assert_called('DELETE', '/servers/1234/backup_schedule')
|
||||
|
||||
|
||||
def test_boot():
|
||||
shell('boot --image 1 some-server')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'min_count': 1, 'max_count': 1}}
|
||||
)
|
||||
|
||||
shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'min_count': 1, 'max_count': 1,
|
||||
'metadata': {'foo': 'bar', 'spam': 'eggs'}}}
|
||||
)
|
||||
|
||||
|
||||
def test_boot_files():
|
||||
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
|
||||
expected_file_data = open(testfile).read().encode('base64')
|
||||
|
||||
shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' %
|
||||
(testfile, testfile))
|
||||
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'min_count': 1, 'max_count': 1,
|
||||
'personality': [
|
||||
{'path': '/tmp/bar', 'contents': expected_file_data},
|
||||
{'path': '/tmp/foo', 'contents': expected_file_data}
|
||||
]}
|
||||
# Patch os.environ to avoid required auth info.
|
||||
def setUp(self):
|
||||
global _old_env
|
||||
fake_env = {
|
||||
'NOVA_USERNAME': 'username',
|
||||
'NOVA_API_KEY': 'password',
|
||||
'NOVA_PROJECT_ID': 'project_id'
|
||||
}
|
||||
)
|
||||
_old_env, os.environ = os.environ, fake_env.copy()
|
||||
|
||||
# Make a fake shell object, a helping wrapper to call it, and a quick way
|
||||
# of asserting that certain API calls were made.
|
||||
global shell, _shell, assert_called, assert_called_anytime
|
||||
_shell = OpenStackComputeShell()
|
||||
_shell._api_class = fakes.FakeClient
|
||||
assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b)
|
||||
assert_called_anytime = lambda m, u, b=None: \
|
||||
_shell.cs.assert_called_anytime(m, u, b)
|
||||
|
||||
def test_boot_invalid_file():
|
||||
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
|
||||
assert_raises(CommandError, shell, 'boot some-server --image 1 '
|
||||
'--file /foo=%s' % invalid_file)
|
||||
def shell(cmd):
|
||||
command = ['--version=1.0',]
|
||||
command.extend(cmd.split())
|
||||
_shell.main(command)
|
||||
|
||||
def tearDown(self):
|
||||
global _old_env
|
||||
os.environ = _old_env
|
||||
|
||||
def test_boot_key_auto():
|
||||
mock_exists = mock.Mock(return_value=True)
|
||||
mock_open = mock.Mock()
|
||||
mock_open.return_value = mock.Mock()
|
||||
mock_open.return_value.read = mock.Mock(return_value='SSHKEY')
|
||||
def test_backup_schedule(self):
|
||||
shell('backup-schedule 1234')
|
||||
assert_called('GET', '/servers/1234/backup_schedule')
|
||||
|
||||
@mock.patch('os.path.exists', mock_exists)
|
||||
@mock.patch('__builtin__.open', mock_open)
|
||||
def test_shell_call():
|
||||
shell('boot some-server --image 1 --key')
|
||||
shell('backup-schedule sample-server --weekly monday')
|
||||
assert_called(
|
||||
'POST', '/servers/1234/backup_schedule',
|
||||
{'backupSchedule': {'enabled': True, 'daily': 'DISABLED',
|
||||
'weekly': 'MONDAY'}}
|
||||
)
|
||||
|
||||
shell('backup-schedule sample-server '
|
||||
'--weekly disabled --daily h_0000_0200')
|
||||
assert_called(
|
||||
'POST', '/servers/1234/backup_schedule',
|
||||
{'backupSchedule': {'enabled': True, 'daily': 'H_0000_0200',
|
||||
'weekly': 'DISABLED'}}
|
||||
)
|
||||
|
||||
shell('backup-schedule sample-server --disable')
|
||||
assert_called(
|
||||
'POST', '/servers/1234/backup_schedule',
|
||||
{'backupSchedule': {'enabled': False, 'daily': 'DISABLED',
|
||||
'weekly': 'DISABLED'}}
|
||||
)
|
||||
|
||||
def test_backup_schedule_delete(self):
|
||||
shell('backup-schedule-delete 1234')
|
||||
assert_called('DELETE', '/servers/1234/backup_schedule')
|
||||
|
||||
def test_boot(self):
|
||||
shell('boot --image 1 some-server')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'min_count': 1, 'max_count': 1}}
|
||||
)
|
||||
|
||||
shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'min_count': 1, 'max_count': 1,
|
||||
'personality': [{
|
||||
'path': '/root/.ssh/authorized_keys2',
|
||||
'contents': ('SSHKEY').encode('base64')},
|
||||
'metadata': {'foo': 'bar', 'spam': 'eggs'}}}
|
||||
)
|
||||
|
||||
def test_boot_files(self):
|
||||
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
|
||||
expected_file_data = open(testfile).read().encode('base64')
|
||||
|
||||
shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' %
|
||||
(testfile, testfile))
|
||||
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'min_count': 1, 'max_count': 1,
|
||||
'personality': [
|
||||
{'path': '/tmp/bar', 'contents': expected_file_data},
|
||||
{'path': '/tmp/foo', 'contents': expected_file_data}
|
||||
]}
|
||||
}
|
||||
)
|
||||
|
||||
test_shell_call()
|
||||
|
||||
|
||||
def test_boot_key_auto_no_keys():
|
||||
mock_exists = mock.Mock(return_value=False)
|
||||
|
||||
@mock.patch('os.path.exists', mock_exists)
|
||||
def test_shell_call():
|
||||
assert_raises(CommandError, shell, 'boot some-server --image 1 --key')
|
||||
|
||||
test_shell_call()
|
||||
|
||||
|
||||
def test_boot_key_file():
|
||||
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
|
||||
expected_file_data = open(testfile).read().encode('base64')
|
||||
shell('boot some-server --image 1 --key %s' % testfile)
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'min_count': 1, 'max_count': 1,
|
||||
'personality': [
|
||||
{'path': '/root/.ssh/authorized_keys2', 'contents':
|
||||
expected_file_data},
|
||||
]}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_boot_invalid_keyfile():
|
||||
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
|
||||
assert_raises(CommandError, shell, 'boot some-server '
|
||||
'--image 1 --key %s' % invalid_file)
|
||||
|
||||
|
||||
def test_boot_ipgroup():
|
||||
shell('boot --image 1 --ipgroup 1 some-server')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}}
|
||||
)
|
||||
|
||||
|
||||
def test_boot_ipgroup_name():
|
||||
shell('boot --image 1 --ipgroup group1 some-server')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}}
|
||||
)
|
||||
|
||||
|
||||
def test_flavor_list():
|
||||
shell('flavor-list')
|
||||
assert_called_anytime('GET', '/flavors/detail')
|
||||
|
||||
|
||||
def test_image_list():
|
||||
shell('image-list')
|
||||
assert_called('GET', '/images/detail')
|
||||
|
||||
|
||||
def test_snapshot_create():
|
||||
shell('image-create sample-server mysnapshot')
|
||||
assert_called(
|
||||
'POST', '/images',
|
||||
{'image': {'name': 'mysnapshot', 'serverId': 1234, 'image_type': 'snapshot', 'backup_type': None, 'rotation': None}}
|
||||
)
|
||||
|
||||
|
||||
def test_backup_create():
|
||||
shell('image-create sample-server mybackup --image-type backup --backup-type daily --rotation 1')
|
||||
assert_called(
|
||||
'POST', '/images',
|
||||
{'image': {'name': 'mybackup', 'serverId': 1234, 'image_type': 'backup', 'backup_type': 'daily', 'rotation': 1}}
|
||||
)
|
||||
|
||||
|
||||
def test_image_delete():
|
||||
shell('image-delete 1')
|
||||
assert_called('DELETE', '/images/1')
|
||||
|
||||
|
||||
def test_ip_share():
|
||||
shell('ip-share sample-server 1 1.2.3.4')
|
||||
assert_called(
|
||||
'PUT', '/servers/1234/ips/public/1.2.3.4',
|
||||
{'shareIp': {'sharedIpGroupId': 1, 'configureServer': True}}
|
||||
)
|
||||
|
||||
|
||||
def test_ip_unshare():
|
||||
shell('ip-unshare sample-server 1.2.3.4')
|
||||
assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
|
||||
|
||||
|
||||
def test_ipgroup_list():
|
||||
shell('ipgroup-list')
|
||||
assert_in(('GET', '/shared_ip_groups/detail', None),
|
||||
_shell.cs.client.callstack)
|
||||
assert_called('GET', '/servers/5678')
|
||||
|
||||
|
||||
def test_ipgroup_show():
|
||||
shell('ipgroup-show 1')
|
||||
assert_called('GET', '/shared_ip_groups/1')
|
||||
shell('ipgroup-show group2')
|
||||
# does a search, not a direct GET
|
||||
assert_called('GET', '/shared_ip_groups/detail')
|
||||
|
||||
|
||||
def test_ipgroup_create():
|
||||
shell('ipgroup-create a-group')
|
||||
assert_called(
|
||||
'POST', '/shared_ip_groups',
|
||||
{'sharedIpGroup': {'name': 'a-group'}}
|
||||
)
|
||||
shell('ipgroup-create a-group sample-server')
|
||||
assert_called(
|
||||
'POST', '/shared_ip_groups',
|
||||
{'sharedIpGroup': {'name': 'a-group', 'server': 1234}}
|
||||
)
|
||||
|
||||
|
||||
def test_ipgroup_delete():
|
||||
shell('ipgroup-delete group1')
|
||||
assert_called('DELETE', '/shared_ip_groups/1')
|
||||
|
||||
|
||||
def test_list():
|
||||
shell('list')
|
||||
assert_called('GET', '/servers/detail')
|
||||
|
||||
|
||||
def test_reboot():
|
||||
shell('reboot sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
|
||||
shell('reboot sample-server --hard')
|
||||
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
|
||||
|
||||
|
||||
def test_rebuild():
|
||||
shell('rebuild sample-server 1')
|
||||
assert_called('POST', '/servers/1234/action', {'rebuild': {'imageId': 1}})
|
||||
|
||||
|
||||
def test_rename():
|
||||
shell('rename sample-server newname')
|
||||
assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
|
||||
|
||||
|
||||
def test_resize():
|
||||
shell('resize sample-server 1')
|
||||
assert_called('POST', '/servers/1234/action', {'resize': {'flavorId': 1}})
|
||||
|
||||
|
||||
def test_resize_confirm():
|
||||
shell('resize-confirm sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'confirmResize': None})
|
||||
|
||||
|
||||
def test_resize_revert():
|
||||
shell('resize-revert sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'revertResize': None})
|
||||
|
||||
|
||||
@mock.patch('getpass.getpass', mock.Mock(return_value='p'))
|
||||
def test_root_password():
|
||||
shell('root-password sample-server')
|
||||
assert_called('PUT', '/servers/1234', {'server': {'adminPass': 'p'}})
|
||||
|
||||
|
||||
def test_show():
|
||||
shell('show 1234')
|
||||
# XXX need a way to test multiple calls
|
||||
# assert_called('GET', '/servers/1234')
|
||||
assert_called('GET', '/images/2')
|
||||
|
||||
|
||||
def test_delete():
|
||||
shell('delete 1234')
|
||||
assert_called('DELETE', '/servers/1234')
|
||||
shell('delete sample-server')
|
||||
assert_called('DELETE', '/servers/1234')
|
||||
|
||||
|
||||
def test_zone():
|
||||
shell('zone 1')
|
||||
assert_called('GET', '/zones/1')
|
||||
|
||||
shell('zone 1 --api_url=http://zzz --zone_username=frank --password=xxx')
|
||||
assert_called(
|
||||
'PUT', '/zones/1',
|
||||
{'zone': {'api_url': 'http://zzz', 'username': 'frank',
|
||||
'password': 'xxx'}}
|
||||
)
|
||||
|
||||
def test_zone_add():
|
||||
shell('zone-add http://zzz frank xxx 0.0 1.0')
|
||||
assert_called(
|
||||
'POST', '/zones',
|
||||
{'zone': {'api_url': 'http://zzz', 'username': 'frank',
|
||||
'password': 'xxx',
|
||||
'weight_offset': '0.0', 'weight_scale': '1.0'}}
|
||||
)
|
||||
|
||||
def test_zone_delete():
|
||||
shell('zone-delete 1')
|
||||
assert_called('DELETE', '/zones/1')
|
||||
|
||||
|
||||
def test_zone_list():
|
||||
shell('zone-list')
|
||||
assert_in(('GET', '/zones/detail', None),
|
||||
_shell.cs.client.callstack)
|
||||
|
||||
|
||||
def test_help():
|
||||
@mock.patch.object(_shell.parser, 'print_help')
|
||||
def test_help(m):
|
||||
shell('help')
|
||||
m.assert_called()
|
||||
|
||||
@mock.patch.object(_shell.subcommands['delete'], 'print_help')
|
||||
def test_help_delete(m):
|
||||
shell('help delete')
|
||||
m.assert_called()
|
||||
|
||||
test_help()
|
||||
test_help_delete()
|
||||
|
||||
assert_raises(CommandError, shell, 'help foofoo')
|
||||
|
||||
|
||||
def test_debug():
|
||||
httplib2.debuglevel = 0
|
||||
shell('--debug list')
|
||||
assert httplib2.debuglevel == 1
|
||||
def test_boot_invalid_file(self):
|
||||
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
|
||||
self.assertRaises(exceptions.CommandError, shell, 'boot some-server --image 1 '
|
||||
'--file /foo=%s' % invalid_file)
|
||||
|
||||
def test_boot_key_auto(self):
|
||||
mock_exists = mock.Mock(return_value=True)
|
||||
mock_open = mock.Mock()
|
||||
mock_open.return_value = mock.Mock()
|
||||
mock_open.return_value.read = mock.Mock(return_value='SSHKEY')
|
||||
|
||||
@mock.patch('os.path.exists', mock_exists)
|
||||
@mock.patch('__builtin__.open', mock_open)
|
||||
def test_shell_call():
|
||||
shell('boot some-server --image 1 --key')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'min_count': 1, 'max_count': 1,
|
||||
'personality': [{
|
||||
'path': '/root/.ssh/authorized_keys2',
|
||||
'contents': ('SSHKEY').encode('base64')},
|
||||
]}
|
||||
}
|
||||
)
|
||||
|
||||
test_shell_call()
|
||||
|
||||
def test_boot_key_auto_no_keys(self):
|
||||
mock_exists = mock.Mock(return_value=False)
|
||||
|
||||
@mock.patch('os.path.exists', mock_exists)
|
||||
def test_shell_call():
|
||||
self.assertRaises(exceptions.CommandError, shell,
|
||||
'boot some-server --image 1 --key')
|
||||
|
||||
test_shell_call()
|
||||
|
||||
def test_boot_key_file(self):
|
||||
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
|
||||
expected_file_data = open(testfile).read().encode('base64')
|
||||
shell('boot some-server --image 1 --key %s' % testfile)
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'min_count': 1, 'max_count': 1,
|
||||
'personality': [
|
||||
{'path': '/root/.ssh/authorized_keys2', 'contents':
|
||||
expected_file_data},
|
||||
]}
|
||||
}
|
||||
)
|
||||
|
||||
def test_boot_invalid_keyfile(self):
|
||||
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
|
||||
self.assertRaises(exceptions.CommandError, shell, 'boot some-server '
|
||||
'--image 1 --key %s' % invalid_file)
|
||||
|
||||
def test_boot_ipgroup(self):
|
||||
shell('boot --image 1 --ipgroup 1 some-server')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}}
|
||||
)
|
||||
|
||||
def test_boot_ipgroup_name(self):
|
||||
shell('boot --image 1 --ipgroup group1 some-server')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
|
||||
'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}}
|
||||
)
|
||||
|
||||
def test_flavor_list(self):
|
||||
shell('flavor-list')
|
||||
assert_called_anytime('GET', '/flavors/detail')
|
||||
|
||||
def test_image_list(self):
|
||||
shell('image-list')
|
||||
assert_called('GET', '/images/detail')
|
||||
|
||||
def test_snapshot_create(self):
|
||||
shell('image-create sample-server mysnapshot')
|
||||
assert_called(
|
||||
'POST', '/images',
|
||||
{'image': {'name': 'mysnapshot', 'serverId': 1234}}
|
||||
)
|
||||
|
||||
def test_image_delete(self):
|
||||
shell('image-delete 1')
|
||||
assert_called('DELETE', '/images/1')
|
||||
|
||||
def test_ip_share(self):
|
||||
shell('ip-share sample-server 1 1.2.3.4')
|
||||
assert_called(
|
||||
'PUT', '/servers/1234/ips/public/1.2.3.4',
|
||||
{'shareIp': {'sharedIpGroupId': 1, 'configureServer': True}}
|
||||
)
|
||||
|
||||
def test_ip_unshare(self):
|
||||
shell('ip-unshare sample-server 1.2.3.4')
|
||||
assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
|
||||
|
||||
def test_ipgroup_list(self):
|
||||
shell('ipgroup-list')
|
||||
assert ('GET', '/shared_ip_groups/detail', None) in \
|
||||
_shell.cs.client.callstack
|
||||
assert_called('GET', '/servers/5678')
|
||||
|
||||
def test_ipgroup_show(self):
|
||||
shell('ipgroup-show 1')
|
||||
assert_called('GET', '/shared_ip_groups/1')
|
||||
shell('ipgroup-show group2')
|
||||
# does a search, not a direct GET
|
||||
assert_called('GET', '/shared_ip_groups/detail')
|
||||
|
||||
def test_ipgroup_create(self):
|
||||
shell('ipgroup-create a-group')
|
||||
assert_called(
|
||||
'POST', '/shared_ip_groups',
|
||||
{'sharedIpGroup': {'name': 'a-group'}}
|
||||
)
|
||||
shell('ipgroup-create a-group sample-server')
|
||||
assert_called(
|
||||
'POST', '/shared_ip_groups',
|
||||
{'sharedIpGroup': {'name': 'a-group', 'server': 1234}}
|
||||
)
|
||||
|
||||
def test_ipgroup_delete(self):
|
||||
shell('ipgroup-delete group1')
|
||||
assert_called('DELETE', '/shared_ip_groups/1')
|
||||
|
||||
def test_list(self):
|
||||
shell('list')
|
||||
assert_called('GET', '/servers/detail')
|
||||
|
||||
def test_reboot(self):
|
||||
shell('reboot sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
|
||||
shell('reboot sample-server --hard')
|
||||
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
|
||||
|
||||
def test_rebuild(self):
|
||||
shell('rebuild sample-server 1')
|
||||
assert_called('POST', '/servers/1234/action', {'rebuild': {'imageId': 1}})
|
||||
|
||||
def test_rename(self):
|
||||
shell('rename sample-server newname')
|
||||
assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
|
||||
|
||||
def test_resize(self):
|
||||
shell('resize sample-server 1')
|
||||
assert_called('POST', '/servers/1234/action', {'resize': {'flavorId': 1}})
|
||||
|
||||
def test_resize_confirm(self):
|
||||
shell('resize-confirm sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'confirmResize': None})
|
||||
|
||||
def test_resize_revert(self):
|
||||
shell('resize-revert sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'revertResize': None})
|
||||
|
||||
def test_backup(self):
|
||||
shell('backup sample-server mybackup daily 1')
|
||||
assert_called(
|
||||
'POST', '/servers/1234/action',
|
||||
{'createBackup': {'name': 'mybackup', 'backup_type': 'daily',
|
||||
'rotation': 1}}
|
||||
)
|
||||
|
||||
@mock.patch('getpass.getpass', mock.Mock(return_value='p'))
|
||||
def test_root_password(self):
|
||||
shell('root-password sample-server')
|
||||
assert_called('PUT', '/servers/1234', {'server': {'adminPass': 'p'}})
|
||||
|
||||
def test_show(self):
|
||||
shell('show 1234')
|
||||
# XXX need a way to test multiple calls
|
||||
# assert_called('GET', '/servers/1234')
|
||||
assert_called('GET', '/images/2')
|
||||
|
||||
def test_delete(self):
|
||||
shell('delete 1234')
|
||||
assert_called('DELETE', '/servers/1234')
|
||||
shell('delete sample-server')
|
||||
assert_called('DELETE', '/servers/1234')
|
||||
|
||||
def test_zone(self):
|
||||
shell('zone 1')
|
||||
assert_called('GET', '/zones/1')
|
||||
|
||||
shell('zone 1 --api_url=http://zzz --zone_username=frank --password=xxx')
|
||||
assert_called(
|
||||
'PUT', '/zones/1',
|
||||
{'zone': {'api_url': 'http://zzz', 'username': 'frank',
|
||||
'password': 'xxx'}}
|
||||
)
|
||||
|
||||
def test_zone_add(self):
|
||||
shell('zone-add http://zzz frank xxx 0.0 1.0')
|
||||
assert_called(
|
||||
'POST', '/zones',
|
||||
{'zone': {'api_url': 'http://zzz', 'username': 'frank',
|
||||
'password': 'xxx',
|
||||
'weight_offset': '0.0', 'weight_scale': '1.0'}}
|
||||
)
|
||||
|
||||
def test_zone_delete(self):
|
||||
shell('zone-delete 1')
|
||||
assert_called('DELETE', '/zones/1')
|
||||
|
||||
def test_zone_list(self):
|
||||
shell('zone-list')
|
||||
assert ('GET', '/zones/detail', None) in _shell.cs.client.callstack
|
||||
|
@ -1,83 +1,76 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import StringIO
|
||||
|
||||
from nose.tools import assert_equal
|
||||
|
||||
from novaclient.v1_0 import zones
|
||||
|
||||
from .fakes import FakeClient
|
||||
from .utils import assert_isinstance
|
||||
|
||||
os = FakeClient()
|
||||
from tests.v1_0 import fakes
|
||||
from tests import utils
|
||||
|
||||
|
||||
def test_list_zones():
|
||||
sl = os.zones.list()
|
||||
os.assert_called('GET', '/zones/detail')
|
||||
[assert_isinstance(s, zones.Zone) for s in sl]
|
||||
os = fakes.FakeClient()
|
||||
|
||||
|
||||
def test_list_zones_undetailed():
|
||||
sl = os.zones.list(detailed=False)
|
||||
os.assert_called('GET', '/zones')
|
||||
[assert_isinstance(s, zones.Zone) for s in sl]
|
||||
class ZonesTest(utils.TestCase):
|
||||
|
||||
def test_list_zones(self):
|
||||
sl = os.zones.list()
|
||||
os.assert_called('GET', '/zones/detail')
|
||||
[self.assertTrue(isinstance(s, zones.Zone)) for s in sl]
|
||||
|
||||
def test_get_zone_details():
|
||||
s = os.zones.get(1)
|
||||
os.assert_called('GET', '/zones/1')
|
||||
assert_isinstance(s, zones.Zone)
|
||||
assert_equal(s.id, 1)
|
||||
assert_equal(s.api_url, 'http://foo.com')
|
||||
def test_list_zones_undetailed(self):
|
||||
sl = os.zones.list(detailed=False)
|
||||
os.assert_called('GET', '/zones')
|
||||
[self.assertTrue(isinstance(s, zones.Zone)) for s in sl]
|
||||
|
||||
def test_get_zone_details(self):
|
||||
s = os.zones.get(1)
|
||||
os.assert_called('GET', '/zones/1')
|
||||
self.assertTrue(isinstance(s, zones.Zone))
|
||||
self.assertEqual(s.id, 1)
|
||||
self.assertEqual(s.api_url, 'http://foo.com')
|
||||
|
||||
def test_create_zone():
|
||||
s = os.zones.create(api_url="http://foo.com", username='bob',
|
||||
password='xxx')
|
||||
os.assert_called('POST', '/zones')
|
||||
assert_isinstance(s, zones.Zone)
|
||||
def test_create_zone(self):
|
||||
s = os.zones.create(api_url="http://foo.com", username='bob',
|
||||
password='xxx')
|
||||
os.assert_called('POST', '/zones')
|
||||
self.assertTrue(isinstance(s, zones.Zone))
|
||||
|
||||
def test_update_zone(self):
|
||||
s = os.zones.get(1)
|
||||
|
||||
def test_update_zone():
|
||||
s = os.zones.get(1)
|
||||
# Update via instance
|
||||
s.update(api_url='http://blah.com')
|
||||
os.assert_called('PUT', '/zones/1')
|
||||
s.update(api_url='http://blah.com', username='alice', password='xxx')
|
||||
os.assert_called('PUT', '/zones/1')
|
||||
|
||||
# Update via instance
|
||||
s.update(api_url='http://blah.com')
|
||||
os.assert_called('PUT', '/zones/1')
|
||||
s.update(api_url='http://blah.com', username='alice', password='xxx')
|
||||
os.assert_called('PUT', '/zones/1')
|
||||
# Silly, but not an error
|
||||
s.update()
|
||||
|
||||
# Silly, but not an error
|
||||
s.update()
|
||||
# Update via manager
|
||||
os.zones.update(s, api_url='http://blah.com')
|
||||
os.assert_called('PUT', '/zones/1')
|
||||
os.zones.update(1, api_url='http://blah.com')
|
||||
os.assert_called('PUT', '/zones/1')
|
||||
os.zones.update(s, api_url='http://blah.com', username='fred',
|
||||
password='zip')
|
||||
os.assert_called('PUT', '/zones/1')
|
||||
|
||||
# Update via manager
|
||||
os.zones.update(s, api_url='http://blah.com')
|
||||
os.assert_called('PUT', '/zones/1')
|
||||
os.zones.update(1, api_url= 'http://blah.com')
|
||||
os.assert_called('PUT', '/zones/1')
|
||||
os.zones.update(s, api_url='http://blah.com', username='fred',
|
||||
password='zip')
|
||||
os.assert_called('PUT', '/zones/1')
|
||||
def test_delete_zone(self):
|
||||
s = os.zones.get(1)
|
||||
s.delete()
|
||||
os.assert_called('DELETE', '/zones/1')
|
||||
os.zones.delete(1)
|
||||
os.assert_called('DELETE', '/zones/1')
|
||||
os.zones.delete(s)
|
||||
os.assert_called('DELETE', '/zones/1')
|
||||
|
||||
def test_find_zone(self):
|
||||
s = os.zones.find(password='qwerty')
|
||||
os.assert_called('GET', '/zones/detail')
|
||||
self.assertEqual(s.username, 'bob')
|
||||
|
||||
def test_delete_zone():
|
||||
s = os.zones.get(1)
|
||||
s.delete()
|
||||
os.assert_called('DELETE', '/zones/1')
|
||||
os.zones.delete(1)
|
||||
os.assert_called('DELETE', '/zones/1')
|
||||
os.zones.delete(s)
|
||||
os.assert_called('DELETE', '/zones/1')
|
||||
|
||||
|
||||
def test_find_zone():
|
||||
s = os.zones.find(password='qwerty')
|
||||
os.assert_called('GET', '/zones/detail')
|
||||
assert_equal(s.username, 'bob')
|
||||
|
||||
# Find with multiple results returns the first item
|
||||
s = os.zones.find(api_url='http://foo.com')
|
||||
sl = os.zones.findall(api_url='http://foo.com')
|
||||
assert_equal(sl[0], s)
|
||||
assert_equal([s.id for s in sl], [1, 2])
|
||||
# Find with multiple results returns the first item
|
||||
s = os.zones.find(api_url='http://foo.com')
|
||||
sl = os.zones.findall(api_url='http://foo.com')
|
||||
self.assertEqual(sl[0], s)
|
||||
self.assertEqual([s.id for s in sl], [1, 2])
|
||||
|
@ -1 +1 @@
|
||||
OH HAI!
|
||||
BLAH
|
||||
|
@ -1,86 +1,23 @@
|
||||
"""
|
||||
A fake server that "responds" to API methods with pre-canned responses.
|
||||
|
||||
All of these responses come from the spec, so if for some reason the spec's
|
||||
wrong the tests might fail. I've indicated in comments the places where actual
|
||||
behavior differs from the spec.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import urlparse
|
||||
import urllib
|
||||
|
||||
import httplib2
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
from novaclient.v1_1 import Client
|
||||
from novaclient.v1_1.client import HTTPClient
|
||||
|
||||
from .utils import fail, assert_in, assert_not_in, assert_has_keys
|
||||
from novaclient import client as base_client
|
||||
from novaclient.v1_1 import client
|
||||
from tests import fakes
|
||||
|
||||
|
||||
def assert_equal(value_one, value_two):
|
||||
try:
|
||||
assert value_one == value_two
|
||||
except AssertionError:
|
||||
print "%(value_one)s does not equal %(value_two)s" % locals()
|
||||
raise
|
||||
class FakeClient(fakes.FakeClient, client.Client):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
client.Client.__init__(self, 'username', 'apikey',
|
||||
'project_id', 'auth_url')
|
||||
self.client = FakeHTTPClient(**kwargs)
|
||||
|
||||
|
||||
class FakeClient(Client):
|
||||
def __init__(self, username=None, password=None, project_id=None,
|
||||
auth_url=None):
|
||||
super(FakeClient, self).__init__('username', 'apikey',
|
||||
'project_id', 'auth_url')
|
||||
self.client = FakeHTTPClient()
|
||||
class FakeHTTPClient(base_client.HTTPClient):
|
||||
|
||||
def assert_called(self, method, url, body=None):
|
||||
"""
|
||||
Assert than an API method was just called.
|
||||
"""
|
||||
expected = (method, url)
|
||||
called = self.client.callstack[-1][0:2]
|
||||
|
||||
assert self.client.callstack, \
|
||||
"Expected %s %s but no calls were made." % expected
|
||||
|
||||
assert expected == called, 'Expected %s %s; got %s %s' % \
|
||||
(expected + called)
|
||||
|
||||
if body is not None:
|
||||
assert_equal(self.client.callstack[-1][2], body)
|
||||
|
||||
self.client.callstack = []
|
||||
|
||||
def assert_called_anytime(self, method, url, body=None):
|
||||
"""
|
||||
Assert than an API method was called anytime in the test.
|
||||
"""
|
||||
expected = (method, url)
|
||||
|
||||
assert self.client.callstack, \
|
||||
"Expected %s %s but no calls were made." % expected
|
||||
|
||||
found = False
|
||||
for entry in self.client.callstack:
|
||||
called = entry[0:2]
|
||||
if expected == entry[0:2]:
|
||||
found = True
|
||||
break
|
||||
|
||||
assert found, 'Expected %s; got %s' % \
|
||||
(expected, self.client.callstack)
|
||||
if body is not None:
|
||||
assert_equal(entry[2], body)
|
||||
|
||||
self.client.callstack = []
|
||||
|
||||
def authenticate(self):
|
||||
pass
|
||||
|
||||
|
||||
class FakeHTTPClient(HTTPClient):
|
||||
def __init__(self):
|
||||
def __init__(self, **kwargs):
|
||||
self.username = 'username'
|
||||
self.apikey = 'apikey'
|
||||
self.auth_url = 'auth_url'
|
||||
@ -89,15 +26,15 @@ class FakeHTTPClient(HTTPClient):
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
# Check that certain things are called correctly
|
||||
if method in ['GET', 'DELETE']:
|
||||
assert_not_in('body', kwargs)
|
||||
assert 'body' not in kwargs
|
||||
elif method in ['PUT', 'POST']:
|
||||
assert_in('body', kwargs)
|
||||
assert 'body' in kwargs
|
||||
|
||||
# Call the method
|
||||
munged_url = url.strip('/').replace('/', '_').replace('.', '_')
|
||||
callback = "%s_%s" % (method.lower(), munged_url)
|
||||
if not hasattr(self, callback):
|
||||
fail('Called unknown API method: %s %s' % (method, url))
|
||||
raise AssertionError('Called unknown API method: %s %s' % (method, url))
|
||||
|
||||
# Note the call
|
||||
self.callstack.append((method, url, kwargs.get('body', None)))
|
||||
@ -184,25 +121,27 @@ class FakeHTTPClient(HTTPClient):
|
||||
"name": "sample-server",
|
||||
"image": {
|
||||
"id": 2,
|
||||
"name": "sample image",
|
||||
},
|
||||
"flavor": {
|
||||
"id": 1,
|
||||
"name": "256 MB Server",
|
||||
},
|
||||
"hostId": "e4d909c290d0fb1ca068ffaddf22cbd0",
|
||||
"status": "BUILD",
|
||||
"progress": 60,
|
||||
"addresses": {
|
||||
"public": [{
|
||||
"addr": "1.2.3.4",
|
||||
"version": 4,
|
||||
"addr": "1.2.3.4",
|
||||
},
|
||||
{
|
||||
"addr": "5.6.7.8",
|
||||
"version": 4,
|
||||
"addr": "5.6.7.8",
|
||||
}],
|
||||
"private": [{
|
||||
"addr": "10.11.12.13",
|
||||
"version": 4,
|
||||
"addr": "10.11.12.13",
|
||||
}],
|
||||
},
|
||||
"metadata": {
|
||||
@ -215,24 +154,26 @@ class FakeHTTPClient(HTTPClient):
|
||||
"name": "sample-server2",
|
||||
"image": {
|
||||
"id": 2,
|
||||
"name": "sample image",
|
||||
},
|
||||
"flavor": {
|
||||
"id": 1,
|
||||
"name": "256 MB Server",
|
||||
},
|
||||
"hostId": "9e107d9d372bb6826bd81d3542a419d6",
|
||||
"status": "ACTIVE",
|
||||
"addresses": {
|
||||
"public": [{
|
||||
"addr": "1.2.3.5",
|
||||
"version": 4,
|
||||
"addr": "4.5.6.7",
|
||||
},
|
||||
{
|
||||
"addr": "5.6.7.9",
|
||||
"version": 4,
|
||||
"addr": "5.6.9.8",
|
||||
}],
|
||||
"private": [{
|
||||
"addr": "10.13.12.13",
|
||||
"version": 4,
|
||||
"addr": "10.13.12.13",
|
||||
}],
|
||||
},
|
||||
"metadata": {
|
||||
@ -242,13 +183,13 @@ class FakeHTTPClient(HTTPClient):
|
||||
]})
|
||||
|
||||
def post_servers(self, body, **kw):
|
||||
assert_equal(body.keys(), ['server'])
|
||||
assert_has_keys(body['server'],
|
||||
assert body.keys() == ['server']
|
||||
fakes.assert_has_keys(body['server'],
|
||||
required=['name', 'imageRef', 'flavorRef'],
|
||||
optional=['metadata', 'personality'])
|
||||
if 'personality' in body['server']:
|
||||
for pfile in body['server']['personality']:
|
||||
assert_has_keys(pfile, required=['path', 'contents'])
|
||||
fakes.assert_has_keys(pfile, required=['path', 'contents'])
|
||||
return (202, self.get_servers_1234()[1])
|
||||
|
||||
def get_servers_1234(self, **kw):
|
||||
@ -260,8 +201,8 @@ class FakeHTTPClient(HTTPClient):
|
||||
return (200, r)
|
||||
|
||||
def put_servers_1234(self, body, **kw):
|
||||
assert_equal(body.keys(), ['server'])
|
||||
assert_has_keys(body['server'], optional=['name', 'adminPass'])
|
||||
assert body.keys() == ['server']
|
||||
fakes.assert_has_keys(body['server'], optional=['name', 'adminPass'])
|
||||
return (204, None)
|
||||
|
||||
def delete_servers_1234(self, **kw):
|
||||
@ -283,12 +224,6 @@ class FakeHTTPClient(HTTPClient):
|
||||
return (200, {'private':
|
||||
self.get_servers_1234_ips()[1]['addresses']['private']})
|
||||
|
||||
def put_servers_1234_ips_public_1_2_3_4(self, body, **kw):
|
||||
assert_equal(body.keys(), ['shareIp'])
|
||||
assert_has_keys(body['shareIp'], required=['sharedIpGroupId',
|
||||
'configureServer'])
|
||||
return (202, None)
|
||||
|
||||
def delete_servers_1234_ips_public_1_2_3_4(self, **kw):
|
||||
return (202, None)
|
||||
|
||||
@ -297,27 +232,27 @@ class FakeHTTPClient(HTTPClient):
|
||||
#
|
||||
|
||||
def post_servers_1234_action(self, body, **kw):
|
||||
assert_equal(len(body.keys()), 1)
|
||||
assert len(body.keys()) == 1
|
||||
action = body.keys()[0]
|
||||
if action == 'reboot':
|
||||
assert_equal(body[action].keys(), ['type'])
|
||||
assert_in(body[action]['type'], ['HARD', 'SOFT'])
|
||||
assert body[action].keys() == ['type']
|
||||
assert body[action]['type'] in ['HARD', 'SOFT']
|
||||
elif action == 'rebuild':
|
||||
assert_equal(body[action].keys(), ['imageRef'])
|
||||
assert body[action].keys() == ['imageRef']
|
||||
elif action == 'resize':
|
||||
assert_equal(body[action].keys(), ['flavorRef'])
|
||||
assert body[action].keys() == ['flavorRef']
|
||||
elif action == 'confirmResize':
|
||||
assert_equal(body[action], None)
|
||||
assert body[action] is None
|
||||
# This one method returns a different response code
|
||||
return (204, None)
|
||||
elif action == 'revertResize':
|
||||
assert_equal(body[action], None)
|
||||
elif action == 'changePassword':
|
||||
assert_equal(body[action].keys(), ["adminPass"])
|
||||
assert body[action] is None
|
||||
elif action == 'createImage':
|
||||
assert_equal(body[action].keys(), ["name", "metadata"])
|
||||
assert set(body[action].keys()) == set(['name', 'metadata'])
|
||||
elif action == 'changePassword':
|
||||
assert body[action].keys() == ['adminPass']
|
||||
else:
|
||||
fail("Unexpected server action: %s" % action)
|
||||
raise AssertionError("Unexpected server action: %s" % action)
|
||||
return (202, None)
|
||||
|
||||
#
|
||||
@ -378,119 +313,9 @@ class FakeHTTPClient(HTTPClient):
|
||||
return (200, {'image': self.get_images_detail()[1]['images'][1]})
|
||||
|
||||
def post_images(self, body, **kw):
|
||||
assert_equal(body.keys(), ['image'])
|
||||
assert_has_keys(body['image'], required=['serverId', 'name', 'image_type', 'backup_type', 'rotation'])
|
||||
assert body.keys() == ['image']
|
||||
fakes.assert_has_keys(body['image'], required=['serverId', 'name'])
|
||||
return (202, self.get_images_1()[1])
|
||||
|
||||
def delete_images_1(self, **kw):
|
||||
return (204, None)
|
||||
|
||||
#
|
||||
# Backup schedules
|
||||
#
|
||||
def get_servers_1234_backup_schedule(self, **kw):
|
||||
return (200, {"backupSchedule": {
|
||||
"enabled": True,
|
||||
"weekly": "THURSDAY",
|
||||
"daily": "H_0400_0600"
|
||||
}})
|
||||
|
||||
def post_servers_1234_backup_schedule(self, body, **kw):
|
||||
assert_equal(body.keys(), ['backupSchedule'])
|
||||
assert_has_keys(body['backupSchedule'], required=['enabled'],
|
||||
optional=['weekly', 'daily'])
|
||||
return (204, None)
|
||||
|
||||
def delete_servers_1234_backup_schedule(self, **kw):
|
||||
return (204, None)
|
||||
|
||||
#
|
||||
# Shared IP groups
|
||||
#
|
||||
def get_shared_ip_groups(self, **kw):
|
||||
return (200, {'sharedIpGroups': [
|
||||
{'id': 1, 'name': 'group1'},
|
||||
{'id': 2, 'name': 'group2'},
|
||||
]})
|
||||
|
||||
def get_shared_ip_groups_detail(self, **kw):
|
||||
return (200, {'sharedIpGroups': [
|
||||
{'id': 1, 'name': 'group1', 'servers': [1234]},
|
||||
{'id': 2, 'name': 'group2', 'servers': [5678]},
|
||||
]})
|
||||
|
||||
def get_shared_ip_groups_1(self, **kw):
|
||||
return (200, {'sharedIpGroup':
|
||||
self.get_shared_ip_groups_detail()[1]['sharedIpGroups'][0]})
|
||||
|
||||
def post_shared_ip_groups(self, body, **kw):
|
||||
assert_equal(body.keys(), ['sharedIpGroup'])
|
||||
assert_has_keys(body['sharedIpGroup'], required=['name'],
|
||||
optional=['server'])
|
||||
return (201, {'sharedIpGroup': {
|
||||
'id': 10101,
|
||||
'name': body['sharedIpGroup']['name'],
|
||||
'servers': 'server' in body['sharedIpGroup'] and \
|
||||
[body['sharedIpGroup']['server']] or None
|
||||
}})
|
||||
|
||||
def delete_shared_ip_groups_1(self, **kw):
|
||||
return (204, None)
|
||||
|
||||
#
|
||||
# Zones
|
||||
#
|
||||
def get_zones(self, **kw):
|
||||
return (200, {'zones': [
|
||||
{'id': 1, 'api_url': 'http://foo.com', 'username': 'bob'},
|
||||
{'id': 2, 'api_url': 'http://foo.com', 'username': 'alice'},
|
||||
]})
|
||||
|
||||
|
||||
def get_zones_detail(self, **kw):
|
||||
return (200, {'zones': [
|
||||
{'id': 1, 'api_url': 'http://foo.com', 'username': 'bob',
|
||||
'password': 'qwerty'},
|
||||
{'id': 2, 'api_url': 'http://foo.com', 'username': 'alice',
|
||||
'password': 'password'}
|
||||
]})
|
||||
|
||||
def get_zones_1(self, **kw):
|
||||
r = {'zone': self.get_zones_detail()[1]['zones'][0]}
|
||||
return (200, r)
|
||||
|
||||
def get_zones_2(self, **kw):
|
||||
r = {'zone': self.get_zones_detail()[1]['zones'][1]}
|
||||
return (200, r)
|
||||
|
||||
def post_zones(self, body, **kw):
|
||||
assert_equal(body.keys(), ['zone'])
|
||||
assert_has_keys(body['zone'],
|
||||
required=['api_url', 'username', 'password'],
|
||||
optional=['weight_offset', 'weight_scale'])
|
||||
|
||||
return (202, self.get_zones_1()[1])
|
||||
|
||||
def put_zones_1(self, body, **kw):
|
||||
assert_equal(body.keys(), ['zone'])
|
||||
assert_has_keys(body['zone'], optional=['api_url', 'username',
|
||||
'password',
|
||||
'weight_offset',
|
||||
'weight_scale'])
|
||||
return (204, None)
|
||||
|
||||
def delete_zones_1(self, **kw):
|
||||
return (202, None)
|
||||
|
||||
#
|
||||
# Accounts
|
||||
#
|
||||
def post_accounts_test_account_create_instance(self, body, **kw):
|
||||
assert_equal(body.keys(), ['server'])
|
||||
assert_has_keys(body['server'],
|
||||
required=['name', 'imageRef', 'flavorRef'],
|
||||
optional=['metadata', 'personality'])
|
||||
if 'personality' in body['server']:
|
||||
for pfile in body['server']['personality']:
|
||||
assert_has_keys(pfile, required=['path', 'contents'])
|
||||
return (202, self.get_servers_1234()[1])
|
||||
|
74
tests/v1_1/test_auth.py
Normal file
74
tests/v1_1/test_auth.py
Normal file
@ -0,0 +1,74 @@
|
||||
|
||||
import httplib2
|
||||
import mock
|
||||
|
||||
from novaclient.v1_1 import client
|
||||
from novaclient import exceptions
|
||||
from tests import utils
|
||||
|
||||
|
||||
class AuthenticationTests(utils.TestCase):
|
||||
|
||||
def test_authenticate_success(self):
|
||||
cs = client.Client("username", "apikey", "project_id", "auth_url")
|
||||
management_url = 'https://servers.api.rackspacecloud.com/v1.1/443470'
|
||||
auth_response = httplib2.Response({
|
||||
'status': 204,
|
||||
'x-server-management-url': management_url,
|
||||
'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
|
||||
})
|
||||
mock_request = mock.Mock(return_value=(auth_response, None))
|
||||
|
||||
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||
def test_auth_call():
|
||||
cs.client.authenticate()
|
||||
headers={
|
||||
'X-Auth-User': 'username',
|
||||
'X-Auth-Key': 'apikey',
|
||||
'X-Auth-Project-Id': 'project_id',
|
||||
'User-Agent': cs.client.USER_AGENT
|
||||
}
|
||||
mock_request.assert_called_with(cs.client.auth_url, 'GET',
|
||||
headers=headers)
|
||||
self.assertEqual(cs.client.management_url,
|
||||
auth_response['x-server-management-url'])
|
||||
self.assertEqual(cs.client.auth_token,
|
||||
auth_response['x-auth-token'])
|
||||
|
||||
test_auth_call()
|
||||
|
||||
def test_authenticate_failure(self):
|
||||
cs = client.Client("username", "apikey", "project_id", "auth_url")
|
||||
auth_response = httplib2.Response({'status': 401})
|
||||
mock_request = mock.Mock(return_value=(auth_response, None))
|
||||
|
||||
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||
def test_auth_call():
|
||||
self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
|
||||
|
||||
test_auth_call()
|
||||
|
||||
def test_auth_automatic(self):
|
||||
cs = client.Client("username", "apikey", "project_id", "auth_url")
|
||||
http_client = cs.client
|
||||
http_client.management_url = ''
|
||||
mock_request = mock.Mock(return_value=(None, None))
|
||||
|
||||
@mock.patch.object(http_client, 'request', mock_request)
|
||||
@mock.patch.object(http_client, 'authenticate')
|
||||
def test_auth_call(m):
|
||||
http_client.get('/')
|
||||
m.assert_called()
|
||||
mock_request.assert_called()
|
||||
|
||||
test_auth_call()
|
||||
|
||||
def test_auth_manual(self):
|
||||
cs = client.Client("username", "apikey", "project_id", "auth_url")
|
||||
|
||||
@mock.patch.object(cs.client, 'authenticate')
|
||||
def test_auth_call(m):
|
||||
cs.authenticate()
|
||||
m.assert_called()
|
||||
|
||||
test_auth_call()
|
@ -1,61 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import mock
|
||||
from nose.tools import assert_equal, assert_not_equal, assert_raises
|
||||
|
||||
from novaclient.v1_1 import flavors
|
||||
from novaclient.v1_1 import exceptions
|
||||
from novaclient.v1_1 import base
|
||||
|
||||
from .fakes import FakeClient
|
||||
|
||||
os = FakeClient()
|
||||
|
||||
|
||||
def test_resource_repr():
|
||||
r = base.Resource(None, dict(foo="bar", baz="spam"))
|
||||
assert_equal(repr(r), "<Resource baz=spam, foo=bar>")
|
||||
|
||||
|
||||
def test_getid():
|
||||
assert_equal(base.getid(4), 4)
|
||||
|
||||
class O(object):
|
||||
id = 4
|
||||
assert_equal(base.getid(O), 4)
|
||||
|
||||
|
||||
def test_resource_lazy_getattr():
|
||||
f = flavors.Flavor(os.flavors, {'id': 1})
|
||||
assert_equal(f.name, '256 MB Server')
|
||||
os.assert_called('GET', '/flavors/1')
|
||||
|
||||
# Missing stuff still fails after a second get
|
||||
assert_raises(AttributeError, getattr, f, 'blahblah')
|
||||
os.assert_called('GET', '/flavors/1')
|
||||
|
||||
|
||||
def test_eq():
|
||||
# Two resources of the same type with the same id: equal
|
||||
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
|
||||
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
|
||||
assert_equal(r1, r2)
|
||||
|
||||
# Two resoruces of different types: never equal
|
||||
r1 = base.Resource(None, {'id': 1})
|
||||
r2 = flavors.Flavor(None, {'id': 1})
|
||||
assert_not_equal(r1, r2)
|
||||
|
||||
# Two resources with no ID: equal if their info is equal
|
||||
r1 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
assert_equal(r1, r2)
|
||||
|
||||
|
||||
def test_findall_invalid_attribute():
|
||||
# Make sure findall with an invalid attribute doesn't cause errors.
|
||||
# The following should not raise an exception.
|
||||
os.flavors.findall(vegetable='carrot')
|
||||
|
||||
# However, find() should raise an error
|
||||
assert_raises(exceptions.NotFound, os.flavors.find, vegetable='carrot')
|
@ -1,52 +0,0 @@
|
||||
import mock
|
||||
import httplib2
|
||||
|
||||
from novaclient.v1_1 import client
|
||||
from nose.tools import assert_equal
|
||||
|
||||
fake_response = httplib2.Response({"status": 200})
|
||||
fake_body = '{"hi": "there"}'
|
||||
mock_request = mock.Mock(return_value=(fake_response, fake_body))
|
||||
|
||||
|
||||
def get_client():
|
||||
cl = client.HTTPClient("username", "apikey", "project_id", "auth_test")
|
||||
cl.management_url = "http://example.com"
|
||||
cl.auth_token = "token"
|
||||
return cl
|
||||
|
||||
|
||||
def test_get():
|
||||
cl = get_client()
|
||||
|
||||
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||
@mock.patch('time.time', mock.Mock(return_value=1234))
|
||||
def test_get_call():
|
||||
resp, body = cl.get("/hi")
|
||||
mock_request.assert_called_with("http://example.com/hi?fresh=1234",
|
||||
"GET",
|
||||
headers={"X-Auth-Token": "token",
|
||||
"X-Auth-Project-Id": "project_id",
|
||||
"User-Agent": cl.USER_AGENT})
|
||||
# Automatic JSON parsing
|
||||
assert_equal(body, {"hi": "there"})
|
||||
|
||||
test_get_call()
|
||||
|
||||
|
||||
def test_post():
|
||||
cl = get_client()
|
||||
|
||||
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||
def test_post_call():
|
||||
cl.post("/hi", body=[1, 2, 3])
|
||||
mock_request.assert_called_with("http://example.com/hi", "POST",
|
||||
headers={
|
||||
"X-Auth-Token": "token",
|
||||
"X-Auth-Project-Id": "project_id",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": cl.USER_AGENT},
|
||||
body='[1, 2, 3]'
|
||||
)
|
||||
|
||||
test_post_call()
|
@ -1,42 +1,38 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from nose.tools import assert_raises, assert_equal
|
||||
|
||||
from novaclient import exceptions
|
||||
from novaclient.v1_1 import flavors
|
||||
from novaclient.v1_1 import exceptions
|
||||
|
||||
from .fakes import FakeClient
|
||||
from .utils import assert_isinstance
|
||||
|
||||
os = FakeClient()
|
||||
from tests.v1_1 import fakes
|
||||
from tests import utils
|
||||
|
||||
|
||||
def test_list_flavors():
|
||||
fl = os.flavors.list()
|
||||
os.assert_called('GET', '/flavors/detail')
|
||||
[assert_isinstance(f, flavors.Flavor) for f in fl]
|
||||
cs = fakes.FakeClient()
|
||||
|
||||
|
||||
def test_list_flavors_undetailed():
|
||||
fl = os.flavors.list(detailed=False)
|
||||
os.assert_called('GET', '/flavors')
|
||||
[assert_isinstance(f, flavors.Flavor) for f in fl]
|
||||
class FlavorsTest(utils.TestCase):
|
||||
|
||||
def test_list_flavors(self):
|
||||
fl = cs.flavors.list()
|
||||
cs.assert_called('GET', '/flavors/detail')
|
||||
[self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl]
|
||||
|
||||
def test_get_flavor_details():
|
||||
f = os.flavors.get(1)
|
||||
os.assert_called('GET', '/flavors/1')
|
||||
assert_isinstance(f, flavors.Flavor)
|
||||
assert_equal(f.ram, 256)
|
||||
assert_equal(f.disk, 10)
|
||||
def test_list_flavors_undetailed(self):
|
||||
fl = cs.flavors.list(detailed=False)
|
||||
cs.assert_called('GET', '/flavors')
|
||||
[self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl]
|
||||
|
||||
def test_get_flavor_details(self):
|
||||
f = cs.flavors.get(1)
|
||||
cs.assert_called('GET', '/flavors/1')
|
||||
self.assertTrue(isinstance(f, flavors.Flavor))
|
||||
self.assertEqual(f.ram, 256)
|
||||
self.assertEqual(f.disk, 10)
|
||||
|
||||
def test_find():
|
||||
f = os.flavors.find(ram=256)
|
||||
os.assert_called('GET', '/flavors/detail')
|
||||
assert_equal(f.name, '256 MB Server')
|
||||
def test_find(self):
|
||||
f = cs.flavors.find(ram=256)
|
||||
cs.assert_called('GET', '/flavors/detail')
|
||||
self.assertEqual(f.name, '256 MB Server')
|
||||
|
||||
f = os.flavors.find(disk=20)
|
||||
assert_equal(f.name, '512 MB Server')
|
||||
f = cs.flavors.find(disk=20)
|
||||
self.assertEqual(f.name, '512 MB Server')
|
||||
|
||||
assert_raises(exceptions.NotFound, os.flavors.find, disk=12345)
|
||||
self.assertRaises(exceptions.NotFound, cs.flavors.find, disk=12345)
|
||||
|
@ -1,51 +1,40 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from nose.tools import assert_equal
|
||||
|
||||
from novaclient.v1_1 import images
|
||||
|
||||
from .fakes import FakeClient
|
||||
from .utils import assert_isinstance
|
||||
|
||||
os = FakeClient()
|
||||
from tests.v1_1 import fakes
|
||||
from tests import utils
|
||||
|
||||
|
||||
def test_list_images():
|
||||
il = os.images.list()
|
||||
os.assert_called('GET', '/images/detail')
|
||||
[assert_isinstance(i, images.Image) for i in il]
|
||||
cs = fakes.FakeClient()
|
||||
|
||||
|
||||
def test_list_images_undetailed():
|
||||
il = os.images.list(detailed=False)
|
||||
os.assert_called('GET', '/images')
|
||||
[assert_isinstance(i, images.Image) for i in il]
|
||||
class ImagesTest(utils.TestCase):
|
||||
|
||||
def test_list_images(self):
|
||||
il = cs.images.list()
|
||||
cs.assert_called('GET', '/images/detail')
|
||||
[self.assertTrue(isinstance(i, images.Image)) for i in il]
|
||||
|
||||
def test_get_image_details():
|
||||
i = os.images.get(1)
|
||||
os.assert_called('GET', '/images/1')
|
||||
assert_isinstance(i, images.Image)
|
||||
assert_equal(i.id, 1)
|
||||
assert_equal(i.name, 'CentOS 5.2')
|
||||
def test_list_images_undetailed(self):
|
||||
il = cs.images.list(detailed=False)
|
||||
cs.assert_called('GET', '/images')
|
||||
[self.assertTrue(isinstance(i, images.Image)) for i in il]
|
||||
|
||||
def test_get_image_details(self):
|
||||
i = cs.images.get(1)
|
||||
cs.assert_called('GET', '/images/1')
|
||||
self.assertTrue(isinstance(i, images.Image))
|
||||
self.assertEqual(i.id, 1)
|
||||
self.assertEqual(i.name, 'CentOS 5.2')
|
||||
|
||||
def test_create_image():
|
||||
i = os.images.create(server=1234, name="Just in case")
|
||||
os.assert_called('POST', '/images')
|
||||
assert_isinstance(i, images.Image)
|
||||
def test_delete_image(self):
|
||||
cs.images.delete(1)
|
||||
cs.assert_called('DELETE', '/images/1')
|
||||
|
||||
def test_find(self):
|
||||
i = cs.images.find(name="CentOS 5.2")
|
||||
self.assertEqual(i.id, 1)
|
||||
cs.assert_called('GET', '/images/detail')
|
||||
|
||||
def test_delete_image():
|
||||
os.images.delete(1)
|
||||
os.assert_called('DELETE', '/images/1')
|
||||
|
||||
|
||||
def test_find():
|
||||
i = os.images.find(name="CentOS 5.2")
|
||||
assert_equal(i.id, 1)
|
||||
os.assert_called('GET', '/images/detail')
|
||||
|
||||
iml = os.images.findall(status='SAVING')
|
||||
assert_equal(len(iml), 1)
|
||||
assert_equal(iml[0].name, 'My Server Backup')
|
||||
iml = cs.images.findall(status='SAVING')
|
||||
self.assertEqual(len(iml), 1)
|
||||
self.assertEqual(iml[0].name, 'My Server Backup')
|
||||
|
@ -1,125 +1,114 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import StringIO
|
||||
|
||||
from nose.tools import assert_equal
|
||||
|
||||
from novaclient.v1_1 import servers
|
||||
|
||||
from .fakes import FakeClient
|
||||
from .utils import assert_isinstance
|
||||
from tests.v1_1 import fakes
|
||||
from tests import utils
|
||||
|
||||
|
||||
os = FakeClient()
|
||||
cs = fakes.FakeClient()
|
||||
|
||||
|
||||
def test_list_servers():
|
||||
sl = os.servers.list()
|
||||
os.assert_called('GET', '/servers/detail')
|
||||
[assert_isinstance(s, servers.Server) for s in sl]
|
||||
class ServersTest(utils.TestCase):
|
||||
|
||||
def test_list_servers(self):
|
||||
sl = cs.servers.list()
|
||||
cs.assert_called('GET', '/servers/detail')
|
||||
[self.assertTrue(isinstance(s, servers.Server)) for s in sl]
|
||||
|
||||
def test_list_servers_undetailed():
|
||||
sl = os.servers.list(detailed=False)
|
||||
os.assert_called('GET', '/servers')
|
||||
[assert_isinstance(s, servers.Server) for s in sl]
|
||||
def test_list_servers_undetailed(self):
|
||||
sl = cs.servers.list(detailed=False)
|
||||
cs.assert_called('GET', '/servers')
|
||||
[self.assertTrue(isinstance(s, servers.Server)) for s in sl]
|
||||
|
||||
def test_get_server_details(self):
|
||||
s = cs.servers.get(1234)
|
||||
cs.assert_called('GET', '/servers/1234')
|
||||
self.assertTrue(isinstance(s, servers.Server))
|
||||
self.assertEqual(s.id, 1234)
|
||||
self.assertEqual(s.status, 'BUILD')
|
||||
|
||||
def test_get_server_details():
|
||||
s = os.servers.get(1234)
|
||||
os.assert_called('GET', '/servers/1234')
|
||||
assert_isinstance(s, servers.Server)
|
||||
assert_equal(s.id, 1234)
|
||||
assert_equal(s.status, 'BUILD')
|
||||
def test_create_server(self):
|
||||
s = cs.servers.create(
|
||||
name="My server",
|
||||
image=1,
|
||||
flavor=1,
|
||||
meta={'foo': 'bar'},
|
||||
files={
|
||||
'/etc/passwd': 'some data', # a file
|
||||
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
|
||||
}
|
||||
)
|
||||
cs.assert_called('POST', '/servers')
|
||||
self.assertTrue(isinstance(s, servers.Server))
|
||||
|
||||
def test_update_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
|
||||
def test_create_server():
|
||||
s = os.servers.create(
|
||||
name="My server",
|
||||
image=1,
|
||||
flavor=1,
|
||||
meta={'foo': 'bar'},
|
||||
files={
|
||||
'/etc/passwd': 'some data', # a file
|
||||
'/tmp/foo.txt': StringIO.StringIO('data') # a stream
|
||||
}
|
||||
)
|
||||
os.assert_called('POST', '/servers')
|
||||
assert_isinstance(s, servers.Server)
|
||||
# Update via instance
|
||||
s.update(name='hi')
|
||||
cs.assert_called('PUT', '/servers/1234')
|
||||
s.update(name='hi')
|
||||
cs.assert_called('PUT', '/servers/1234')
|
||||
|
||||
# Silly, but not an error
|
||||
s.update()
|
||||
|
||||
def test_update_server():
|
||||
s = os.servers.get(1234)
|
||||
# Update via manager
|
||||
cs.servers.update(s, name='hi')
|
||||
cs.assert_called('PUT', '/servers/1234')
|
||||
|
||||
# Update via instance
|
||||
s.update(name='hi')
|
||||
os.assert_called('PUT', '/servers/1234')
|
||||
def test_delete_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.delete()
|
||||
cs.assert_called('DELETE', '/servers/1234')
|
||||
cs.servers.delete(1234)
|
||||
cs.assert_called('DELETE', '/servers/1234')
|
||||
cs.servers.delete(s)
|
||||
cs.assert_called('DELETE', '/servers/1234')
|
||||
|
||||
# Silly, but not an error
|
||||
s.update()
|
||||
def test_find(self):
|
||||
s = cs.servers.find(name='sample-server')
|
||||
cs.assert_called('GET', '/servers/detail')
|
||||
self.assertEqual(s.name, 'sample-server')
|
||||
|
||||
# Update via manager
|
||||
os.servers.update(s, name='hi')
|
||||
os.assert_called('PUT', '/servers/1234')
|
||||
# Find with multiple results arbitraility returns the first item
|
||||
s = cs.servers.find(flavor={"id": 1, "name": "256 MB Server"})
|
||||
sl = cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"})
|
||||
self.assertEqual(sl[0], s)
|
||||
self.assertEqual([s.id for s in sl], [1234, 5678])
|
||||
|
||||
def test_reboot_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.reboot()
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.reboot(s, type='HARD')
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
def test_delete_server():
|
||||
s = os.servers.get(1234)
|
||||
s.delete()
|
||||
os.assert_called('DELETE', '/servers/1234')
|
||||
os.servers.delete(1234)
|
||||
os.assert_called('DELETE', '/servers/1234')
|
||||
os.servers.delete(s)
|
||||
os.assert_called('DELETE', '/servers/1234')
|
||||
def test_rebuild_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.rebuild(image=1)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.rebuild(s, image=1)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
def test_resize_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.resize(flavor=1)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.resize(s, flavor=1)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
def test_find():
|
||||
s = os.servers.find(name='sample-server')
|
||||
os.assert_called('GET', '/servers/detail')
|
||||
assert_equal(s.name, 'sample-server')
|
||||
def test_confirm_resized_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.confirm_resize()
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.confirm_resize(s)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
# Find with multiple results arbitraility returns the first item
|
||||
s = os.servers.find(flavor_id=1)
|
||||
sl = os.servers.findall(flavor_id=1)
|
||||
assert_equal(sl[0], s)
|
||||
assert_equal([s.id for s in sl], [1234, 5678])
|
||||
|
||||
|
||||
def test_reboot_server():
|
||||
s = os.servers.get(1234)
|
||||
s.reboot()
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.reboot(s, type='HARD')
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
|
||||
def test_rebuild_server():
|
||||
s = os.servers.get(1234)
|
||||
s.rebuild(image=1)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.rebuild(s, image=1)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
|
||||
def test_resize_server():
|
||||
s = os.servers.get(1234)
|
||||
s.resize(flavor=1)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.resize(s, flavor=1)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
|
||||
def test_confirm_resized_server():
|
||||
s = os.servers.get(1234)
|
||||
s.confirm_resize()
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.confirm_resize(s)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
|
||||
|
||||
def test_revert_resized_server():
|
||||
s = os.servers.get(1234)
|
||||
s.revert_resize()
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
os.servers.revert_resize(s)
|
||||
os.assert_called('POST', '/servers/1234/action')
|
||||
def test_revert_resized_server(self):
|
||||
s = cs.servers.get(1234)
|
||||
s.revert_resize()
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
cs.servers.revert_resize(s)
|
||||
cs.assert_called('POST', '/servers/1234/action')
|
||||
|
@ -1,234 +1,210 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import mock
|
||||
import httplib2
|
||||
|
||||
from nose.tools import assert_raises, assert_equal
|
||||
|
||||
from novaclient.v1_1.shell import OpenStackShell, CommandError
|
||||
|
||||
from .fakes import FakeClient
|
||||
from .utils import assert_in
|
||||
from novaclient.shell import OpenStackComputeShell
|
||||
from novaclient import exceptions
|
||||
from tests.v1_1 import fakes
|
||||
from tests import utils
|
||||
|
||||
|
||||
# Patch os.environ to avoid required auth info.
|
||||
def setup():
|
||||
global _old_env
|
||||
fake_env = {
|
||||
'NOVA_USERNAME': 'username',
|
||||
'NOVA_API_KEY': 'password',
|
||||
'NOVA_PROJECT_ID': 'project_id'
|
||||
}
|
||||
_old_env, os.environ = os.environ, fake_env.copy()
|
||||
class ShellTest(utils.TestCase):
|
||||
|
||||
# Make a fake shell object, a helping wrapper to call it, and a quick way
|
||||
# of asserting that certain API calls were made.
|
||||
global shell, _shell, assert_called, assert_called_anytime
|
||||
_shell = OpenStackShell()
|
||||
_shell._api_class = FakeClient
|
||||
assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b)
|
||||
assert_called_anytime = lambda m, u, b=None: \
|
||||
_shell.cs.assert_called_anytime(m, u, b)
|
||||
shell = lambda cmd: _shell.main(cmd.split())
|
||||
|
||||
|
||||
def teardown():
|
||||
global _old_env
|
||||
os.environ = _old_env
|
||||
|
||||
|
||||
def test_boot():
|
||||
shell('boot --image 1 some-server')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1'}}
|
||||
)
|
||||
|
||||
shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
|
||||
'metadata': {'foo': 'bar', 'spam': 'eggs'}}}
|
||||
)
|
||||
|
||||
|
||||
def test_boot_files():
|
||||
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
|
||||
expected_file_data = open(testfile).read().encode('base64')
|
||||
|
||||
shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' %
|
||||
(testfile, testfile))
|
||||
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
|
||||
'personality': [
|
||||
{'path': '/tmp/bar', 'contents': expected_file_data},
|
||||
{'path': '/tmp/foo', 'contents': expected_file_data}
|
||||
]}
|
||||
# Patch os.environ to avoid required auth info.
|
||||
def setUp(self):
|
||||
global _old_env
|
||||
fake_env = {
|
||||
'NOVA_USERNAME': 'username',
|
||||
'NOVA_API_KEY': 'password',
|
||||
'NOVA_PROJECT_ID': 'project_id'
|
||||
}
|
||||
)
|
||||
_old_env, os.environ = os.environ, fake_env.copy()
|
||||
|
||||
# Make a fake shell object, a helping wrapper to call it, and a quick way
|
||||
# of asserting that certain API calls were made.
|
||||
global shell, _shell, assert_called, assert_called_anytime
|
||||
_shell = OpenStackComputeShell()
|
||||
_shell._api_class = fakes.FakeClient
|
||||
assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b)
|
||||
assert_called_anytime = lambda m, u, b=None: \
|
||||
_shell.cs.assert_called_anytime(m, u, b)
|
||||
shell = lambda cmd: _shell.main(cmd.split())
|
||||
|
||||
def test_boot_invalid_file():
|
||||
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
|
||||
assert_raises(CommandError, shell, 'boot some-server --image 1 '
|
||||
'--file /foo=%s' % invalid_file)
|
||||
def tearDown(self):
|
||||
global _old_env
|
||||
os.environ = _old_env
|
||||
|
||||
|
||||
def test_boot_key_auto():
|
||||
mock_exists = mock.Mock(return_value=True)
|
||||
mock_open = mock.Mock()
|
||||
mock_open.return_value = mock.Mock()
|
||||
mock_open.return_value.read = mock.Mock(return_value='SSHKEY')
|
||||
|
||||
@mock.patch('os.path.exists', mock_exists)
|
||||
@mock.patch('__builtin__.open', mock_open)
|
||||
def test_shell_call():
|
||||
shell('boot some-server --image 1 --key')
|
||||
def test_boot(self):
|
||||
shell('boot --image 1 some-server')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
|
||||
'personality': [{
|
||||
'path': '/root/.ssh/authorized_keys2',
|
||||
'contents': ('SSHKEY').encode('base64')},
|
||||
]}
|
||||
{'server': {
|
||||
'flavorRef': 1,
|
||||
'name': 'some-server',
|
||||
'imageRef': '1',
|
||||
'personality': [],
|
||||
'metadata': {},
|
||||
}}
|
||||
)
|
||||
|
||||
shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {
|
||||
'flavorRef': 1,
|
||||
'name': 'some-server',
|
||||
'imageRef': '1',
|
||||
'metadata': {'foo': 'bar', 'spam': 'eggs'},
|
||||
'personality': [],
|
||||
}}
|
||||
)
|
||||
|
||||
def test_boot_files(self):
|
||||
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
|
||||
expected_file_data = open(testfile).read().encode('base64')
|
||||
|
||||
cmd = 'boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s'
|
||||
shell(cmd % (testfile, testfile))
|
||||
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {
|
||||
'flavorRef': 1,
|
||||
'name': 'some-server',
|
||||
'imageRef': '1',
|
||||
'metadata': {},
|
||||
'personality': [
|
||||
{'path': '/tmp/bar', 'contents': expected_file_data},
|
||||
{'path': '/tmp/foo', 'contents': expected_file_data}
|
||||
]}
|
||||
}
|
||||
)
|
||||
|
||||
test_shell_call()
|
||||
def test_boot_invalid_file(self):
|
||||
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
|
||||
cmd = 'boot some-server --image 1 --file /foo=%s' % invalid_file
|
||||
self.assertRaises(exceptions.CommandError, shell, cmd)
|
||||
|
||||
def test_boot_key_auto(self):
|
||||
mock_exists = mock.Mock(return_value=True)
|
||||
mock_open = mock.Mock()
|
||||
mock_open.return_value = mock.Mock()
|
||||
mock_open.return_value.read = mock.Mock(return_value='SSHKEY')
|
||||
|
||||
def test_boot_key_auto_no_keys():
|
||||
mock_exists = mock.Mock(return_value=False)
|
||||
|
||||
@mock.patch('os.path.exists', mock_exists)
|
||||
def test_shell_call():
|
||||
assert_raises(CommandError, shell, 'boot some-server --image 1 --key')
|
||||
|
||||
test_shell_call()
|
||||
|
||||
|
||||
def test_boot_key_file():
|
||||
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
|
||||
expected_file_data = open(testfile).read().encode('base64')
|
||||
shell('boot some-server --image 1 --key %s' % testfile)
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
|
||||
'personality': [
|
||||
{'path': '/root/.ssh/authorized_keys2', 'contents':
|
||||
expected_file_data},
|
||||
@mock.patch('os.path.exists', mock_exists)
|
||||
@mock.patch('__builtin__.open', mock_open)
|
||||
def test_shell_call():
|
||||
shell('boot some-server --image 1 --key')
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {
|
||||
'flavorRef': 1,
|
||||
'name': 'some-server',
|
||||
'imageRef': '1',
|
||||
'metadata': {},
|
||||
'personality': [{
|
||||
'path': '/root/.ssh/authorized_keys2',
|
||||
'contents': ('SSHKEY').encode('base64')},
|
||||
]}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test_shell_call()
|
||||
|
||||
def test_boot_invalid_keyfile():
|
||||
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
|
||||
assert_raises(CommandError, shell, 'boot some-server '
|
||||
'--image 1 --key %s' % invalid_file)
|
||||
def test_boot_key_auto_no_keys(self):
|
||||
mock_exists = mock.Mock(return_value=False)
|
||||
|
||||
@mock.patch('os.path.exists', mock_exists)
|
||||
def test_shell_call():
|
||||
self.assertRaises(exceptions.CommandError, shell,
|
||||
'boot some-server --image 1 --key')
|
||||
|
||||
def test_flavor_list():
|
||||
shell('flavor-list')
|
||||
assert_called_anytime('GET', '/flavors/detail')
|
||||
test_shell_call()
|
||||
|
||||
def test_boot_key_file(self):
|
||||
testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
|
||||
expected_file_data = open(testfile).read().encode('base64')
|
||||
shell('boot some-server --image 1 --key %s' % testfile)
|
||||
assert_called(
|
||||
'POST', '/servers',
|
||||
{'server': {
|
||||
'flavorRef': 1,
|
||||
'name': 'some-server', 'imageRef': '1',
|
||||
'metadata': {},
|
||||
'personality': [
|
||||
{'path': '/root/.ssh/authorized_keys2',
|
||||
'contents':expected_file_data},
|
||||
]}
|
||||
}
|
||||
)
|
||||
|
||||
def test_image_list():
|
||||
shell('image-list')
|
||||
assert_called('GET', '/images/detail')
|
||||
def test_boot_invalid_keyfile(self):
|
||||
invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
|
||||
self.assertRaises(exceptions.CommandError, shell, 'boot some-server '
|
||||
'--image 1 --key %s' % invalid_file)
|
||||
|
||||
def test_flavor_list(self):
|
||||
shell('flavor-list')
|
||||
assert_called_anytime('GET', '/flavors/detail')
|
||||
|
||||
def test_create_image():
|
||||
shell('create-image sample-server mysnapshot')
|
||||
assert_called(
|
||||
'POST', '/servers/1234/action',
|
||||
{'createImage': {'name': 'mysnapshot', "metadata": {}}}
|
||||
)
|
||||
def test_image_list(self):
|
||||
shell('image-list')
|
||||
assert_called('GET', '/images/detail')
|
||||
|
||||
def test_create_image(self):
|
||||
shell('create-image sample-server mysnapshot')
|
||||
assert_called(
|
||||
'POST', '/servers/1234/action',
|
||||
{'createImage': {'name': 'mysnapshot', 'metadata': {}}}
|
||||
)
|
||||
|
||||
def test_image_delete():
|
||||
shell('image-delete 1')
|
||||
assert_called('DELETE', '/images/1')
|
||||
def test_image_delete(self):
|
||||
shell('image-delete 1')
|
||||
assert_called('DELETE', '/images/1')
|
||||
|
||||
def test_list(self):
|
||||
shell('list')
|
||||
assert_called('GET', '/servers/detail')
|
||||
|
||||
def test_list():
|
||||
shell('list')
|
||||
assert_called('GET', '/servers/detail')
|
||||
def test_reboot(self):
|
||||
shell('reboot sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
|
||||
shell('reboot sample-server --hard')
|
||||
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
|
||||
|
||||
def test_rebuild(self):
|
||||
shell('rebuild sample-server 1')
|
||||
assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1}})
|
||||
|
||||
def test_reboot():
|
||||
shell('reboot sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
|
||||
shell('reboot sample-server --hard')
|
||||
assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
|
||||
def test_rename(self):
|
||||
shell('rename sample-server newname')
|
||||
assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
|
||||
|
||||
def test_resize(self):
|
||||
shell('resize sample-server 1')
|
||||
assert_called('POST', '/servers/1234/action', {'resize': {'flavorRef': 1}})
|
||||
|
||||
def test_rebuild():
|
||||
shell('rebuild sample-server 1')
|
||||
assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1}})
|
||||
def test_resize_confirm(self):
|
||||
shell('resize-confirm sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'confirmResize': None})
|
||||
|
||||
def test_resize_revert(self):
|
||||
shell('resize-revert sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'revertResize': None})
|
||||
|
||||
def test_rename():
|
||||
shell('rename sample-server newname')
|
||||
assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
|
||||
@mock.patch('getpass.getpass', mock.Mock(return_value='p'))
|
||||
def test_root_password(self):
|
||||
shell('root-password sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}})
|
||||
|
||||
def test_show(self):
|
||||
shell('show 1234')
|
||||
# XXX need a way to test multiple calls
|
||||
# assert_called('GET', '/servers/1234')
|
||||
assert_called('GET', '/images/2')
|
||||
|
||||
def test_resize():
|
||||
shell('resize sample-server 1')
|
||||
assert_called('POST', '/servers/1234/action', {'resize': {'flavorRef': 1}})
|
||||
|
||||
|
||||
def test_resize_confirm():
|
||||
shell('resize-confirm sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'confirmResize': None})
|
||||
|
||||
|
||||
def test_resize_revert():
|
||||
shell('resize-revert sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'revertResize': None})
|
||||
|
||||
|
||||
@mock.patch('getpass.getpass', mock.Mock(return_value='p'))
|
||||
def test_root_password():
|
||||
shell('root-password sample-server')
|
||||
assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}})
|
||||
|
||||
|
||||
def test_show():
|
||||
shell('show 1234')
|
||||
# XXX need a way to test multiple calls
|
||||
# assert_called('GET', '/servers/1234')
|
||||
assert_called('GET', '/images/2')
|
||||
|
||||
|
||||
def test_delete():
|
||||
shell('delete 1234')
|
||||
assert_called('DELETE', '/servers/1234')
|
||||
shell('delete sample-server')
|
||||
assert_called('DELETE', '/servers/1234')
|
||||
|
||||
|
||||
def test_help():
|
||||
@mock.patch.object(_shell.parser, 'print_help')
|
||||
def test_help(m):
|
||||
shell('help')
|
||||
m.assert_called()
|
||||
|
||||
@mock.patch.object(_shell.subcommands['delete'], 'print_help')
|
||||
def test_help_delete(m):
|
||||
shell('help delete')
|
||||
m.assert_called()
|
||||
|
||||
test_help()
|
||||
test_help_delete()
|
||||
|
||||
assert_raises(CommandError, shell, 'help foofoo')
|
||||
|
||||
|
||||
def test_debug():
|
||||
httplib2.debuglevel = 0
|
||||
shell('--debug list')
|
||||
assert httplib2.debuglevel == 1
|
||||
def test_delete(self):
|
||||
shell('delete 1234')
|
||||
assert_called('DELETE', '/servers/1234')
|
||||
shell('delete sample-server')
|
||||
assert_called('DELETE', '/servers/1234')
|
||||
|
@ -1 +1 @@
|
||||
OH HAI!
|
||||
BLAH
|
||||
|
Loading…
x
Reference in New Issue
Block a user